Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inventory plugin iocage #9262

Merged
merged 22 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ files:
maintainers: opoplawski
$inventories/gitlab_runners.py:
maintainers: morph027
$inventories/iocage.py:
maintainers: vbotka
$inventories/icinga2.py:
maintainers: BongoEADGC6
$inventories/linode.py:
Expand Down
264 changes: 264 additions & 0 deletions plugins/inventory/iocage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-

# Copyright (c) 2024 Vladimir Botka <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
name: iocage
short_description: iocage inventory source
version_added: 10.2.0
author:
- Vladimir Botka (@vbotka)
requirements:
- iocage >= 1.8
description:
- Get inventory hosts from the iocage jail manager running on O(host).
- By default, O(host) is V(localhost). If O(host) is not V(localhost) it
is expected that the user running Ansible on the controller can
connect to the O(host) account O(user) with SSH non-interactively and
execute the command C(iocage list).
- Uses a configuration file as an inventory source, it must end
in C(.iocage.yml) or C(.iocage.yaml).
russoz marked this conversation as resolved.
Show resolved Hide resolved
extends_documentation_fragment:
- ansible.builtin.constructed
- ansible.builtin.inventory_cache
options:
plugin:
description:
- The name of this plugin, it should always be set to
V(community.general.iocage) for this plugin to recognize
it as its own.
required: true
choices: ['community.general.iocage']
type: str
host:
description: The IP/hostname of the C(iocage) host.
type: str
default: localhost
user:
description:
- C(iocage) user.
It is expected that the O(user) is able to connect to the
O(host) with SSH and execute the command C(iocage list).
This option is not required if O(host) is V(localhost).
type: str
get_properties:
description:
- Get jails' properties.
Creates dictionary C(iocage_properties) for each added host.
type: boolean
default: false
env:
description: O(user)'s environment on O(host).
type: list
elements: str
default: []
russoz marked this conversation as resolved.
Show resolved Hide resolved
notes:
- You might want to test the command C(ssh user@host iocage list -l) on
the controller before using this inventory plugin with O(user) specified
and with O(host) other than V(localhost).
- If you run this inventory plugin on V(localhost) C(ssh) is not used.
In this case, test the command C(iocage list -l).
- This inventory plugin creates variables C(iocage_*) for each added host.
- The values of these variables are collected from the output of the
command C(iocage list -l).
- The names of these variables correspond to the output columns.
- The column C(NAME) is used to name the added host.
'''

EXAMPLES = '''
# file name must end with iocage.yaml or iocage.yml
plugin: community.general.iocage
host: 10.1.0.73
user: admin

# user is not required if iocage is running on localhost (default)
plugin: community.general.iocage

# run cryptography without legacy algorithms
plugin: community.general.iocage
host: 10.1.0.73
user: admin
env:
- CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
russoz marked this conversation as resolved.
Show resolved Hide resolved

# enable cache
plugin: community.general.iocage
host: 10.1.0.73
user: admin
env:
- CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
cache: true

