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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
251 changes: 251 additions & 0 deletions plugins/inventory/iocage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# -*- 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.
- 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:
- constructed
- inventory_cache
vbotka marked this conversation as resolved.
Show resolved Hide resolved
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/name of the C(iocage) host.
vbotka marked this conversation as resolved.
Show resolved Hide resolved
required: true
type: str
russoz marked this conversation as resolved.
Show resolved Hide resolved
user:
description:
- C(iocage) user.
It is expected that the O(user) is able to connect to the
O(host) and execute the command C(iocage list).
vbotka marked this conversation as resolved.
Show resolved Hide resolved
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.
russoz marked this conversation as resolved.
Show resolved Hide resolved
vbotka marked this conversation as resolved.
Show resolved Hide resolved
- 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)
vbotka marked this conversation as resolved.
Show resolved Hide resolved
- 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
plugin: community.general.iocage
host: localhost

# 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 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(f"ssh {user}@{host}")
if env:
cmd.append(' '.join(env))
cmd.append(self.IOCAGE)

cmd_list = cmd.copy()
cmd_list.append('list --header --long')
cmd_list = ' '.join(cmd_list)
try:
p = Popen(cmd_list, shell=True, text=True, stdout=PIPE, stderr=PIPE)
russoz marked this conversation as resolved.
Show resolved Hide resolved
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleParserError('Failed to run cmd=%s, rc=%s, stderr=%s' %
(cmd_list, p.returncode, to_native(stderr)))
russoz marked this conversation as resolved.
Show resolved Hide resolved

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

iocage_data = [x.split() for x in t_stdout.splitlines()]
results = {'_meta': {'hostvars': {}}}

for jail in iocage_data:
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]

except Exception as e:
raise AnsibleParserError('Failed to parse %s: %s. iocage_data: %s' %
(to_native(path), to_native(e), to_native(iocage_data)))
vbotka marked this conversation as resolved.
Show resolved Hide resolved

if get_properties:
for hostname, host_vars in results['_meta']['hostvars'].items():
cmd_get_properties = cmd.copy()
cmd_get_properties.append(f"get --all {hostname}")
cmd_get_properties = ' '.join(cmd_get_properties)
try:
p = Popen(cmd_get_properties, shell=True, text=True, stdout=PIPE, stderr=PIPE)
russoz marked this conversation as resolved.
Show resolved Hide resolved
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleParserError('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 AnsibleParserError('Invalid (non unicode) input returned: %s' % to_native(e))

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

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

return results

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)
23 changes: 23 additions & 0 deletions tests/unit/plugins/inventory/test_iocage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- 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

import pytest

from ansible_collections.community.general.plugins.inventory.iocage import InventoryModule


@pytest.fixture(scope="module")
def inventory():
return InventoryModule()


def test_verify_file(tmp_path, inventory):
file = tmp_path / "foobar.iocage.yml"
file.touch()
assert inventory.verify_file(str(file)) is True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have more extensive tests, than this absolutely bare minimum :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completed the tests.

Loading