-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…9378) Add inventory plugin iocage (#9262) * Add inventory plugin iocage #9261 * inventory/iocage.py BOTMETA entry added. * Remove missing methods from the test. * Avoid shell mode. * Parameter host is optional default=localhost * Fix AnsibleError calls. * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * iocage_data removed. Not necessarily defined at this point. * Description mentions that remote hosts are contacted via SSH. * test inventory iocage * Create get_jails and get_properties in iocage plugin to simplify testing. * Update test_iocage.py * Add fixtures iocage_* * Update documentation. * Update documentation. * Update documentation. * Fix localhost environment. * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/inventory/iocage.py Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Felix Fontein <felix@fontein.de> (cherry picked from commit 23d97fa) Co-authored-by: Vladimir Botka <vbotka@gmail.com>
- Loading branch information
1 parent
f6fa7fb
commit 60ba39d
Showing
19 changed files
with
1,791 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
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,266 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright (c) 2024 Vladimir Botka <vbotka@gmail.com> | ||
# 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). | ||
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: dict | ||
default: {} | ||
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 | ||
# 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 | ||
import os | ||
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 = [] | ||
my_env = os.environ.copy() | ||
if host == 'localhost': | ||
my_env.update({str(k): str(v) for k, v in env.items()}) | ||
else: | ||
user = self.get_option('user') | ||
cmd.append("ssh") | ||
cmd.append(f"{user}@{host}") | ||
cmd.extend([f"{k}={v}" for k, v in env.items()]) | ||
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, env=my_env) | ||
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, env=my_env) | ||
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) |
Oops, something went wrong.