From a8666396a4b3ee0f5d39b7f72429ef5338e264f9 Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Sun, 29 Dec 2024 20:11:58 -0500 Subject: [PATCH] add vm inventory plugin --- hosts.vms.yml | 3 + plugins/inventory/esxi_hosts.py | 133 +------- plugins/inventory/vms.py | 305 ++++++++++++++++++ plugins/inventory_utils/_base.py | 132 ++++++++ .../vmware_inventory_vms/defaults/main.yml | 1 + .../vmware_inventory_vms/files/test.vms.yml | 8 + .../targets/vmware_inventory_vms/run.yml | 13 + .../vmware_inventory_vms/tasks/main.yml | 51 +++ 8 files changed, 523 insertions(+), 123 deletions(-) create mode 100644 hosts.vms.yml create mode 100644 plugins/inventory/vms.py create mode 100644 tests/integration/targets/vmware_inventory_vms/defaults/main.yml create mode 100644 tests/integration/targets/vmware_inventory_vms/files/test.vms.yml create mode 100644 tests/integration/targets/vmware_inventory_vms/run.yml create mode 100644 tests/integration/targets/vmware_inventory_vms/tasks/main.yml diff --git a/hosts.vms.yml b/hosts.vms.yml new file mode 100644 index 00000000..2b467a74 --- /dev/null +++ b/hosts.vms.yml @@ -0,0 +1,3 @@ +plugin: vmware.vmware.vms +search_paths: + - /Eco-Datacenter/vm/yblum diff --git a/plugins/inventory/esxi_hosts.py b/plugins/inventory/esxi_hosts.py index 28b6ab6a..477130d0 100644 --- a/plugins/inventory/esxi_hosts.py +++ b/plugins/inventory/esxi_hosts.py @@ -150,71 +150,28 @@ from ansible.errors import AnsibleError from ansible.module_utils.common.text.converters import to_native -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.vmware.vmware.plugins.inventory_utils._base import VmwareInventoryBase -from ansible_collections.vmware.vmware.plugins.module_utils._vmware_folder_paths import ( - get_folder_path_of_vsphere_object -) -from ansible_collections.vmware.vmware.plugins.module_utils._vmware_facts import ( - vmware_obj_to_json, - flatten_dict +from ansible_collections.vmware.vmware.plugins.inventory_utils._base import ( + VmwareInventoryBase, + VmwareInventoryHost ) -class EsxiInventoryHost(): +class EsxiInventoryHost(VmwareInventoryHost): def __init__(self): - self.object = None - self.inventory_hostname = None - self.path = '' - self.properties = dict() + super().__init__() self._management_ip = None @classmethod - def create_from_cache(cls, inventory_hostname, host_properties): - """ - Create the class from the inventory cache. We don't want to refresh the data or make any calls to vCenter. - Properties are populated from whatever we had previously cached. - """ - host = cls() - host.inventory_hostname = inventory_hostname - host.properties = host_properties - return host - - @classmethod - def create_from_object(cls, host_object, properties_to_gather, pyvmomi_client): + def create_from_object(cls, vmware_object, properties_to_gather, pyvmomi_client): """ Create the class from a host object that we got from pyvmomi. The host properties will be populated from the object and additional calls to vCenter """ - host = cls() - host.object = host_object - host.path = get_folder_path_of_vsphere_object(host_object) - host.properties = host._set_properties_from_pyvmomi(properties_to_gather, pyvmomi_client) + host = super().create_from_object(vmware_object, properties_to_gather, pyvmomi_client) + host.properties['management_ip'] = host.management_ip return host - def _set_properties_from_pyvmomi(self, properties_to_gather, pyvmomi_client): - properties = vmware_obj_to_json(self.object, properties_to_gather) - properties['path'] = self.path - properties['management_ip'] = self.management_ip - - # Custom values - if hasattr(self.object, "customValue"): - properties['customValue'] = dict() - field_mgr = pyvmomi_client.custom_field_mgr - for cust_value in self.object.customValue: - properties['customValue'][ - [y.name for y in field_mgr if y.key == cust_value.key][0] - ] = cust_value.value - - return properties - - def sanitize_properties(self): - self.properties = camel_dict_to_snake_dict(self.properties) - - def flatten_properties(self): - self.properties = flatten_dict(self.properties) - @property def management_ip(self): # We already looked up the management IP from vcenter this session, so @@ -313,7 +270,7 @@ def populate_from_cache(self, cache_data): for inventory_hostname, host_properties in cache_data.items(): esxi_host = EsxiInventoryHost.create_from_cache( inventory_hostname=inventory_hostname, - host_properties=host_properties + properties=host_properties ) self.__update_inventory(esxi_host) @@ -332,7 +289,7 @@ def populate_from_vcenter(self, config_data): continue esxi_host = EsxiInventoryHost.create_from_object( - host_object=host_object, + vmware_object=host_object, properties_to_gather=properties_to_gather, pyvmomi_client=self.pyvmomi_client ) @@ -354,35 +311,6 @@ def __update_inventory(self, esxi_host): self.add_host_to_groups_based_on_path(esxi_host) self.set_host_variables_from_host_properties(esxi_host) - def set_inventory_hostname(self, esxi_host): - """ - The user can specify a list of jinja templates, and the first valid template should be used for the - host's inventory hostname. The inventory hostname is mostly for decorative purposes since the - ansible_host value takes precedence when trying to connect. - """ - hostname = None - errors = [] - - for hostname_pattern in self.get_option("hostnames"): - try: - hostname = self._compose(template=hostname_pattern, variables=esxi_host.properties) - except Exception as e: - if self.get_option("strict"): - raise AnsibleError( - "Could not compose %s as hostnames - %s" - % (hostname_pattern, to_native(e)) - ) - - errors.append((hostname_pattern, str(e))) - if hostname: - esxi_host.inventory_hostname = hostname - return - - raise AnsibleError( - "Could not template any hostname for host, errors for each preference: %s" - % (", ".join(["%s: %s" % (pref, err) for pref, err in errors])) - ) - def add_host_to_inventory(self, esxi_host: EsxiInventoryHost): """ Add the host to the inventory and any groups that the user wants to create based on inventory @@ -398,44 +326,3 @@ def add_host_to_inventory(self, esxi_host: EsxiInventoryHost): self.get_option("groups"), esxi_host.properties, esxi_host.inventory_hostname, strict=strict) self._add_host_to_keyed_groups( self.get_option("keyed_groups"), esxi_host.properties, esxi_host.inventory_hostname, strict=strict) - - def add_host_to_groups_based_on_path(self, esxi_host: EsxiInventoryHost): - """ - If the user desires, create groups based on each ESXi host's path. A group is created for each - step down in the path, with the group from the step above containing subsequent groups. - Optionally, the user can add a prefix to the groups created by this process. - The final group in the path will be where the ESXi host is added. - """ - if not self.get_option("group_by_paths"): - return - - path_parts = esxi_host.path.split('/') - group_name_parts = [] - last_created_group = None - - if self.get_option("group_by_paths_prefix"): - group_name_parts = [self.get_option("group_by_paths_prefix")] - - for path_part in path_parts: - if not path_part: - continue - group_name_parts.append(path_part) - group_name = self._sanitize_group_name('_'.join(group_name_parts)) - group = self.inventory.add_group(group_name) - - if last_created_group: - self.inventory.add_child(last_created_group, group) - last_created_group = group - - if last_created_group: - self.inventory.add_host(esxi_host.inventory_hostname, last_created_group) - - def set_host_variables_from_host_properties(self, esxi_host): - if self.get_option("sanitize_property_names"): - esxi_host.sanitize_properties() - - if self.get_option("flatten_nested_properties"): - esxi_host.flatten_properties() - - for k, v in esxi_host.properties.items(): - self.inventory.set_variable(esxi_host.inventory_hostname, k, v) diff --git a/plugins/inventory/vms.py b/plugins/inventory/vms.py new file mode 100644 index 00000000..5dc85989 --- /dev/null +++ b/plugins/inventory/vms.py @@ -0,0 +1,305 @@ +# Copyright: (c) 2024, Ansible Cloud Team +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt 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 = r""" +name: vms +short_description: Create an inventory containing VMware VMs +author: + - Ansible Cloud Team (@ansible-collections) +description: + - Create a dynamic inventory of VMware VMs from a vCenter or ESXi environment. + - Uses any file which ends with vms.yml, vms.yaml, vmware_vms.yml, or vmware_vms.yaml as a YAML configuration file. + +extends_documentation_fragment: + - vmware.vmware.base_options + - vmware.vmware.additional_rest_options + - vmware.vmware.plugin_base_options + - ansible.builtin.inventory_cache + - ansible.builtin.constructed + +requirements: + - vSphere Automation SDK (when gather_tags is True) + +options: + gather_tags: + description: + - If true, gather any tags attached to the associated VMs + - Requires 'vSphere Automation SDK' library to be installed on the Ansible controller machine. + default: false + type: bool + hostnames: + description: + - A list of templates evaluated in order to compose inventory_hostname. + - Each value in the list should be a jinja template. You can see the examples section for more details. + - Templates that result in an empty string or None value are ignored and the next template is evaluated. + - You can use hostvars such as properties specified in O(properties) as variables in the template. + type: list + elements: string + default: ['name'] + properties: + description: + - Specify a list of VMware schema properties associated with the VM to collect and return as hostvars. + - Each value in the list can be a path to a specific property in VM object or a path to a collection of VM objects. + - Please make sure that if you use a property in another parameter that it is included in this option. + - Some properties are always returned, such as name, customValue, and summary.runtime.powerState + - Use V(all) to return all properties available for the VM. + type: list + elements: string + default: [ + 'name', 'config.cpuHotAddEnabled', 'config.cpuHotRemoveEnabled', + 'config.instanceUuid', 'config.hardware.numCPU', 'config.template', + 'config.name', 'config.uuid', 'guest.hostName', 'guest.ipAddress', + 'guest.guestId', 'guest.guestState', 'runtime.maxMemoryUsage', + 'customValue', 'summary.runtime.powerState', 'config.guestId' + ] + flatten_nested_properties: + description: + - If true, flatten any nested properties into their dot notation names. + - For example 'summary["runtime"]["powerState"]' would become "summary.runtime.powerState" + type: bool + default: false + keyed_groups: + description: + - Use the values of VM properties or other hostvars to create and populate groups. + type: list + default: [ + {key: 'config.guestId', separator: ''}, + {key: 'summary.runtime.powerState', separator: ''}, + ] + search_paths: + description: + - Specify a list of paths that should be searched recursively for VMs. + - This effectively allows you to only include VMs in certain datacenters, clusters, or folders. + - >- + Filtering is done before the initial VM gathering query. If you have a large number of VMs, specifying + a subset of paths to search can help speed up the inventory plugin. + - The default value is an empty list, which means all paths (i.e. all datacenters) will be searched. + type: list + elements: str + default: [] + group_by_paths: + description: + - If true, groups will be created based on the VM's paths. + - >- + Paths will be sanitized to match Ansible group name standards. + For example, any slashes or dashes in the paths will be replaced by underscores in the group names. + - A group is created for each step down in the path, with the group from the step above containing subsequent groups. + - For example, a path /DC-01/vms/Cluster will create groups 'DC_01' which contains group 'DC_01_vms' which contains group 'DC_01_vms_Cluster' + default: false + type: bool + group_by_paths_prefix: + description: + - If O(group_by_paths) is true, set this variable if you want to add a prefix to any groups created based on paths. + - By default, no prefix is added to the group names. + default: '' + type: str + sanitize_property_names: + description: + - If true, sanitize VM property names so they can safely be referenced within Ansible playbooks. + - This option also transforms property names to snake case. For example, powerState would become power_state. + type: bool + default: false +""" + +EXAMPLES = r""" +# Below are examples of inventory configuration files that can be used with this plugin. +# To test these and see the resulting inventory, save the snippet in a file named hosts.vmware_vms.yml and run: +# ansible-inventory -i hosts.vmware_vms.yml --list + + +# Simple configuration with in-file authentication parameters +plugin: vmware.vmware.vms +hostname: 10.65.223.31 +username: administrator@vsphere.local +password: Esxi@123$% +validate_certs: false + + +# More complex configuration. Authentication parameters are assumed to be set as environment variables. +plugin: vmware.vmware.vms + +# Create groups based on host paths +group_by_paths: true + +# Create a group with hosts that support vMotion using the vmotionSupported property +properties: ["name", "capability"] +groups: + vmotion_supported: capability.vmotionSupported + +# Only gather hosts found in certain paths +search_paths: + - /DC1/host/ClusterA + - /DC1/host/ClusterC + - /DC3 + +# Set custom inventory hostnames based on attributes +hostnames: + - "'ESXi - ' + name + ' - ' + management_ip" + - "'ESXi - ' + name" + +# Use compose to set variables for the hosts that we find +compose: + ansible_user: "'root'" + ansible_connection: "'ssh'" + # assuming path is something like /MyDC/vms/MyCluster + datacenter: "(path | split('/'))[1]" + cluster: "(path | split('/'))[3]" +""" + +try: + from pyVmomi import vim +except ImportError: + # Already handled in base class + pass + +from ansible_collections.vmware.vmware.plugins.inventory_utils._base import ( + VmwareInventoryHost, + VmwareInventoryBase +) + + +class VmInventoryHost(VmwareInventoryHost): + def __init__(self): + super().__init__() + self._guest_ip = None + + @property + def guest_ip(self): + if self._guest_ip: + return self._guest_ip + + try: + self._guest_ip = self.properties['guest']['ipAddress'] + except KeyError: + self._guest_ip = "" + + return self._guest_ip + + +class InventoryModule(VmwareInventoryBase): + + NAME = "vmware.vmware.vms" + + def verify_file(self, path): + """ + Checks the plugin configuration file format and name, and returns True + if everything is valid. + Args: + path: Path to the configuration YAML file + Returns: + True if everything is correct, else False + """ + if super(InventoryModule, self).verify_file(path): + return path.endswith( + ( + "vms.yml", + "vms.yaml", + "vmware_vms.yaml", + "vmware_vms.yml" + ) + ) + return False + + def parse(self, inventory, loader, path, cache=True): + """ + Parses the inventory file options and creates an inventory based on those inputs + """ + super(InventoryModule, self).parse(inventory, loader, path, cache=cache) + cache_key = self.get_cache_key(path) + result_was_cached, results = self.get_cached_result(cache, cache_key) + + if result_was_cached: + self.populate_from_cache(results) + else: + results = self.populate_from_vcenter(self._read_config_data(path)) + + self.update_cached_result(cache, cache_key, results) + + def parse_properties_param(self): + """ + The properties option can be a variety of inputs from the user and we need to + manipulate it into a list of properties that can be used later. + Returns: + A list of property names that should be returned in the inventory. An empty + list means all properties should be collected + """ + properties_param = self.get_option("properties") + if not isinstance(properties_param, list): + properties_param = [properties_param] + + if "all" in properties_param: + return [] + + if "name" not in properties_param: + properties_param.append("name") + + if "summary.runtime.powerState" not in properties_param: + properties_param.append("summary.runtime.powerState") + + return properties_param + + def populate_from_cache(self, cache_data): + """ + Populate inventory data from cache + """ + for inventory_hostname, vm_properties in cache_data.items(): + vm = VmInventoryHost.create_from_cache( + inventory_hostname=inventory_hostname, + properties=vm_properties + ) + self.__update_inventory(vm) + + def populate_from_vcenter(self, config_data): + """ + Populate inventory data from vCenter + """ + hostvars = {} + properties_to_gather = self.parse_properties_param() + self.initialize_pyvmomi_client(config_data) + if self.get_option("gather_tags"): + self.initialize_rest_client(config_data) + + for vm_object in self.get_objects_by_type(vim_type=[vim.VirtualMachine]): + vm = VmInventoryHost.create_from_object( + vmware_object=vm_object, + properties_to_gather=properties_to_gather, + pyvmomi_client=self.pyvmomi_client + ) + + if self.get_option("gather_tags"): + tags, tags_by_category = self.gather_tags(vm.object._GetMoId()) + vm.properties["tags"] = tags + vm.properties["tags_by_category"] = tags_by_category + + self.set_inventory_hostname(vm) + if vm.inventory_hostname not in hostvars: + hostvars[vm.inventory_hostname] = vm.properties + self.__update_inventory(vm) + + return hostvars + + def __update_inventory(self, vm): + self.add_host_to_inventory(vm) + self.add_host_to_groups_based_on_path(vm) + self.set_host_variables_from_host_properties(vm) + + def add_host_to_inventory(self, vm: VmInventoryHost): + """ + Add the host to the inventory and any groups that the user wants to create based on inventory + parameters like groups or keyed groups. + """ + strict = self.get_option("strict") + self.inventory.add_host(vm.inventory_hostname) + self.inventory.set_variable(vm.inventory_hostname, "ansible_host", vm.guest_ip) + + self._set_composite_vars( + self.get_option("compose"), vm.properties, vm.inventory_hostname, strict=strict) + self._add_host_to_composed_groups( + self.get_option("groups"), vm.properties, vm.inventory_hostname, strict=strict) + self._add_host_to_keyed_groups( + self.get_option("keyed_groups"), vm.properties, vm.inventory_hostname, strict=strict) diff --git a/plugins/inventory_utils/_base.py b/plugins/inventory_utils/_base.py index b19268d8..00971d99 100644 --- a/plugins/inventory_utils/_base.py +++ b/plugins/inventory_utils/_base.py @@ -6,13 +6,75 @@ __metaclass__ = type +from ansible.errors import AnsibleError from ansible.errors import AnsibleParserError from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + from ansible_collections.vmware.vmware.plugins.module_utils.clients._pyvmomi import PyvmomiClient from ansible_collections.vmware.vmware.plugins.module_utils.clients._rest import VmwareRestClient +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_folder_paths import ( + get_folder_path_of_vsphere_object +) +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_facts import ( + vmware_obj_to_json, + flatten_dict +) + + +class VmwareInventoryHost(): + def __init__(self): + self.object = None + self.inventory_hostname = None + self.path = '' + self.properties = dict() + + @classmethod + def create_from_cache(cls, inventory_hostname, properties): + """ + Create the class from the inventory cache. We don't want to refresh the data or make any calls to vCenter. + Properties are populated from whatever we had previously cached. + """ + host = cls() + host.inventory_hostname = inventory_hostname + host.properties = properties + return host + + @classmethod + def create_from_object(cls, vmware_object, properties_to_gather, pyvmomi_client): + """ + Create the class from a host object that we got from pyvmomi. The host properties will be populated + from the object and additional calls to vCenter + """ + host = cls() + host.object = vmware_object + host.path = get_folder_path_of_vsphere_object(vmware_object) + host.properties = host.set_properties_from_pyvmomi(properties_to_gather, pyvmomi_client) + return host + + def get_properties_from_pyvmomi(self, properties_to_gather, pyvmomi_client): + properties = vmware_obj_to_json(self.object, properties_to_gather) + properties['path'] = self.path + + # Custom values + if hasattr(self.object, "customValue"): + properties['customValue'] = dict() + field_mgr = pyvmomi_client.custom_field_mgr + for cust_value in self.object.customValue: + properties['customValue'][ + [y.name for y in field_mgr if y.key == cust_value.key][0] + ] = cust_value.value + + return properties + + def sanitize_properties(self): + self.properties = camel_dict_to_snake_dict(self.properties) + + def flatten_properties(self): + self.properties = flatten_dict(self.properties) class VmwareInventoryBase(BaseInventoryPlugin, Constructable, Cacheable): @@ -185,3 +247,73 @@ def gather_tags(self, object_moid): tags_by_category[category_name].append({tag.id: tag.name}) return tags, tags_by_category + + def set_inventory_hostname(self, vmware_host_object): + """ + The user can specify a list of jinja templates, and the first valid template should be used for the + host's inventory hostname. The inventory hostname is mostly for decorative purposes since the + ansible_host value takes precedence when trying to connect. + """ + hostname = None + errors = [] + + for hostname_pattern in self.get_option("hostnames"): + try: + hostname = self._compose(template=hostname_pattern, variables=vmware_host_object.properties) + except Exception as e: + if self.get_option("strict"): + raise AnsibleError( + "Could not compose %s as hostnames - %s" + % (hostname_pattern, to_native(e)) + ) + + errors.append((hostname_pattern, str(e))) + if hostname: + vmware_host_object.inventory_hostname = hostname + return + + raise AnsibleError( + "Could not template any hostname for host, errors for each preference: %s" + % (", ".join(["%s: %s" % (pref, err) for pref, err in errors])) + ) + + def set_host_variables_from_host_properties(self, vmware_host_object): + if self.get_option("sanitize_property_names"): + vmware_host_object.sanitize_properties() + + if self.get_option("flatten_nested_properties"): + vmware_host_object.flatten_properties() + + for k, v in vmware_host_object.properties.items(): + self.inventory.set_variable(vmware_host_object.inventory_hostname, k, v) + + def add_host_to_groups_based_on_path(self, vmwre_host_object): + """ + If the user desires, create groups based on each VM's path. A group is created for each + step down in the path, with the group from the step above containing subsequent groups. + Optionally, the user can add a prefix to the groups created by this process. + The final group in the path will be where the VM is added. + """ + if not self.get_option("group_by_paths"): + return + + path_parts = vmwre_host_object.path.split('/') + group_name_parts = [] + last_created_group = None + + if self.get_option("group_by_paths_prefix"): + group_name_parts = [self.get_option("group_by_paths_prefix")] + + for path_part in path_parts: + if not path_part: + continue + group_name_parts.append(path_part) + group_name = self._sanitize_group_name('_'.join(group_name_parts)) + group = self.inventory.add_group(group_name) + + if last_created_group: + self.inventory.add_child(last_created_group, group) + last_created_group = group + + if last_created_group: + self.inventory.add_host(vmwre_host_object.inventory_hostname, last_created_group) diff --git a/tests/integration/targets/vmware_inventory_vms/defaults/main.yml b/tests/integration/targets/vmware_inventory_vms/defaults/main.yml new file mode 100644 index 00000000..539b766c --- /dev/null +++ b/tests/integration/targets/vmware_inventory_vms/defaults/main.yml @@ -0,0 +1 @@ +run_on_simulator: false diff --git a/tests/integration/targets/vmware_inventory_vms/files/test.vms.yml b/tests/integration/targets/vmware_inventory_vms/files/test.vms.yml new file mode 100644 index 00000000..f09167b9 --- /dev/null +++ b/tests/integration/targets/vmware_inventory_vms/files/test.vms.yml @@ -0,0 +1,8 @@ +--- +plugin: vmware.vmware.vms +cache: false +group_by_paths: true +group_by_paths_prefix: test +gather_tags: true +search_paths: + - /Eco-Datacenter/vm/ecoqe2 diff --git a/tests/integration/targets/vmware_inventory_vms/run.yml b/tests/integration/targets/vmware_inventory_vms/run.yml new file mode 100644 index 00000000..03166de0 --- /dev/null +++ b/tests/integration/targets/vmware_inventory_vms/run.yml @@ -0,0 +1,13 @@ +- hosts: localhost + gather_facts: no + tasks: + - name: Import eco-vcenter credentials + ansible.builtin.include_vars: + file: ../../integration_config.yml + tags: eco-vcenter-ci + + - name: Call vms inventory role + ansible.builtin.import_role: + name: vmware_inventory_vms + tags: + - eco-vcenter-ci diff --git a/tests/integration/targets/vmware_inventory_vms/tasks/main.yml b/tests/integration/targets/vmware_inventory_vms/tasks/main.yml new file mode 100644 index 00000000..7ef076fd --- /dev/null +++ b/tests/integration/targets/vmware_inventory_vms/tasks/main.yml @@ -0,0 +1,51 @@ +--- +- block: + - name: Import common vars + ansible.builtin.include_vars: + file: ../group_vars.yml + + # the ansible-inventory process does not have access to any of the variables in this playbook, + # so the auth vars are exposed as env vars + - name: Run Inventory Plugin + ansible.builtin.command: ansible-inventory -i "{{ role_path }}/files/test.vms.yml" --list + register: _inventory_out + environment: + VMWARE_HOST: "{{ vcenter_hostname }}" + VMWARE_USER: "{{ vcenter_username }}" + VMWARE_PASSWORD: "{{ vcenter_password }}" + VMWARE_PORT: "{{ vcenter_port }}" + VMWARE_VALIDATE_CERTS: "false" + + - name: Parse Inventory Results as JSON + ansible.builtin.set_fact: + inventory_results: "{{ _inventory_out.stdout | from_json }}" + + - name: Debug Inventory Output Because It Failed + when: not inventory_results._meta.hostvars + block: + - name: Try to Print Inventory Stderr + ansible.builtin.debug: + var: _inventory_out.stderr + - name: Try to Print the Parsed Stdout + ansible.builtin.debug: + var: (_inventory_out.stdout | from_json) + rescue: + - name: Print the Raw Output Since Parsing Failed + ansible.builtin.debug: + var: _inventory_out + + # you can't reference the 'all' property here for some reason. It reverts back to the test playbook inventory + # instead of the inventory_results + - name: Check Output + ansible.builtin.assert: + that: + - first_host.ansible_host is regex('^[\d+\.]+$') or first_host.ansible_host is regex('^[(\w{3}|\w{4}):]+$') + - first_host.tags is defined and first_host.tags is mapping + - first_host.tags_by_category is defined and first_host.tags_by_category is mapping + - >- + (inventory_results.poweredOn.hosts | length) == + (inventory_results._meta.hostvars.values() | selectattr('summary.runtime.powerState', 'equalto', 'poweredOn') | length) + - (inventory_results | length) > 3 + - ('test_' + vcenter_datacenter | replace('-', '_')) in inventory_results.keys() + vars: + first_host: "{{ (inventory_results._meta.hostvars.values() | first) }}"