From 5d6fcaef535fabc91005f8f2d54ec415d969c92b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 14 Dec 2021 06:42:47 +0100 Subject: [PATCH] LXD inventory: Support virtual machines (#3519) (#3900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LXD 4.x compatibility (Containers and VMs) * add changelog fragment * update fixture * update plugin options * backwards compatible alias Co-authored-by: Felix Fontein * Update changelogs/fragments/3519-inventory-support-lxd-4.yml Co-authored-by: Felix Fontein * add lxd 4.0 requirement * filter for type of virtualization added. due to duplication in the namespace, "type" is not used as the keyword but "nature". * add type filter Since the first version of this inventory plugin only supports containers, a filter function was added to filter between containers and virtual machines or both. By default only containers are displayed, as in the first version of the plugin. This behavior will change in the future. * rename C(nature) to C(type) The term "nature" does not fit into the lxd namespace. Therefore i renamed nature to type. * update changelog fragment * Update plugins/inventory/lxd.py Co-authored-by: Felix Fontein * Apply suggestions from code review Co-authored-by: Felix Fontein * rename typefilter to type_filter * fix tests with type_filter * Update plugins/inventory/lxd.py * Update plugins/inventory/lxd.py Co-authored-by: Felix Fontein Co-authored-by: Frank Dornheim <“dornheim@posteo.de@users.noreply.github.com”> (cherry picked from commit 8825ef4711ab44598f9e480b3d5ea75c608701c8) Co-authored-by: Élie --- .../3519-inventory-support-lxd-4.yml | 2 + plugins/inventory/lxd.py | 444 +++++++++++------- .../inventory/fixtures/lxd_inventory.atd | 4 +- tests/unit/plugins/inventory/test_lxd.py | 5 +- 4 files changed, 271 insertions(+), 184 deletions(-) create mode 100644 changelogs/fragments/3519-inventory-support-lxd-4.yml diff --git a/changelogs/fragments/3519-inventory-support-lxd-4.yml b/changelogs/fragments/3519-inventory-support-lxd-4.yml new file mode 100644 index 0000000000..0d7e9710d0 --- /dev/null +++ b/changelogs/fragments/3519-inventory-support-lxd-4.yml @@ -0,0 +1,2 @@ +minor_changes: + - lxd inventory plugin - support virtual machines (https://github.com/ansible-collections/community.general/pull/3519). diff --git a/plugins/inventory/lxd.py b/plugins/inventory/lxd.py index 59bb8845ff..cdb0fa19b1 100644 --- a/plugins/inventory/lxd.py +++ b/plugins/inventory/lxd.py @@ -15,6 +15,7 @@ DOCUMENTATION = r''' author: "Frank Dornheim (@conloos)" requirements: - ipaddress + - lxd >= 4.0 options: plugin: description: Token that ensures this is a source file for the 'lxd' plugin. @@ -49,26 +50,38 @@ DOCUMENTATION = r''' - 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. + description: Filter the instance according to the current status. type: str default: none choices: [ 'STOPPED', 'STARTING', 'RUNNING', 'none' ] - prefered_container_network_interface: + type_filter: description: - - If a container has multiple network interfaces, select which one is the prefered as pattern. + - Filter the instances by type C(virtual-machine), C(container) or C(both). + - The first version of the inventory only supported containers. + type: str + default: container + choices: [ 'virtual-machine', 'container', 'both' ] + version_added: 4.2.0 + prefered_instance_network_interface: + description: + - If an instance 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. + - The option has been renamed from I(prefered_container_network_interface) to I(prefered_instance_network_interface) in community.general 3.8.0. + The old name still works as an alias. type: str default: eth - prefered_container_network_family: + aliases: + - prefered_container_network_interface + prefered_instance_network_family: description: - - If a container has multiple network interfaces, which one is the prefered by family. + - If an instance 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). + - Create groups by the following keywords C(location), C(network_range), C(os), C(pattern), C(profile), C(release), C(type), C(vlanid). - See example for syntax. type: dict ''' @@ -83,38 +96,49 @@ plugin: community.general.lxd url: unix:/var/snap/lxd/common/lxd/unix.socket state: RUNNING +# simple lxd.yml including virtual machines and containers +plugin: community.general.lxd +url: unix:/var/snap/lxd/common/lxd/unix.socket +type_filter: both + # 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 + osUbuntu: + type: os + attribute: ubuntu + testpattern: + type: pattern + attribute: test + profileDefault: + type: profile + attribute: default + profileX11: + type: profile + attribute: x11 + releaseFocal: + type: release + attribute: focal + releaseBionic: + type: release + attribute: bionic + typeVM: + type: type + attribute: virtual-machine + typeContainer: + type: type + attribute: container + vlan666: + type: vlanid + attribute: 666 ''' import binascii @@ -283,10 +307,10 @@ class InventoryModule(BaseInventoryPlugin): 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 + def _get_instances(self): + """Get instancenames - Returns all containernames + Returns all instancenames Args: None @@ -295,25 +319,27 @@ class InventoryModule(BaseInventoryPlugin): 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']] + list(names): names of all instances""" + # e.g. { + # "metadata": [ + # "/1.0/instances/foo", + # "/1.0/instances/bar" + # ], + # "status": "Success", + # "status_code": 200, + # "type": "sync" + # } + instances = self.socket.do('GET', '/1.0/instances') + return [m.split('/')[3] for m in instances['metadata']] def _get_config(self, branch, name): - """Get inventory of container + """Get inventory of instance - Get config of container + Get config of instance Args: str(branch): Name oft the API-Branch - str(name): Name of Container + str(name): Name of instance Kwargs: None Source: @@ -321,7 +347,7 @@ class InventoryModule(BaseInventoryPlugin): Raises: None Returns: - dict(config): Config of the container""" + dict(config): Config of the instance""" 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])))} @@ -329,13 +355,13 @@ class InventoryModule(BaseInventoryPlugin): 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 + def get_instance_data(self, names): + """Create Inventory of the instance - Iterate through the different branches of the containers and collect Informations. + Iterate through the different branches of the instances and collect Informations. Args: - list(names): List of container names + list(names): List of instance names Kwargs: None Raises: @@ -344,20 +370,20 @@ class InventoryModule(BaseInventoryPlugin): None""" # tuple(('instances','metadata/templates')) to get section in branch # e.g. /1.0/instances//metadata/templates - branches = ['containers', ('instances', 'state')] - container_config = {} + branches = ['instances', ('instances', 'state')] + instance_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) + instance_config['instances'] = self._get_config(branch, name) + self.data = dict_merge(instance_config, self.data) def get_network_data(self, names): - """Create Inventory of the container + """Create Inventory of the instance - Iterate through the different branches of the containers and collect Informations. + Iterate through the different branches of the instances and collect Informations. Args: - list(names): List of container names + list(names): List of instance names Kwargs: None Raises: @@ -376,26 +402,26 @@ class InventoryModule(BaseInventoryPlugin): network_config['networks'] = {name: None} self.data = dict_merge(network_config, self.data) - def extract_network_information_from_container_config(self, container_name): + def extract_network_information_from_instance_config(self, instance_name): """Returns the network interface configuration - Returns the network ipv4 and ipv6 config of the container without local-link + Returns the network ipv4 and ipv6 config of the instance without local-link Args: - str(container_name): Name oft he container + str(instance_name): Name oft he instance 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)) + instance_network_interfaces = self._get_data_entry('instances/{0}/state/metadata/network'.format(instance_name)) network_configuration = None - if container_network_interfaces: + if instance_network_interfaces: network_configuration = {} - gen_interface_names = [interface_name for interface_name in container_network_interfaces if interface_name != 'lo'] + gen_interface_names = [interface_name for interface_name in instance_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'] + gen_address = [address for address in instance_network_interfaces[interface_name]['addresses'] if address.get('scope') != 'link'] network_configuration[interface_name] = [] for address in gen_address: address_set = {} @@ -406,24 +432,24 @@ class InventoryModule(BaseInventoryPlugin): 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 + def get_prefered_instance_network_interface(self, instance_name): + """Helper to get the prefered interface of thr instance - Helper to get the prefered interface provide by neme pattern from 'prefered_container_network_interface'. + Helper to get the prefered interface provide by neme pattern from 'prefered_instance_network_interface'. Args: - str(containe_name): name of container + str(containe_name): name of instance 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)) + instance_network_interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name)) prefered_interface = None # init - if container_network_interfaces: # container have network interfaces + if instance_network_interfaces: # instance 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)] + net_generator = [interface for interface in instance_network_interfaces if interface.startswith(self.prefered_instance_network_interface)] selected_interfaces = [] # init for interface in net_generator: selected_interfaces.append(interface) @@ -431,13 +457,13 @@ class InventoryModule(BaseInventoryPlugin): prefered_interface = sorted(selected_interfaces)[0] return prefered_interface - def get_container_vlans(self, container_name): - """Get VLAN(s) from container + def get_instance_vlans(self, instance_name): + """Get VLAN(s) from instance - Helper to get the VLAN_ID from the container + Helper to get the VLAN_ID from the instance Args: - str(containe_name): name of container + str(containe_name): name of instance Kwargs: None Raises: @@ -450,13 +476,13 @@ class InventoryModule(BaseInventoryPlugin): 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 + # get networkdevices of instance 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))) + devices = self._get_data_entry('instances/{0}/instances/metadata/expanded_devices'.format(to_native(instance_name))) for device in devices: if 'network' in devices[device]: if devices[device]['network'] in network_vlans: @@ -492,14 +518,14 @@ class InventoryModule(BaseInventoryPlugin): except KeyError: return None - def _set_data_entry(self, container_name, key, value, path=None): + def _set_data_entry(self, instance_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(instance_name): name of instance str(key): same as dict *(value): same as dict Kwargs: @@ -510,24 +536,24 @@ class InventoryModule(BaseInventoryPlugin): None""" if not path: path = self.data['inventory'] - if container_name not in path: - path[container_name] = {} + if instance_name not in path: + path[instance_name] = {} try: - if isinstance(value, dict) and key in path[container_name]: - path[container_name] = dict_merge(value, path[container_name][key]) + if isinstance(value, dict) and key in path[instance_name]: + path[instance_name] = dict_merge(value, path[instance_name][key]) else: - path[container_name][key] = value + path[instance_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): + def extract_information_from_instance_configs(self): """Process configuration information Preparation of the data Args: - dict(configs): Container configurations + dict(configs): instance configurations Kwargs: None Raises: @@ -538,33 +564,35 @@ class InventoryModule(BaseInventoryPlugin): 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)) + for instance_name in self.data['instances']: + self._set_data_entry(instance_name, 'os', self._get_data_entry( + 'instances/{0}/instances/metadata/config/image.os'.format(instance_name))) + self._set_data_entry(instance_name, 'release', self._get_data_entry( + 'instances/{0}/instances/metadata/config/image.release'.format(instance_name))) + self._set_data_entry(instance_name, 'version', self._get_data_entry( + 'instances/{0}/instances/metadata/config/image.version'.format(instance_name))) + self._set_data_entry(instance_name, 'profile', self._get_data_entry( + 'instances/{0}/instances/metadata/profiles'.format(instance_name))) + self._set_data_entry(instance_name, 'location', self._get_data_entry( + 'instances/{0}/instances/metadata/location'.format(instance_name))) + self._set_data_entry(instance_name, 'state', self._get_data_entry( + 'instances/{0}/instances/metadata/config/volatile.last_state.power'.format(instance_name))) + self._set_data_entry(instance_name, 'type', self._get_data_entry( + 'instances/{0}/instances/metadata/type'.format(instance_name))) + self._set_data_entry(instance_name, 'network_interfaces', self.extract_network_information_from_instance_config(instance_name)) + self._set_data_entry(instance_name, 'preferred_interface', self.get_prefered_instance_network_interface(instance_name)) + self._set_data_entry(instance_name, 'vlan_ids', self.get_instance_vlans(instance_name)) - def build_inventory_network(self, container_name): - """Add the network interfaces of the container to the inventory + def build_inventory_network(self, instance_name): + """Add the network interfaces of the instance 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: ' - - first Interface from: network_interfaces prefered_container_network_family -> 'ansible_connection: ssh' & 'ansible_host: ' + - if the instance have no interface -> 'ansible_connection: local' + - get preferred_interface & prefered_instance_network_family -> 'ansible_connection: ssh' & 'ansible_host: ' + - first Interface from: network_interfaces prefered_instance_network_family -> 'ansible_connection: ssh' & 'ansible_host: ' Args: - str(container_name): name of container + str(instance_name): name of instance Kwargs: None Raises: @@ -572,45 +600,45 @@ class InventoryModule(BaseInventoryPlugin): Returns: None""" - def interface_selection(container_name): - """Select container Interface for inventory + def interface_selection(instance_name): + """Select instance Interface for inventory Logic: - - get preferred_interface & prefered_container_network_family -> str(IP) - - first Interface from: network_interfaces prefered_container_network_family -> str(IP) + - get preferred_interface & prefered_instance_network_family -> str(IP) + - first Interface from: network_interfaces prefered_instance_network_family -> str(IP) Args: - str(container_name): name of container + str(instance_name): name of instance 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 + prefered_interface = self._get_data_entry('inventory/{0}/preferred_interface'.format(instance_name)) # name or None + prefered_instance_network_family = self.prefered_instance_network_family ip_address = '' if prefered_interface: - interface = self._get_data_entry('inventory/{0}/network_interfaces/{1}'.format(container_name, prefered_interface)) + interface = self._get_data_entry('inventory/{0}/network_interfaces/{1}'.format(instance_name, prefered_interface)) for config in interface: - if config['family'] == prefered_container_network_family: + if config['family'] == prefered_instance_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 + interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name)) + for interface in interfaces.values(): + for config in interface: + if config['family'] == prefered_instance_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)) + if self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name)): # instance have network interfaces + self.inventory.set_variable(instance_name, 'ansible_connection', 'ssh') + self.inventory.set_variable(instance_name, 'ansible_host', interface_selection(instance_name)) else: - self.inventory.set_variable(container_name, 'ansible_connection', 'local') + self.inventory.set_variable(instance_name, 'ansible_connection', 'local') def build_inventory_hosts(self): """Build host-part dynamic inventory @@ -626,29 +654,33 @@ class InventoryModule(BaseInventoryPlugin): None Returns: None""" - for container_name in self.data['inventory']: - # Only consider containers that match the "state" filter, if self.state is not None + for instance_name in self.data['inventory']: + instance_state = str(self._get_data_entry('inventory/{0}/state'.format(instance_name)) or "STOPPED").lower() + + # Only consider instances 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(): + if self.filter.lower() != instance_state: continue - # add container - self.inventory.add_host(container_name) + # add instance + self.inventory.add_host(instance_name) # add network informations - self.build_inventory_network(container_name) + self.build_inventory_network(instance_name) # add os - self.inventory.set_variable(container_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(container_name)).lower()) + self.inventory.set_variable(instance_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(instance_name)).lower()) # add release - self.inventory.set_variable(container_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(container_name)).lower()) + self.inventory.set_variable(instance_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(instance_name)).lower()) # add profile - self.inventory.set_variable(container_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(container_name))) + self.inventory.set_variable(instance_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(instance_name))) # add state - self.inventory.set_variable(container_name, 'ansible_lxd_state', self._get_data_entry('inventory/{0}/state'.format(container_name)).lower()) + self.inventory.set_variable(instance_name, 'ansible_lxd_state', instance_state) + # add type + self.inventory.set_variable(instance_name, 'ansible_lxd_type', self._get_data_entry('inventory/{0}/type'.format(instance_name))) # 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))) + if self._get_data_entry('inventory/{0}/location'.format(instance_name)) != "none": # wrong type by lxd 'none' != 'None' + self.inventory.set_variable(instance_name, 'ansible_lxd_location', self._get_data_entry('inventory/{0}/location'.format(instance_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))) + if self._get_data_entry('inventory/{0}/vlan_ids'.format(instance_name)): + self.inventory.set_variable(instance_name, 'ansible_lxd_vlan_ids', self._get_data_entry('inventory/{0}/vlan_ids'.format(instance_name))) def build_inventory_groups_location(self, group_name): """create group by attribute: location @@ -665,9 +697,9 @@ class InventoryModule(BaseInventoryPlugin): 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) + for instance_name in self.inventory.hosts: + if 'ansible_lxd_location' in self.inventory.get_host(instance_name).get_vars(): + self.inventory.add_child(group_name, instance_name) def build_inventory_groups_pattern(self, group_name): """create group by name pattern @@ -686,10 +718,10 @@ class InventoryModule(BaseInventoryPlugin): regex_pattern = self.groupby[group_name].get('attribute') - for container_name in self.inventory.hosts: - result = re.search(regex_pattern, container_name) + for instance_name in self.inventory.hosts: + result = re.search(regex_pattern, instance_name) if result: - self.inventory.add_child(group_name, container_name) + self.inventory.add_child(group_name, instance_name) def build_inventory_groups_network_range(self, group_name): """check if IP is in network-class @@ -712,14 +744,14 @@ class InventoryModule(BaseInventoryPlugin): 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]: + for instance_name in self.inventory.hosts: + if self.data['inventory'][instance_name].get('network_interfaces') is not None: + for interface in self.data['inventory'][instance_name].get('network_interfaces'): + for interface_family in self.data['inventory'][instance_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) + self.inventory.add_child(group_name, instance_name) except ValueError: # Ignore invalid IP addresses returned by lxd pass @@ -730,7 +762,7 @@ class InventoryModule(BaseInventoryPlugin): Args: str(group_name): Group name Kwargs: - Noneself.data['inventory'][container_name][interface] + None Raises: None Returns: @@ -739,12 +771,12 @@ class InventoryModule(BaseInventoryPlugin): 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) + gen_instances = [ + instance_name for instance_name in self.inventory.hosts + if 'ansible_lxd_os' in self.inventory.get_host(instance_name).get_vars()] + for instance_name in gen_instances: + if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_os'): + self.inventory.add_child(group_name, instance_name) def build_inventory_groups_release(self, group_name): """create group by attribute: release @@ -761,12 +793,12 @@ class InventoryModule(BaseInventoryPlugin): 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) + gen_instances = [ + instance_name for instance_name in self.inventory.hosts + if 'ansible_lxd_release' in self.inventory.get_host(instance_name).get_vars()] + for instance_name in gen_instances: + if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_release'): + self.inventory.add_child(group_name, instance_name) def build_inventory_groups_profile(self, group_name): """create group by attribute: profile @@ -783,12 +815,12 @@ class InventoryModule(BaseInventoryPlugin): 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) + gen_instances = [ + instance_name for instance_name in self.inventory.hosts.keys() + if 'ansible_lxd_profile' in self.inventory.get_host(instance_name).get_vars().keys()] + for instance_name in gen_instances: + if self.groupby[group_name].get('attribute').lower() in self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_profile'): + self.inventory.add_child(group_name, instance_name) def build_inventory_groups_vlanid(self, group_name): """create group by attribute: vlanid @@ -805,12 +837,34 @@ class InventoryModule(BaseInventoryPlugin): 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) + gen_instances = [ + instance_name for instance_name in self.inventory.hosts.keys() + if 'ansible_lxd_vlan_ids' in self.inventory.get_host(instance_name).get_vars().keys()] + for instance_name in gen_instances: + if self.groupby[group_name].get('attribute') in self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_vlan_ids').values(): + self.inventory.add_child(group_name, instance_name) + + def build_inventory_groups_type(self, group_name): + """create group by attribute: type + + 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_instances = [ + instance_name for instance_name in self.inventory.hosts + if 'ansible_lxd_type' in self.inventory.get_host(instance_name).get_vars()] + for instance_name in gen_instances: + if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_type'): + self.inventory.add_child(group_name, instance_name) def build_inventory_groups(self): """Build group-part dynamic inventory @@ -839,6 +893,7 @@ class InventoryModule(BaseInventoryPlugin): * 'release' * 'profile' * 'vlanid' + * 'type' Args: str(group_name): Group name @@ -864,6 +919,8 @@ class InventoryModule(BaseInventoryPlugin): self.build_inventory_groups_profile(group_name) elif self.groupby[group_name].get('type') == 'vlanid': self.build_inventory_groups_vlanid(group_name) + elif self.groupby[group_name].get('type') == 'type': + self.build_inventory_groups_type(group_name) else: raise AnsibleParserError('Unknown group type: {0}'.format(to_native(group_name))) @@ -890,10 +947,30 @@ class InventoryModule(BaseInventoryPlugin): self.build_inventory_hosts() self.build_inventory_groups() + def cleandata(self): + """Clean the dynamic inventory + + The first version of the inventory only supported container. + This will change in the future. + The following function cleans up the data and remove the all items with the wrong type. + + Args: + None + Kwargs: + None + Raises: + None + Returns: + None""" + iter_keys = list(self.data['instances'].keys()) + for instance_name in iter_keys: + if self._get_data_entry('instances/{0}/instances/metadata/type'.format(instance_name)) != self.type_filter: + del self.data['instances'][instance_name] + def _populate(self): """Return the hosts and groups - Returns the processed container configurations from the lxd import + Returns the processed instance configurations from the lxd import Args: None @@ -906,10 +983,16 @@ class InventoryModule(BaseInventoryPlugin): 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_instance_data(self._get_instances()) self.get_network_data(self._get_networks()) - self.extract_information_from_container_configs() + # The first version of the inventory only supported containers. + # This will change in the future. + # The following function cleans up the data. + if self.type_filter != 'both': + self.cleandata() + + self.extract_information_from_instance_configs() # self.display.vvv(self.save_json_data([os.path.abspath(__file__)])) @@ -948,8 +1031,9 @@ class InventoryModule(BaseInventoryPlugin): 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') + self.prefered_instance_network_family = self.get_option('prefered_instance_network_family') + self.prefered_instance_network_interface = self.get_option('prefered_instance_network_interface') + self.type_filter = self.get_option('type_filter') if self.get_option('state').lower() == 'none': # none in config is str() self.filter = None else: diff --git a/tests/unit/plugins/inventory/fixtures/lxd_inventory.atd b/tests/unit/plugins/inventory/fixtures/lxd_inventory.atd index b308243228..dd6baa885a 100644 --- a/tests/unit/plugins/inventory/fixtures/lxd_inventory.atd +++ b/tests/unit/plugins/inventory/fixtures/lxd_inventory.atd @@ -1,7 +1,7 @@ { - "containers":{ + "instances":{ "vlantest":{ - "containers":{ + "instances":{ "metadata":{ "config":{ "image.os":"ubuntu", diff --git a/tests/unit/plugins/inventory/test_lxd.py b/tests/unit/plugins/inventory/test_lxd.py index 04cea0af71..42142c8d64 100644 --- a/tests/unit/plugins/inventory/test_lxd.py +++ b/tests/unit/plugins/inventory/test_lxd.py @@ -43,10 +43,11 @@ def inventory(): # 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.prefered_instance_network_interface = 'eth' + inv.prefered_instance_network_family = 'inet' inv.filter = 'running' inv.dump_data = False + inv.type_filter = 'both' return inv