forked from ansible-collections/community.general
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add inventory plugin iocage ansible-collections#9261
- Loading branch information
Showing
2 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |