Skip to content

Commit

Permalink
Add inventory plugin iocage ansible-collections#9261
Browse files Browse the repository at this point in the history
  • Loading branch information
vbotka committed Dec 16, 2024
1 parent f6dae1f commit 2250bf7
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 0 deletions.
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).
extends_documentation_fragment:
- constructed
- 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/name of the C(iocage) host.
required: true
type: str
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).
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: []
notes:
- You might want to test the command C(ssh user@host iocage list -l)
on the controller before using this inventory plugin.
- 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
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
# 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)
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)))

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)))

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)
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)
32 changes: 32 additions & 0 deletions tests/unit/plugins/inventory/test_iocage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- 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_init_cache(inventory):
inventory._init_cache()
assert inventory._cache[inventory.cache_key] == {}


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


def test_verify_file_bad_config(inventory):
assert inventory.verify_file('foobar.iocage.yml') is False

0 comments on commit 2250bf7

Please sign in to comment.