1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

lxd: dynamic inventory (#2026)

* lxd dynamic inventory and test data

* added ``merge_profile`` parameter to merge configurations from the play to an existing profile

* cosmetic changes

* added ``merge_profile`` parameter to merge configurations from the play to an existing profile

* cosmetic changes

* fix pylint errors

* fix flake8 warnings

* fix pep8 errors without "line to long"

* fix ansible tests

* fix typo

* fix version added

* fix lost of suggestions from felixfontein

* fix filter
fix ansible test errors

* delete test config

* delete 'notes:' and copy content to description

* move testdata
load testdata by path from config

* updated documentation

* fix test data and remove inventory branch

* fix spellings and rename lxd to community.general.lxd

* fix documentation

* remove selftest

* strip example data

* add unit test

* switch to ansible.module_utils.common.dict_transformations

* documentation cleanup

* move lxd_inventory.atd from files to fixtures

* update documentation
move lxd_inventory.atd

* rename self.groups to self dispose
remove dumpdata

* cleanup

* fix unittests
comment out dump_data, it breaks the unit tests

* fix pep8

* Apply suggestions from code review

* Update plugins/inventory/lxd.py

* add test if no groupby is selected

* rename disposed to groupby
remove unused constant
other suggested cleanups

* Use bundled ipaddress instead of own code.

* Update plugins/inventory/lxd.py

* Exceptions should not be eaten.

* Improve error handling for network range/address parsing.

* Fix typo.

* Make network range valid.

* Do not error when groupby is not a dict.

Co-authored-by: Frank Dornheim <“dornheim@posteo.de@users.noreply.github.com”>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Frank Dornheim 2021-04-26 18:25:45 +02:00 committed by GitHub
parent 99c564398a
commit 2cc3ce0230
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 1224 additions and 0 deletions

950
plugins/inventory/lxd.py Normal file
View file

@ -0,0 +1,950 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Frank Dornheim <dornheim@posteo.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
name: community.general.lxd
short_description: Returns Ansible inventory from lxd host
description:
- Get inventory from the lxd.
- Uses a YAML configuration file that ends with 'lxd.(yml|yaml)'.
version_added: "3.0.0"
author: "Frank Dornheim (@conloos)"
options:
plugin:
description: Token that ensures this is a source file for the 'lxd' plugin.
required: true
choices: [ 'community.general.lxd' ]
url:
description:
- The unix domain socket path or the https URL for the lxd server.
- Sockets in filesystem have to start with C(unix:).
- Mostly C(unix:/var/lib/lxd/unix.socket) or C(unix:/var/snap/lxd/common/lxd/unix.socket).
default: unix:/var/snap/lxd/common/lxd/unix.socket
type: str
client_key:
description:
- The client certificate key file path.
aliases: [ key_file ]
default: $HOME/.config/lxc/client.key
type: path
client_cert:
description:
- The client certificate file path.
aliases: [ cert_file ]
default: $HOME/.config/lxc/client.crt
type: path
trust_password:
description:
- The client trusted password.
- You need to set this password on the lxd server before
running this module using the following command
C(lxc config set core.trust_password <some random password>)
See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/).
- If I(trust_password) is set, this module send a request for authentication before sending any requests.
type: str
state:
description: Filter the container according to the current status.
type: str
default: none
choices: [ 'STOPPED', 'STARTING', 'RUNNING', 'none' ]
prefered_container_network_interface:
description:
- If a container has multiple network interfaces, select which one is the prefered as pattern.
- Combined with the first number that can be found e.g. 'eth' + 0.
type: str
default: eth
prefered_container_network_family:
description:
- If a container has multiple network interfaces, which one is the prefered by family.
- Specify C(inet) for IPv4 and C(inet6) for IPv6.
type: str
default: inet
choices: [ 'inet', 'inet6' ]
groupby:
description:
- Create groups by the following keywords C(location), C(pattern), C(network_range), C(os), C(release), C(profile), C(vlanid).
- See example for syntax.
type: json
'''
EXAMPLES = '''
# simple lxd.yml
plugin: community.general.lxd
url: unix:/var/snap/lxd/common/lxd/unix.socket
# simple lxd.yml including filter
plugin: community.general.lxd
url: unix:/var/snap/lxd/common/lxd/unix.socket
state: RUNNING
# grouping lxd.yml
groupby:
testpattern:
type: pattern
attribute: test
vlan666:
type: vlanid
attribute: 666
locationBerlin:
type: location
attribute: Berlin
osUbuntu:
type: os
attribute: ubuntu
releaseFocal:
type: release
attribute: focal
releaseBionic:
type: release
attribute: bionic
profileDefault:
type: profile
attribute: default
profileX11:
type: profile
attribute: x11
netRangeIPv4:
type: network_range
attribute: 10.98.143.0/24
netRangeIPv6:
type: network_range
attribute: fd42:bd00:7b11:2167:216:3eff::/24
'''
import binascii
import json
import re
import time
import os
import socket
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible.errors import AnsibleError, AnsibleParserError
from ansible_collections.community.general.plugins.module_utils.compat import ipaddress
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
class InventoryModule(BaseInventoryPlugin):
DEBUG = 4
NAME = 'community.general.lxd'
SNAP_SOCKET_URL = 'unix:/var/snap/lxd/common/lxd/unix.socket'
SOCKET_URL = 'unix:/var/lib/lxd/unix.socket'
@staticmethod
def load_json_data(path):
"""Load json data
Load json data from file
Args:
list(path): Path elements
str(file_name): Filename of data
Kwargs:
None
Raises:
None
Returns:
dict(json_data): json data"""
try:
with open(path, 'r') as json_file:
return json.load(json_file)
except (IOError, json.decoder.JSONDecodeError) as err:
raise AnsibleParserError('Could not load the test data from {0}: {1}'.format(to_native(path), to_native(err)))
def save_json_data(self, path, file_name=None):
"""save data as json
Save data as json file
Args:
list(path): Path elements
str(file_name): Filename of data
Kwargs:
None
Raises:
None
Returns:
None"""
if file_name:
path.append(file_name)
else:
prefix = 'lxd_data-'
time_stamp = time.strftime('%Y%m%d-%H%M%S')
suffix = '.atd'
path.append(prefix + time_stamp + suffix)
try:
cwd = os.path.abspath(os.path.dirname(__file__))
with open(os.path.abspath(os.path.join(cwd, *path)), 'w') as json_file:
json.dump(self.data, json_file)
except IOError as err:
raise AnsibleParserError('Could not save data: {0}'.format(to_native(err)))
def verify_file(self, path):
"""Check the config
Return true/false if the config-file is valid for this plugin
Args:
str(path): path to the config
Kwargs:
None
Raises:
None
Returns:
bool(valid): is valid"""
valid = False
if super(InventoryModule, self).verify_file(path):
if path.endswith(('lxd.yaml', 'lxd.yml')):
valid = True
else:
self.display.vvv('Inventory source not ending in "lxd.yaml" or "lxd.yml"')
return valid
@staticmethod
def validate_url(url):
"""validate url
check whether the url is correctly formatted
Args:
url
Kwargs:
None
Raises:
AnsibleError
Returns:
bool"""
if not isinstance(url, str):
return False
if not url.startswith(('unix:', 'https:')):
raise AnsibleError('URL is malformed: {0}'.format(to_native(url)))
return True
def _connect_to_socket(self):
"""connect to lxd socket
Connect to lxd socket by provided url or defaults
Args:
None
Kwargs:
None
Raises:
AnsibleError
Returns:
None"""
error_storage = {}
url_list = [self.get_option('url'), self.SNAP_SOCKET_URL, self.SOCKET_URL]
urls = (url for url in url_list if self.validate_url(url))
for url in urls:
try:
socket_connection = LXDClient(url, self.client_key, self.client_cert, self.debug)
return socket_connection
except LXDClientException as err:
error_storage[url] = err
raise AnsibleError('No connection to the socket: {0}'.format(to_native(error_storage)))
def _get_networks(self):
"""Get Networknames
Returns all network config names
Args:
None
Kwargs:
None
Raises:
None
Returns:
list(names): names of all network_configs"""
# e.g. {'type': 'sync',
# 'status': 'Success',
# 'status_code': 200,
# 'operation': '',
# 'error_code': 0,
# 'error': '',
# 'metadata': ['/1.0/networks/lxdbr0']}
network_configs = self.socket.do('GET', '/1.0/networks')
return [m.split('/')[3] for m in network_configs['metadata']]
def _get_containers(self):
"""Get Containernames
Returns all containernames
Args:
None
Kwargs:
None
Raises:
None
Returns:
list(names): names of all containers"""
# e.g. {'type': 'sync',
# 'status': 'Success',
# 'status_code': 200,
# 'operation': '',
# 'error_code': 0,
# 'error': '',
# 'metadata': ['/1.0/containers/udemy-ansible-ubuntu-2004']}
containers = self.socket.do('GET', '/1.0/containers')
return [m.split('/')[3] for m in containers['metadata']]
def _get_config(self, branch, name):
"""Get inventory of container
Get config of container
Args:
str(branch): Name oft the API-Branch
str(name): Name of Container
Kwargs:
None
Source:
https://github.com/lxc/lxd/blob/master/doc/rest-api.md
Raises:
None
Returns:
dict(config): Config of the container"""
config = {}
if isinstance(branch, (tuple, list)):
config[name] = {branch[1]: self.socket.do('GET', '/1.0/{0}/{1}/{2}'.format(to_native(branch[0]), to_native(name), to_native(branch[1])))}
else:
config[name] = {branch: self.socket.do('GET', '/1.0/{0}/{1}'.format(to_native(branch), to_native(name)))}
return config
def get_container_data(self, names):
"""Create Inventory of the container
Iterate through the different branches of the containers and collect Informations.
Args:
list(names): List of container names
Kwargs:
None
Raises:
None
Returns:
None"""
# tuple(('instances','metadata/templates')) to get section in branch
# e.g. /1.0/instances/<name>/metadata/templates
branches = ['containers', ('instances', 'state')]
container_config = {}
for branch in branches:
for name in names:
container_config['containers'] = self._get_config(branch, name)
self.data = dict_merge(container_config, self.data)
def get_network_data(self, names):
"""Create Inventory of the container
Iterate through the different branches of the containers and collect Informations.
Args:
list(names): List of container names
Kwargs:
None
Raises:
None
Returns:
None"""
# tuple(('instances','metadata/templates')) to get section in branch
# e.g. /1.0/instances/<name>/metadata/templates
branches = [('networks', 'state')]
network_config = {}
for branch in branches:
for name in names:
try:
network_config['networks'] = self._get_config(branch, name)
except LXDClientException:
network_config['networks'] = {name: None}
self.data = dict_merge(network_config, self.data)
def extract_network_information_from_container_config(self, container_name):
"""Returns the network interface configuration
Returns the network ipv4 and ipv6 config of the container without local-link
Args:
str(container_name): Name oft he container
Kwargs:
None
Raises:
None
Returns:
dict(network_configuration): network config"""
container_network_interfaces = self._get_data_entry('containers/{0}/state/metadata/network'.format(container_name))
network_configuration = None
if container_network_interfaces:
network_configuration = {}
gen_interface_names = [interface_name for interface_name in container_network_interfaces if interface_name != 'lo']
for interface_name in gen_interface_names:
gen_address = [address for address in container_network_interfaces[interface_name]['addresses'] if address.get('scope') != 'link']
network_configuration[interface_name] = []
for address in gen_address:
address_set = {}
address_set['family'] = address.get('family')
address_set['address'] = address.get('address')
address_set['netmask'] = address.get('netmask')
address_set['combined'] = address.get('address') + '/' + address.get('netmask')
network_configuration[interface_name].append(address_set)
return network_configuration
def get_prefered_container_network_interface(self, container_name):
"""Helper to get the prefered interface of thr container
Helper to get the prefered interface provide by neme pattern from 'prefered_container_network_interface'.
Args:
str(containe_name): name of container
Kwargs:
None
Raises:
None
Returns:
str(prefered_interface): None or interface name"""
container_network_interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name))
prefered_interface = None # init
if container_network_interfaces: # container have network interfaces
# generator if interfaces which start with the desired pattern
net_generator = [interface for interface in container_network_interfaces if interface.startswith(self.prefered_container_network_interface)]
selected_interfaces = [] # init
for interface in net_generator:
selected_interfaces.append(interface)
if len(selected_interfaces) > 0:
prefered_interface = sorted(selected_interfaces)[0]
return prefered_interface
def get_container_vlans(self, container_name):
"""Get VLAN(s) from container
Helper to get the VLAN_ID from the container
Args:
str(containe_name): name of container
Kwargs:
None
Raises:
None
Returns:
None"""
# get network device configuration and store {network: vlan_id}
network_vlans = {}
for network in self._get_data_entry('networks'):
if self._get_data_entry('state/metadata/vlan/vid', data=self.data['networks'].get(network)):
network_vlans[network] = self._get_data_entry('state/metadata/vlan/vid', data=self.data['networks'].get(network))
# get networkdevices of container and return
# e.g.
# "eth0":{ "name":"eth0",
# "network":"lxdbr0",
# "type":"nic"},
vlan_ids = {}
devices = self._get_data_entry('containers/{0}/containers/metadata/expanded_devices'.format(to_native(container_name)))
for device in devices:
if 'network' in devices[device]:
if devices[device]['network'] in network_vlans:
vlan_ids[devices[device].get('network')] = network_vlans[devices[device].get('network')]
return vlan_ids if vlan_ids else None
def _get_data_entry(self, path, data=None, delimiter='/'):
"""Helper to get data
Helper to get data from self.data by a path like 'path/to/target'
Attention: Escaping of the delimiter is not (yet) provided.
Args:
str(path): path to nested dict
Kwargs:
dict(data): datastore
str(delimiter): delimiter in Path.
Raises:
None
Returns:
*(value)"""
try:
if not data:
data = self.data
if delimiter in path:
path = path.split(delimiter)
if isinstance(path, list) and len(path) > 1:
data = data[path.pop(0)]
path = delimiter.join(path)
return self._get_data_entry(path, data, delimiter) # recursion
return data[path]
except KeyError:
return None
def _set_data_entry(self, container_name, key, value, path=None):
"""Helper to save data
Helper to save the data in self.data
Detect if data is allready in branch and use dict_merge() to prevent that branch is overwritten.
Args:
str(container_name): name of container
str(key): same as dict
*(value): same as dict
Kwargs:
str(path): path to branch-part
Raises:
AnsibleParserError
Returns:
None"""
if not path:
path = self.data['inventory']
if container_name not in path:
path[container_name] = {}
try:
if isinstance(value, dict) and key in path[container_name]:
path[container_name] = dict_merge(value, path[container_name][key])
else:
path[container_name][key] = value
except KeyError as err:
raise AnsibleParserError("Unable to store Informations: {0}".format(to_native(err)))
def extract_information_from_container_configs(self):
"""Process configuration information
Preparation of the data
Args:
dict(configs): Container configurations
Kwargs:
None
Raises:
None
Returns:
None"""
# create branch "inventory"
if 'inventory' not in self.data:
self.data['inventory'] = {}
for container_name in self.data['containers']:
self._set_data_entry(container_name, 'os', self._get_data_entry(
'containers/{0}/containers/metadata/config/image.os'.format(container_name)))
self._set_data_entry(container_name, 'release', self._get_data_entry(
'containers/{0}/containers/metadata/config/image.release'.format(container_name)))
self._set_data_entry(container_name, 'version', self._get_data_entry(
'containers/{0}/containers/metadata/config/image.version'.format(container_name)))
self._set_data_entry(container_name, 'profile', self._get_data_entry(
'containers/{0}/containers/metadata/profiles'.format(container_name)))
self._set_data_entry(container_name, 'location', self._get_data_entry(
'containers/{0}/containers/metadata/location'.format(container_name)))
self._set_data_entry(container_name, 'state', self._get_data_entry(
'containers/{0}/containers/metadata/config/volatile.last_state.power'.format(container_name)))
self._set_data_entry(container_name, 'network_interfaces', self.extract_network_information_from_container_config(container_name))
self._set_data_entry(container_name, 'preferred_interface', self.get_prefered_container_network_interface(container_name))
self._set_data_entry(container_name, 'vlan_ids', self.get_container_vlans(container_name))
def build_inventory_network(self, container_name):
"""Add the network interfaces of the container to the inventory
Logic:
- if the container have no interface -> 'ansible_connection: local'
- get preferred_interface & prefered_container_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
- first Interface from: network_interfaces prefered_container_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
Args:
str(container_name): name of container
Kwargs:
None
Raises:
None
Returns:
None"""
def interface_selection(container_name):
"""Select container Interface for inventory
Logic:
- get preferred_interface & prefered_container_network_family -> str(IP)
- first Interface from: network_interfaces prefered_container_network_family -> str(IP)
Args:
str(container_name): name of container
Kwargs:
None
Raises:
None
Returns:
dict(interface_name: ip)"""
prefered_interface = self._get_data_entry('inventory/{0}/preferred_interface'.format(container_name)) # name or None
prefered_container_network_family = self.prefered_container_network_family
ip_address = ''
if prefered_interface:
interface = self._get_data_entry('inventory/{0}/network_interfaces/{1}'.format(container_name, prefered_interface))
for config in interface:
if config['family'] == prefered_container_network_family:
ip_address = config['address']
break
else:
interface = self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name))
for config in interface:
if config['family'] == prefered_container_network_family:
ip_address = config['address']
break
return ip_address
if self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name)): # container have network interfaces
if self._get_data_entry('inventory/{0}/preferred_interface'.format(container_name)): # container have a preferred interface
self.inventory.set_variable(container_name, 'ansible_connection', 'ssh')
self.inventory.set_variable(container_name, 'ansible_host', interface_selection(container_name))
else:
self.inventory.set_variable(container_name, 'ansible_connection', 'local')
def build_inventory_hosts(self):
"""Build host-part dynamic inventory
Build the host-part of the dynamic inventory.
Add Hosts and host_vars to the inventory.
Args:
None
Kwargs:
None
Raises:
None
Returns:
None"""
for container_name in self.data['inventory']:
# Only consider containers that match the "state" filter, if self.state is not None
if self.filter:
if self.filter.lower() != self._get_data_entry('inventory/{0}/state'.format(container_name)).lower():
continue
# add container
self.inventory.add_host(container_name)
# add network informations
self.build_inventory_network(container_name)
# add os
self.inventory.set_variable(container_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(container_name)).lower())
# add release
self.inventory.set_variable(container_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(container_name)).lower())
# add profile
self.inventory.set_variable(container_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(container_name)))
# add state
self.inventory.set_variable(container_name, 'ansible_lxd_state', self._get_data_entry('inventory/{0}/state'.format(container_name)).lower())
# add location information
if self._get_data_entry('inventory/{0}/location'.format(container_name)) != "none": # wrong type by lxd 'none' != 'None'
self.inventory.set_variable(container_name, 'ansible_lxd_location', self._get_data_entry('inventory/{0}/location'.format(container_name)))
# add VLAN_ID information
if self._get_data_entry('inventory/{0}/vlan_ids'.format(container_name)):
self.inventory.set_variable(container_name, 'ansible_lxd_vlan_ids', self._get_data_entry('inventory/{0}/vlan_ids'.format(container_name)))
def build_inventory_groups_location(self, group_name):
"""create group by attribute: location
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
for container_name in self.inventory.hosts:
if 'ansible_lxd_location' in self.inventory.get_host(container_name).get_vars():
self.inventory.add_child(group_name, container_name)
def build_inventory_groups_pattern(self, group_name):
"""create group by name pattern
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
regex_pattern = self.groupby[group_name].get('attribute')
for container_name in self.inventory.hosts:
result = re.search(regex_pattern, container_name)
if result:
self.inventory.add_child(group_name, container_name)
def build_inventory_groups_network_range(self, group_name):
"""check if IP is in network-class
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
try:
network = ipaddress.ip_network(to_text(self.groupby[group_name].get('attribute')))
except ValueError as err:
raise AnsibleParserError(
'Error while parsing network range {0}: {1}'.format(self.groupby[group_name].get('attribute'), to_native(err)))
for container_name in self.inventory.hosts:
if self.data['inventory'][container_name].get('network_interfaces') is not None:
for interface in self.data['inventory'][container_name].get('network_interfaces'):
for interface_family in self.data['inventory'][container_name].get('network_interfaces')[interface]:
try:
address = ipaddress.ip_address(to_text(interface_family['address']))
if address.version == network.version and address in network:
self.inventory.add_child(group_name, container_name)
except ValueError:
# Ignore invalid IP addresses returned by lxd
pass
def build_inventory_groups_os(self, group_name):
"""create group by attribute: os
Args:
str(group_name): Group name
Kwargs:
Noneself.data['inventory'][container_name][interface]
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
gen_containers = [
container_name for container_name in self.inventory.hosts
if 'ansible_lxd_os' in self.inventory.get_host(container_name).get_vars()]
for container_name in gen_containers:
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(container_name).get_vars().get('ansible_lxd_os'):
self.inventory.add_child(group_name, container_name)
def build_inventory_groups_release(self, group_name):
"""create group by attribute: release
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
gen_containers = [
container_name for container_name in self.inventory.hosts
if 'ansible_lxd_release' in self.inventory.get_host(container_name).get_vars()]
for container_name in gen_containers:
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(container_name).get_vars().get('ansible_lxd_release'):
self.inventory.add_child(group_name, container_name)
def build_inventory_groups_profile(self, group_name):
"""create group by attribute: profile
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
gen_containers = [
container_name for container_name in self.inventory.hosts.keys()
if 'ansible_lxd_profile' in self.inventory.get_host(container_name).get_vars().keys()]
for container_name in gen_containers:
if self.groupby[group_name].get('attribute').lower() in self.inventory.get_host(container_name).get_vars().get('ansible_lxd_profile'):
self.inventory.add_child(group_name, container_name)
def build_inventory_groups_vlanid(self, group_name):
"""create group by attribute: vlanid
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# maybe we just want to expand one group
if group_name not in self.inventory.groups:
self.inventory.add_group(group_name)
gen_containers = [
container_name for container_name in self.inventory.hosts.keys()
if 'ansible_lxd_vlan_ids' in self.inventory.get_host(container_name).get_vars().keys()]
for container_name in gen_containers:
if self.groupby[group_name].get('attribute') in self.inventory.get_host(container_name).get_vars().get('ansible_lxd_vlan_ids').values():
self.inventory.add_child(group_name, container_name)
def build_inventory_groups(self):
"""Build group-part dynamic inventory
Build the group-part of the dynamic inventory.
Add groups to the inventory.
Args:
None
Kwargs:
None
Raises:
None
Returns:
None"""
def group_type(group_name):
"""create groups defined by lxd.yml or defaultvalues
create groups defined by lxd.yml or defaultvalues
supportetd:
* 'location'
* 'pattern'
* 'network_range'
* 'os'
* 'release'
* 'profile'
* 'vlanid'
Args:
str(group_name): Group name
Kwargs:
None
Raises:
None
Returns:
None"""
# Due to the compatibility with python 2 no use of map
if self.groupby[group_name].get('type') == 'location':
self.build_inventory_groups_location(group_name)
elif self.groupby[group_name].get('type') == 'pattern':
self.build_inventory_groups_pattern(group_name)
elif self.groupby[group_name].get('type') == 'network_range':
self.build_inventory_groups_network_range(group_name)
elif self.groupby[group_name].get('type') == 'os':
self.build_inventory_groups_os(group_name)
elif self.groupby[group_name].get('type') == 'release':
self.build_inventory_groups_release(group_name)
elif self.groupby[group_name].get('type') == 'profile':
self.build_inventory_groups_profile(group_name)
elif self.groupby[group_name].get('type') == 'vlanid':
self.build_inventory_groups_vlanid(group_name)
else:
raise AnsibleParserError('Unknown group type: {0}'.format(to_native(group_name)))
if self.groupby:
for group_name in self.groupby:
if not group_name.isalnum():
raise AnsibleParserError('Invalid character(s) in groupname: {0}'.format(to_native(group_name)))
group_type(group_name)
def build_inventory(self):
"""Build dynamic inventory
Build the dynamic inventory.
Args:
None
Kwargs:
None
Raises:
None
Returns:
None"""
self.build_inventory_hosts()
self.build_inventory_groups()
def _populate(self):
"""Return the hosts and groups
Returns the processed container configurations from the lxd import
Args:
None
Kwargs:
None
Raises:
None
Returns:
None"""
if len(self.data) == 0: # If no data is injected by unittests open socket
self.socket = self._connect_to_socket()
self.get_container_data(self._get_containers())
self.get_network_data(self._get_networks())
self.extract_information_from_container_configs()
# self.display.vvv(self.save_json_data([os.path.abspath(__file__)]))
self.build_inventory()
def parse(self, inventory, loader, path, cache):
"""Return dynamic inventory from source
Returns the processed inventory from the lxd import
Args:
str(inventory): inventory object with existing data and
the methods to add hosts/groups/variables
to inventory
str(loader): Ansible's DataLoader
str(path): path to the config
bool(cache): use or avoid caches
Kwargs:
None
Raises:
AnsibleParserError
Returns:
None"""
super(InventoryModule, self).parse(inventory, loader, path, cache=False)
# Read the inventory YAML file
self._read_config_data(path)
try:
self.client_key = self.get_option('client_key')
self.client_cert = self.get_option('client_cert')
self.debug = self.DEBUG
self.data = {} # store for inventory-data
self.groupby = self.get_option('groupby')
self.plugin = self.get_option('plugin')
self.prefered_container_network_family = self.get_option('prefered_container_network_family')
self.prefered_container_network_interface = self.get_option('prefered_container_network_interface')
if self.get_option('state').lower() == 'none': # none in config is str()
self.filter = None
else:
self.filter = self.get_option('state').lower()
self.trust_password = self.get_option('trust_password')
self.url = self.get_option('url')
except Exception as err:
raise AnsibleParserError(
'All correct options required: {0}'.format(to_native(err)))
# Call our internal helper to populate the dynamic inventory
self._populate()

View file

@ -0,0 +1,174 @@
{
"containers":{
"vlantest":{
"containers":{
"metadata":{
"config":{
"image.os":"ubuntu",
"image.release":"focal",
"image.version":"20.04",
"volatile.last_state.power":"RUNNING"
},
"devices":{
"eth0":{
"name":"eth0",
"network":"my-macvlan",
"type":"nic"
}
},
"profiles":[
"default"
],
"expanded_devices":{
"eth0":{
"name":"eth0",
"network":"my-macvlan",
"type":"nic"
}
},
"name":"vlantest",
"status":"Running",
"location":"Berlin"
}
},
"state":{
"metadata":{
"status":"Running",
"network":{
"eth0":{
"addresses":[
{
"family":"inet",
"address":"10.98.143.199",
"netmask":"24",
"scope":"global"
},
{
"family":"inet6",
"address":"fd42:bd00:7b11:2167:216:3eff:fe78:2ef3",
"netmask":"64",
"scope":"global"
},
{
"family":"inet6",
"address":"fe80::216:3eff:fed3:7af3",
"netmask":"64",
"scope":"link"
}
]
},
"lo":{
"addresses":[
{
"family":"inet",
"address":"127.0.0.1",
"netmask":"8",
"scope":"local"
},
{
"family":"inet6",
"address":"::1",
"netmask":"128",
"scope":"local"
}
]
}
}
}
}
}
},
"networks":{
"my-macvlan":{
"state":{
"metadata":{
"addresses":[
{
"family":"inet",
"address":"192.168.178.199",
"netmask":"24",
"scope":"global"
},
{
"family":"inet6",
"address":"fd42:bd00:7b11:2167:216:3eff:fe78:2ef3",
"netmask":"64",
"scope":"global"
},
{
"family":"inet6",
"address":"fe80::216:3eff:fed3:7af3",
"netmask":"64",
"scope":"link"
}
],
"vlan":{
"lower_device":"eno1",
"vid":666
}
}
}
},
"lo":{
"state":{
"metadata":{
"addresses":[
{
"family":"inet",
"address":"127.0.0.1",
"netmask":"8",
"scope":"local"
},
{
"family":"inet6",
"address":"::1",
"netmask":"128",
"scope":"local"
}
],
"vlan":null
}
}
},
"eno1":{
"state":{
"metadata":{
"addresses":[
{
"family":"inet",
"address":"192.168.178.126",
"netmask":"24",
"scope":"global"
},
{
"family":"inet6",
"address":"fe80::3c0b:7da9:3cc7:9e40",
"netmask":"64",
"scope":"link"
}
],
"vlan":null
}
}
},
"eno1.666":{
"state":{
"metadata":{
"addresses":[
{
"family":"inet6",
"address":"fe80::de4a:3eff:fe8d:f356",
"netmask":"64",
"scope":"link"
}
],
"vlan":{
"lower_device":"eno1",
"vid":666
}
}
}
}
}
}

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Frank Dornheim <dornheim@posteo.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
import os
from ansible.errors import AnsibleError
from ansible.inventory.data import InventoryData
from ansible_collections.community.general.plugins.inventory.lxd import InventoryModule
HOST_COMPARATIVE_DATA = {
'ansible_connection': 'ssh', 'ansible_host': '10.98.143.199', 'ansible_lxd_os': 'ubuntu', 'ansible_lxd_release': 'focal',
'ansible_lxd_profile': ['default'], 'ansible_lxd_state': 'running', 'ansible_lxd_location': 'Berlin',
'ansible_lxd_vlan_ids': {'my-macvlan': 666}, 'inventory_hostname': 'vlantest', 'inventory_hostname_short': 'vlantest'}
GROUP_COMPARATIVE_DATA = {
'all': [], 'ungrouped': [], 'testpattern': ['vlantest'], 'vlan666': ['vlantest'], 'locationBerlin': ['vlantest'],
'osUbuntu': ['vlantest'], 'releaseFocal': ['vlantest'], 'releaseBionic': [], 'profileDefault': ['vlantest'],
'profileX11': [], 'netRangeIPv4': ['vlantest'], 'netRangeIPv6': ['vlantest']}
GROUP_Config = {
'testpattern': {'type': 'pattern', 'attribute': 'test'},
'vlan666': {'type': 'vlanid', 'attribute': 666},
'locationBerlin': {'type': 'location', 'attribute': 'Berlin'},
'osUbuntu': {'type': 'os', 'attribute': 'ubuntu'},
'releaseFocal': {'type': 'release', 'attribute': 'focal'},
'releaseBionic': {'type': 'release', 'attribute': 'bionic'},
'profileDefault': {'type': 'profile', 'attribute': 'default'},
'profileX11': {'type': 'profile', 'attribute': 'x11'},
'netRangeIPv4': {'type': 'network_range', 'attribute': '10.98.143.0/24'},
'netRangeIPv6': {'type': 'network_range', 'attribute': 'fd42:bd00:7b11:2167:216:3eff::/96'}}
@pytest.fixture
def inventory():
inv = InventoryModule()
inv.inventory = InventoryData()
# Test Values
inv.data = inv.load_json_data('tests/unit/plugins/inventory/fixtures/lxd_inventory.atd') # Load Test Data
inv.groupby = GROUP_Config
inv.prefered_container_network_interface = 'eth'
inv.prefered_container_network_family = 'inet'
inv.filter = 'running'
inv.dump_data = False
return inv
def test_verify_file_bad_config(inventory):
assert inventory.verify_file('foobar.lxd.yml') is False
def test_build_inventory_hosts(inventory):
"""Load example data and start the inventoryto test the host generation.
After the inventory plugin has run with the test data, the result of the host is checked."""
inventory._populate()
generated_data = inventory.inventory.get_host('vlantest').get_vars()
eq = True
for key, value in HOST_COMPARATIVE_DATA.items():
if generated_data[key] != value:
eq = False
assert eq
def test_build_inventory_groups(inventory):
"""Load example data and start the inventory to test the group generation.
After the inventory plugin has run with the test data, the result of the host is checked."""
inventory._populate()
generated_data = inventory.inventory.get_groups_dict()
eq = True
for key, value in GROUP_COMPARATIVE_DATA.items():
if generated_data[key] != value:
eq = False
assert eq
def test_build_inventory_groups_with_no_groupselection(inventory):
"""Load example data and start the inventory to test the group generation with groupby is none.
After the inventory plugin has run with the test data, the result of the host is checked."""
inventory.groupby = None
inventory._populate()
generated_data = inventory.inventory.get_groups_dict()
group_comparative_data = {'all': [], 'ungrouped': []}
eq = True
print("data: {0}".format(generated_data))
for key, value in group_comparative_data.items():
if generated_data[key] != value:
eq = False
assert eq