# see inventory plugin ansible.builtin.constructed
plugin: community.general.iocage
host: 10.1.0.73
user: admin
env:
- CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
cache: true
strict: false
compose:
ansible_host: iocage_ip4
release: iocage_release | split('-') | first
groups:
test: inventory_hostname.startswith('test')
keyed_groups:
- prefix: distro
key: iocage_release
- prefix: state
key: iocage_state
'''

import re
from subprocess import Popen, PIPE

from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.utils.display import Display

display = Display()


def _parse_ip4(ip4):
if ip4 == '-':
return ip4
return re.split('\\||/', ip4)[1]


class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
''' Host inventory parser for ansible using iocage as source. '''

NAME = 'community.general.iocage'
IOCAGE = '/usr/local/bin/iocage'

def __init__(self):
super(InventoryModule, self).__init__()

def verify_file(self, path):
valid = False
if super(InventoryModule, self).verify_file(path):
if path.endswith(('iocage.yaml', 'iocage.yml')):
valid = True
else:
self.display.vvv('Skipping due to inventory source not ending in "iocage.yaml" nor "iocage.yml"')
return valid

def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)

self._read_config_data(path)
cache_key = self.get_cache_key(path)

user_cache_setting = self.get_option('cache')
attempt_to_read_cache = user_cache_setting and cache
cache_needs_update = user_cache_setting and not cache

if attempt_to_read_cache:
try:
results = self._cache[cache_key]
except KeyError:
cache_needs_update = True
if not attempt_to_read_cache or cache_needs_update:
results = self.get_inventory(path)
if cache_needs_update:
self._cache[cache_key] = results

self.populate(results)

def get_inventory(self, path):
host = self.get_option('host')
env = self.get_option('env')
get_properties = self.get_option('get_properties')

cmd = []
if host != 'localhost':
user = self.get_option('user')
cmd.append("ssh")
cmd.append(f"{user}@{host}")
if env:
cmd.extend(env)
russoz marked this conversation as resolved.
Show resolved Hide resolved
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
cmd.append(self.IOCAGE)

cmd_list = cmd.copy()
cmd_list.append('list')
cmd_list.append('--header')
cmd_list.append('--long')
try:
p = Popen(cmd_list, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError('Failed to run cmd=%s, rc=%s, stderr=%s' %
(cmd_list, p.returncode, to_native(stderr)))

try:
t_stdout = to_text(stdout, errors='surrogate_or_strict')
except UnicodeError as e:
raise AnsibleError('Invalid (non unicode) input returned: %s' % to_native(e)) from e

except Exception as e:
raise AnsibleParserError('Failed to parse %s: %s' %
(to_native(path), to_native(e))) from e

results = {'_meta': {'hostvars': {}}}
self.get_jails(t_stdout, results)

if get_properties:
for hostname, host_vars in results['_meta']['hostvars'].items():
cmd_get_properties = cmd.copy()
cmd_get_properties.append("get")
cmd_get_properties.append("--all")
cmd_get_properties.append(f"{hostname}")
try:
p = Popen(cmd_get_properties, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError('Failed to run cmd=%s, rc=%s, stderr=%s' %
(cmd_get_properties, p.returncode, to_native(stderr)))

try:
t_stdout = to_text(stdout, errors='surrogate_or_strict')
except UnicodeError as e:
raise AnsibleError('Invalid (non unicode) input returned: %s' % to_native(e)) from e

except Exception as e:
raise AnsibleError('Failed to get properties: %s' % to_native(e)) from e

self.get_properties(t_stdout, results, hostname)

return results

def get_jails(self, t_stdout, results):
jails = [x.split() for x in t_stdout.splitlines()]
for jail in jails:
iocage_name = jail[1]
results['_meta']['hostvars'][iocage_name] = {}
results['_meta']['hostvars'][iocage_name]['iocage_jid'] = jail[0]
results['_meta']['hostvars'][iocage_name]['iocage_boot'] = jail[2]
results['_meta']['hostvars'][iocage_name]['iocage_state'] = jail[3]
results['_meta']['hostvars'][iocage_name]['iocage_type'] = jail[4]
results['_meta']['hostvars'][iocage_name]['iocage_release'] = jail[5]
results['_meta']['hostvars'][iocage_name]['iocage_ip4'] = _parse_ip4(jail[6])
results['_meta']['hostvars'][iocage_name]['iocage_ip6'] = jail[7]
results['_meta']['hostvars'][iocage_name]['iocage_template'] = jail[8]
results['_meta']['hostvars'][iocage_name]['iocage_basejail'] = jail[9]

def get_properties(self, t_stdout, results, hostname):
properties = dict([x.split(':', 1) for x in t_stdout.splitlines()])
results['_meta']['hostvars'][hostname]['iocage_properties'] = properties

def populate(self, results):
strict = self.get_option('strict')

for hostname, host_vars in results['_meta']['hostvars'].items():
self.inventory.add_host(hostname, group='all')
for var, value in host_vars.items():
self.inventory.set_variable(hostname, var, value)
self._set_composite_vars(self.get_option('compose'), host_vars, hostname, strict=True)
self._add_host_to_composed_groups(self.get_option('groups'), host_vars, hostname, strict=strict)
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host_vars, hostname, strict=strict)
Loading
Loading