From 5313bde408c98038375b0816272f9b0853968a98 Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Mon, 11 Nov 2024 11:11:07 -0500 Subject: [PATCH 1/6] migrate cluster info module from community --- .../80-migrate-cluster-info-module.yml | 2 + meta/runtime.yml | 1 + plugins/module_utils/_vmware_facts.py | 124 ++++++++- plugins/module_utils/_vmware_rest_client.py | 12 + plugins/modules/cluster_info.py | 249 ++++++++++++++++++ .../targets/vmware_cluster/tasks/main.yml | 2 +- .../targets/vmware_cluster_dpm/tasks/main.yml | 9 +- .../targets/vmware_cluster_drs/tasks/main.yml | 9 +- .../vmware_cluster_info/defaults/main.yml | 3 + .../targets/vmware_cluster_info/run.yml | 25 ++ .../vmware_cluster_info/tasks/main.yml | 90 +++++++ .../targets/vmware_cluster_info/vars.yml | 8 + tests/unit/plugins/modules/common/utils.py | 8 + .../modules/common/vmware_object_mocks.py | 21 ++ .../unit/plugins/modules/test_cluster_info.py | 40 +++ 15 files changed, 593 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/80-migrate-cluster-info-module.yml create mode 100644 plugins/modules/cluster_info.py create mode 100644 tests/integration/targets/vmware_cluster_info/defaults/main.yml create mode 100644 tests/integration/targets/vmware_cluster_info/run.yml create mode 100644 tests/integration/targets/vmware_cluster_info/tasks/main.yml create mode 100644 tests/integration/targets/vmware_cluster_info/vars.yml create mode 100644 tests/unit/plugins/modules/common/vmware_object_mocks.py create mode 100644 tests/unit/plugins/modules/test_cluster_info.py diff --git a/changelogs/fragments/80-migrate-cluster-info-module.yml b/changelogs/fragments/80-migrate-cluster-info-module.yml new file mode 100644 index 00000000..8989403f --- /dev/null +++ b/changelogs/fragments/80-migrate-cluster-info-module.yml @@ -0,0 +1,2 @@ +minor_changes: + - cluster_info - Migrate cluster_info module from the community.vmware collection to here diff --git a/meta/runtime.yml b/meta/runtime.yml index 08ed58b8..3c03e632 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -7,6 +7,7 @@ action_groups: - cluster_dpm - cluster_drs - cluster_drs_recommendations + - cluster_info - cluster_vcls - content_template - folder_template_from_vm diff --git a/plugins/module_utils/_vmware_facts.py b/plugins/module_utils/_vmware_facts.py index 2ae2d071..02318f25 100644 --- a/plugins/module_utils/_vmware_facts.py +++ b/plugins/module_utils/_vmware_facts.py @@ -15,7 +15,7 @@ PYVMOMI_IMP_ERR = None try: - from pyVmomi import vim, VmomiSupport + from pyVmomi import vim, VmomiSupport, vmodl except ImportError: pass @@ -281,6 +281,128 @@ def hw_network_device_facts(self): } +class ClusterFacts(): + def __init__(self, cluster): + self.cluster = cluster + + def all_facts(self): + return { + **self.host_facts(), + **self.ha_facts(), + **self.identifier_facts(), + **self.drs_facts(), + **self.vsan_facts(), + **self.resource_usage_facts(), + **self.dpm_facts() + } + + def identifier_facts(self): + return { + "moid": self.cluster._moId, + "datacenter": self.cluster.parent.parent.name + } + + def host_facts(self): + hosts = [] + for host in self.cluster.host: + hosts.append({ + 'name': host.name, + 'folder': get_folder_path_of_vm(host), + }) + return {"hosts": hosts} + + def ha_facts(self): + fact_keys = [ + "ha_enabled", "ha_failover_level", "ha_vm_monitoring", "ha_admission_control_enabled", + "ha_restart_priority", "ha_vm_tools_monitoring", "ha_vm_min_up_time", "ha_vm_max_failures", + "ha_vm_max_failure_window", "ha_vm_failure_interval" + ] + ha_facts = dict.fromkeys(fact_keys, None) + das_config = self.cluster.configurationEx.dasConfig + if not das_config: + ha_facts['ha_enabled'] = False + return ha_facts + + ha_facts["ha_enabled"] = das_config.enabled + ha_facts["ha_vm_monitoring"] = das_config.vmMonitoring + ha_facts["ha_host_monitoring"] = das_config.hostMonitoring + ha_facts["ha_admission_control_enabled"] = das_config.admissionControlEnabled + + if getattr(das_config, "admissionControlPolicy"): + ha_facts['ha_failover_level'] = das_config.admissionControlPolicy.failoverLevel + if getattr(das_config, "defaultVmSettings"): + ha_facts['ha_restart_priority'] = das_config.defaultVmSettings.restartPriority + ha_facts['ha_vm_tools_monitoring'] = das_config.defaultVmSettings.vmToolsMonitoringSettings.vmMonitoring + ha_facts['ha_vm_min_up_time'] = das_config.defaultVmSettings.vmToolsMonitoringSettings.minUpTime + ha_facts['ha_vm_max_failures'] = das_config.defaultVmSettings.vmToolsMonitoringSettings.maxFailures + ha_facts['ha_vm_max_failure_window'] = das_config.defaultVmSettings.vmToolsMonitoringSettings.maxFailureWindow + ha_facts['ha_vm_failure_interval'] = das_config.defaultVmSettings.vmToolsMonitoringSettings.failureInterval + + return ha_facts + + def dpm_facts(self): + fact_keys = ["dpm_enabled", "dpm_default_dpm_behavior", "dpm_host_power_action_rate"] + output_facts = dict.fromkeys(fact_keys, None) + dpm_config = self.cluster.configurationEx.dpmConfigInfo + if not dpm_config: + output_facts["dpm_enabled"] = False + return output_facts + + output_facts["dpm_enabled"] = dpm_config.enabled + output_facts["dpm_default_dpm_behavior"] = getattr(dpm_config, "defaultDpmBehavior", None) + # dpm host power rate is reversed by the vsphere API. so a 1 in the API is really a 5 in the UI + try: + output_facts["dpm_host_power_action_rate"] = 6 - int(dpm_config.hostPowerActionRate) + except (TypeError, AttributeError): + output_facts["dpm_host_power_action_rate"] = 3 + + return output_facts + + def drs_facts(self): + fact_keys = ["drs_enabled", "drs_enable_vm_behavior_overrides", "drs_default_vm_behavior", "drs_vmotion_rate"] + output_facts = dict.fromkeys(fact_keys, None) + drs_config = self.cluster.configurationEx.drsConfig + if not drs_config: + output_facts["drs_enabled"] = False + return output_facts + + output_facts["drs_enabled"] = drs_config.enabled + output_facts["drs_enable_vm_behavior_overrides"] = getattr(drs_config, "enableVmBehaviorOverrides", None) + + # docs call one option 'automatic' but the API calls it 'automated'. So we adjust here to match docs + _drs_default_vm_behavior = getattr(drs_config, "defaultVmBehavior", None) + output_facts["drs_default_vm_behavior"] = 'automatic' if _drs_default_vm_behavior == 'automated' else _drs_default_vm_behavior + + # drs vmotion rate is reversed by the vsphere API. so a 1 in the API is really a 5 in the UI + try: + output_facts["drs_vmotion_rate"] = 6 - int(drs_config.vmotionRate) + except (TypeError, AttributeError): + output_facts["drs_vmotion_rate"] = 3 + + return output_facts + + def vsan_facts(self): + vsan_config = getattr(self.cluster.configurationEx, 'vsanConfigInfo', None) + vsan_facts = { + "vsan_enabled": False, + "vsan_auto_claim_storage": None + } + if vsan_config: + vsan_facts['vsan_enabled'] = vsan_config.enabled + vsan_facts['vsan_auto_claim_storage'] = vsan_config.defaultConfig.autoClaimStorage + return vsan_facts + + def resource_usage_facts(self): + try: + resource_summary = vmware_obj_to_json(self.cluster.GetResourceUsage()) + except vmodl.fault.MethodNotFound: + return {'resource_summary': {}} + + if '_vimtype' in resource_summary: + del resource_summary['_vimtype'] + return {'resource_summary': resource_summary} + + def get_vm_prop_or_none(vm, attributes): """Safely get a property or return None""" result = vm diff --git a/plugins/module_utils/_vmware_rest_client.py b/plugins/module_utils/_vmware_rest_client.py index 0b700472..09d8ac3c 100644 --- a/plugins/module_utils/_vmware_rest_client.py +++ b/plugins/module_utils/_vmware_rest_client.py @@ -440,6 +440,18 @@ def get_tags_by_vm_moid(self, vm_moid): dobj = DynamicID(type='VirtualMachine', id=vm_moid) return self.get_tags_for_dynamic_id_obj(dobj=dobj) + def get_tags_by_cluster_moid(self, cluster_moid): + """ + Get a list of tag objects attached to a cluster + Args: + vm_mid: the cluster MOID to use to gather tags + + Returns: + List of tag object associated with the given cluster + """ + dobj = DynamicID(type='ClusterComputeResource', id=cluster_moid) + return self.get_tags_for_dynamic_id_obj(dobj=dobj) + def format_tag_identity_as_dict(self, tag_obj): """ Takes a tag object and outputs a dictionary with identifying details about the tag, diff --git a/plugins/modules/cluster_info.py b/plugins/modules/cluster_info.py new file mode 100644 index 00000000..777a566e --- /dev/null +++ b/plugins/modules/cluster_info.py @@ -0,0 +1,249 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Ansible Cloud Team (@ansible-collections) +# 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''' +--- +module: cluster_info +short_description: Gathers information about one or more clusters +description: + - >- + Gathers information about one or more clusters. + You can search for clusters based on the cluster name, datacenter name, or a combination of the two. +author: + - Ansible Cloud Team (@ansible-collections) + +options: + cluster: + description: + - The name of the cluster on which to gather info. + - At least one of O(datacenter) or O(cluster) is required. + type: str + required: false + aliases: [cluster_name, name] + datacenter: + description: + - The name of the datacenter. + - At least one of O(datacenter) or O(cluster) is required. + type: str + required: false + aliases: [datacenter_name] + gather_tags: + description: + - If true, gather any tags attached to the cluster(s) + type: bool + default: false + required: false + schema: + description: + - Specify the output schema desired. + - The V(summary) output schema is the legacy output from the module. + - The V(vsphere) output schema is the vSphere API class definition. + choices: ['summary', 'vsphere'] + default: 'summary' + type: str + properties: + description: + - If the schema is 'vsphere', gather these specific properties only + type: list + elements: str + +extends_documentation_fragment: + - vmware.vmware.vmware.vcenter_documentation +''' + +EXAMPLES = r''' +- name: Gather Cluster Information + vmware.vmware.cluster_info: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + datacenter_name: datacenter + cluster_name: my_cluster + register: _out + +- name: Gather Information About All Clusters In a Datacenter + vmware.vmware.cluster_info: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + datacenter_name: datacenter + register: _out + +- name: Gather Specific Properties About a Cluster + vmware.vmware.cluster_info: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + cluster_name: my_cluster + schema: vsphere + properties: + - name + - configuration.dasConfig.enabled + - summary.totalCpu + register: _out +''' + +RETURN = r''' +clusters: + description: + - A dictionary that describes the clusters found by the search parameters + - The keys are the cluster names and the values are dictionaries with the cluster info. + returned: On success + type: dict + sample: { + "clusters": { + "My-Cluster": { + "datacenter": "My-Datacenter", + "dpm_default_dpm_behavior": "automated", + "dpm_enabled": false, + "dpm_host_power_action_rate": 3, + "drs_default_vm_behavior": "fullyAutomated", + "drs_enable_vm_behavior_overrides": true, + "drs_enabled": true, + "drs_vmotion_rate": 3, + "ha_admission_control_enabled": true, + "ha_enabled": false, + "ha_failover_level": 1, + "ha_host_monitoring": "enabled", + "ha_restart_priority": "medium", + "ha_vm_failure_interval": 30, + "ha_vm_max_failure_window": -1, + "ha_vm_max_failures": 3, + "ha_vm_min_up_time": 120, + "ha_vm_monitoring": "vmMonitoringDisabled", + "ha_vm_tools_monitoring": "vmMonitoringDisabled", + "hosts": [ + { + "folder": "/My-Datacenter/host/My-Cluster", + "name": "Esxi-1" + }, + { + "folder": "/My-Datacenter/host/My-Cluster", + "name": "Esxi-2" + } + ], + "moid": "domain-c11", + "resource_summary": { + "cpuCapacityMHz": 514080, + "cpuUsedMHz": 21241, + "memCapacityMB": 1832586, + "memUsedMB": 348366, + "pMemAvailableMB": 0, + "pMemCapacityMB": 0, + "storageCapacityMB": 12238642, + "storageUsedMB": 4562117 + }, + "tags": [], + "vsan_auto_claim_storage": false, + "vsan_enabled": false + }, + } + } +''' + +try: + from pyVmomi import vim +except ImportError: + pass +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vmware.vmware.plugins.module_utils._vmware import PyVmomi, vmware_argument_spec +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_rest_client import VmwareRestClient +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_facts import ( + ClusterFacts, + vmware_obj_to_json +) + + +class ClusterInfo(PyVmomi): + def __init__(self, module): + super(ClusterInfo, self).__init__(module) + self.rest_client = None + if module.params['gather_tags']: + self.rest_client = VmwareRestClient(module) + + def get_clusters(self): + """ + Gets clusters matching the search parameters input by the user. + Returns: List of clusters to gather info about + """ + datacenter, search_folder = None, None + if self.params.get('datacenter'): + datacenter = self.get_datacenter_by_name(self.params.get('datacenter'), fail_on_missing=False) + search_folder = datacenter.hostFolder + + if self.params.get('cluster'): + _cluster = self.get_cluster_by_name(self.params.get('cluster'), fail_on_missing=False, datacenter=datacenter) + return [_cluster] if _cluster else [] + else: + _clusters = self.list_all_objs_by_type( + [vim.ClusterComputeResource], + folder=search_folder, + recurse=False + ) + return _clusters.keys() + + def gather_info_for_clusters(self): + """ + Gather information about one or more clusters + """ + all_cluster_info = {} + for cluster in self.get_clusters(): + cluster_info = {} + if self.params['schema'] == 'summary': + cluster_facts = ClusterFacts(cluster) + cluster_info = cluster_facts.all_facts() + else: + cluster_info = vmware_obj_to_json(cluster, self.params['properties']) + + cluster_info['tags'] = self._get_tags(cluster) + all_cluster_info[cluster.name] = cluster_info + + return all_cluster_info + + def _get_tags(self, cluster): + """ + Gets the tags on a cluster. Tags are formatted as a list of dictionaries corresponding to each tag + """ + output = [] + if not self.params.get('gather_tags'): + return output + + tags = self.rest_client.get_tags_by_cluster_moid(cluster._moId) + for tag in tags: + output.append(self.rest_client.format_tag_identity_as_dict(tag)) + + return output + + +def main(): + module = AnsibleModule( + argument_spec={ + **vmware_argument_spec(), **dict( + cluster=dict(type='str', aliases=['cluster_name', 'name']), + datacenter=dict(type='str', aliases=['datacenter_name']), + gather_tags=dict(type='bool', default=False), + schema=dict(type='str', choices=['summary', 'vsphere'], default='summary'), + properties=dict(type='list', elements='str'), + ) + }, + supports_check_mode=True, + required_one_of=[('cluster', 'datacenter')], + ) + if module.params['schema'] != 'vsphere' and module.params.get('properties'): + module.fail_json(msg="The option 'properties' is only valid when the schema is 'vsphere'") + + cluster_info = ClusterInfo(module) + clusters = cluster_info.gather_info_for_clusters() + module.exit_json(changed=False, clusters=clusters) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/vmware_cluster/tasks/main.yml b/tests/integration/targets/vmware_cluster/tasks/main.yml index d79b651b..8c7960bd 100644 --- a/tests/integration/targets/vmware_cluster/tasks/main.yml +++ b/tests/integration/targets/vmware_cluster/tasks/main.yml @@ -36,7 +36,7 @@ cluster_name: "{{ test_cluster }}" register: _create - name: Gather Cluster Info - community.vmware.vmware_cluster_info: + vmware.vmware.cluster_info: validate_certs: false hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_username }}" diff --git a/tests/integration/targets/vmware_cluster_dpm/tasks/main.yml b/tests/integration/targets/vmware_cluster_dpm/tasks/main.yml index 8126e7b3..f6e22315 100644 --- a/tests/integration/targets/vmware_cluster_dpm/tasks/main.yml +++ b/tests/integration/targets/vmware_cluster_dpm/tasks/main.yml @@ -62,7 +62,7 @@ ansible.builtin.assert: that: _out is not changed - name: Gather Cluster Settings - community.vmware.vmware_cluster_info: + vmware.vmware.cluster_info: validate_certs: false hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_username }}" @@ -71,7 +71,12 @@ cluster_name: "{{ test_cluster }}" port: "{{ vcenter_port }}" register: _cluster_info - # cluster_info doesn't return DPM config right now so we can't validate this + - name: Validate DPM Output + ansible.builtin.assert: + that: + - _cluster_info.clusters[test_cluster].dpm_enabled + - _cluster_info.clusters[test_cluster].dpm_default_dpm_behavior == dpm_automation_level + - _cluster_info.clusters[test_cluster].dpm_host_power_action_rate == dpm_recommendation_priority_threshold always: - name: Destroy Test Cluster diff --git a/tests/integration/targets/vmware_cluster_drs/tasks/main.yml b/tests/integration/targets/vmware_cluster_drs/tasks/main.yml index cf08a801..567f9b83 100644 --- a/tests/integration/targets/vmware_cluster_drs/tasks/main.yml +++ b/tests/integration/targets/vmware_cluster_drs/tasks/main.yml @@ -65,7 +65,7 @@ predictive_drs: "{{ drs_predictive_drs }}" register: _idempotence_check - name: Gather Cluster Settings - community.vmware.vmware_cluster_info: + vmware.vmware.cluster_info: validate_certs: false hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_username }}" @@ -74,17 +74,14 @@ cluster_name: "{{ test_cluster }}" port: "{{ vcenter_port }}" register: _cluster_info - # drs vmotion rate reported by vcenter api is backwards. So 1 is actually 5 in the UI - # and 5 is actually 1 in the UI. When we migrate cluster_info there is a ticket to fix the output - # so the number we return to the user makes sense, but for now we will fix it here with (6 - ) - name: Check DRS Settings Were Applied ansible.builtin.assert: that: - _idempotence_check is not changed - _config.drs_default_vm_behavior == drs_default_vm_behavior - _config.drs_enable_vm_behavior_overrides == drs_enable_vm_behavior_overrides - - _config.drs_vmotion_rate == (6 - drs_vmotion_rate) - - _config.enabled_drs == drs_enable + - _config.drs_vmotion_rate == drs_vmotion_rate + - _config.drs_enabled == drs_enable vars: _config: "{{ _cluster_info.clusters[test_cluster] }}" diff --git a/tests/integration/targets/vmware_cluster_info/defaults/main.yml b/tests/integration/targets/vmware_cluster_info/defaults/main.yml new file mode 100644 index 00000000..e48f624f --- /dev/null +++ b/tests/integration/targets/vmware_cluster_info/defaults/main.yml @@ -0,0 +1,3 @@ +--- +test_cluster: "{{ tiny_prefix }}_cluster_info_test" +run_on_simulator: false diff --git a/tests/integration/targets/vmware_cluster_info/run.yml b/tests/integration/targets/vmware_cluster_info/run.yml new file mode 100644 index 00000000..61e74777 --- /dev/null +++ b/tests/integration/targets/vmware_cluster_info/run.yml @@ -0,0 +1,25 @@ +- hosts: localhost + gather_facts: no + + tasks: + - name: Import eco-vcenter credentials + ansible.builtin.include_vars: + file: ../../integration_config.yml + tags: eco-vcenter-ci + + - name: Import simulator vars + ansible.builtin.include_vars: + file: vars.yml + tags: integration-ci + + - name: Vcsim + ansible.builtin.import_role: + name: prepare_vcsim + tags: integration-ci + + - name: Import vmware_cluster_info role + ansible.builtin.import_role: + name: vmware_cluster_info + tags: + - integration-ci + - eco-vcenter-ci diff --git a/tests/integration/targets/vmware_cluster_info/tasks/main.yml b/tests/integration/targets/vmware_cluster_info/tasks/main.yml new file mode 100644 index 00000000..e4f3f176 --- /dev/null +++ b/tests/integration/targets/vmware_cluster_info/tasks/main.yml @@ -0,0 +1,90 @@ +--- +- name: Test On Simulator + when: run_on_simulator + block: + - name: Create Cluster + vmware.vmware.cluster: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + cluster: "{{ test_cluster }}" + port: "{{ vcenter_port }}" + state: present + register: _create + - name: Gather Cluster Info + vmware.vmware.cluster_info: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + cluster: "{{ test_cluster }}" + port: "{{ vcenter_port }}" + register: _cluster_info + - name: Check Cluster Output + ansible.builtin.assert: + that: + - _cluster_info.clusters[test_cluster] is defined + - _create.cluster.moid == _cluster_info.clusters[test_cluster].moid + # deleting a cluster in the simulator is not supporter + +- name: Test On VCenter + when: not run_on_simulator + block: + - name: Import common vars + ansible.builtin.include_vars: + file: ../group_vars.yml + - name: Create Test Cluster + vmware.vmware.cluster: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + validate_certs: false + port: "{{ vcenter_port }}" + cluster_name: "{{ test_cluster }}" + register: _create + - name: Gather Cluster Info + vmware.vmware.cluster_info: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + cluster_name: "{{ test_cluster }}" + port: "{{ vcenter_port }}" + register: _cluster_info + - name: Check Cluster Output + ansible.builtin.assert: + that: + - _cluster_info.clusters[test_cluster] is defined + - _create.cluster.moid == _cluster_info.clusters[test_cluster].moid + - name: Gather Missing Cluster Info + vmware.vmware.cluster_info: + validate_certs: false + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + cluster_name: foo + port: "{{ vcenter_port }}" + register: _cluster_info + - name: Check Cluster Output + ansible.builtin.assert: + that: + - _cluster_info.clusters is defined + - not _cluster_info.clusters + + always: + - name: Destroy Test Cluster + vmware.vmware.cluster: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "{{ vcenter_datacenter }}" + port: "{{ vcenter_port }}" + validate_certs: false + cluster_name: "{{ test_cluster }}" + state: absent diff --git a/tests/integration/targets/vmware_cluster_info/vars.yml b/tests/integration/targets/vmware_cluster_info/vars.yml new file mode 100644 index 00000000..b1095dbb --- /dev/null +++ b/tests/integration/targets/vmware_cluster_info/vars.yml @@ -0,0 +1,8 @@ +vcenter_hostname: "127.0.0.1" +vcenter_username: "user" +vcenter_password: "pass" +vcenter_port: 8989 +vcenter_datacenter: DC0 +test_cluster: cluster_test + +run_on_simulator: true diff --git a/tests/unit/plugins/modules/common/utils.py b/tests/unit/plugins/modules/common/utils.py index fc371a64..d5d038e4 100644 --- a/tests/unit/plugins/modules/common/utils.py +++ b/tests/unit/plugins/modules/common/utils.py @@ -6,6 +6,7 @@ from ansible.module_utils import basic from ansible.module_utils._text import to_bytes +from ansible_collections.vmware.vmware.plugins.module_utils import _vmware import mock @@ -28,6 +29,13 @@ def set_module_args(add_cluster=True, **args): basic._ANSIBLE_ARGS = to_bytes(args) +def mock_pyvmomi(mocker): + connect_to_api = mocker.patch.object(_vmware, "connect_to_api") + _content = type('', (), {})() + _content.customFieldsManager = False + connect_to_api.return_value = None, _content + + class DummyDatacenter: pass diff --git a/tests/unit/plugins/modules/common/vmware_object_mocks.py b/tests/unit/plugins/modules/common/vmware_object_mocks.py new file mode 100644 index 00000000..1150a4f8 --- /dev/null +++ b/tests/unit/plugins/modules/common/vmware_object_mocks.py @@ -0,0 +1,21 @@ +class MockClusterConfiguration(): + def __init__(self): + self.dasConfig = None + self.dpmConfigInfo = None + self.drsConfig = None + + +class MockCluster(): + def __init__(self, name="test"): + self.configurationEx = MockClusterConfiguration() + self.host = [] + + self.name = name + self._moId = "1" + + self.parent = type('', (), {})() + self.parent.parent = type('', (), {})() + self.parent.parent.name = "dc" + + def GetResourceUsage(self): + return {} diff --git a/tests/unit/plugins/modules/test_cluster_info.py b/tests/unit/plugins/modules/test_cluster_info.py new file mode 100644 index 00000000..18cfa58a --- /dev/null +++ b/tests/unit/plugins/modules/test_cluster_info.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import sys +import pytest + +from ansible_collections.vmware.vmware.plugins.modules import cluster_info + +from .common.utils import ( + AnsibleExitJson, ModuleTestCase, set_module_args, mock_pyvmomi +) +from .common.vmware_object_mocks import MockCluster + +pytestmark = pytest.mark.skipif( + sys.version_info < (2, 7), reason="requires python2.7 or higher" +) + + +class TestClusterInfo(ModuleTestCase): + + def __prepare(self, mocker): + mock_pyvmomi(mocker) + + get_clusters = mocker.patch.object(cluster_info.ClusterInfo, "get_clusters") + get_clusters.return_value = [MockCluster()] + + def test_gather(self, mocker): + self.__prepare(mocker) + + set_module_args( + hostname="127.0.0.1", + username="administrator@local", + password="123456", + add_cluster=True, + ) + + with pytest.raises(AnsibleExitJson) as c: + cluster_info.main() + + assert c.value.args[0]["changed"] is False From ea704c01debab62eb56361f3142bc1e27d7bec0a Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Mon, 18 Nov 2024 09:11:39 -0500 Subject: [PATCH 2/6] bugfixes --- plugins/modules/cluster_info.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/modules/cluster_info.py b/plugins/modules/cluster_info.py index 777a566e..51e2ffbd 100644 --- a/plugins/modules/cluster_info.py +++ b/plugins/modules/cluster_info.py @@ -38,6 +38,7 @@ gather_tags: description: - If true, gather any tags attached to the cluster(s) + - This has no affect if the O(schema) is set to V(vsphere). In that case, add 'tag' to O(properties) or leave O(properties) unset. type: bool default: false required: false @@ -200,10 +201,13 @@ def gather_info_for_clusters(self): if self.params['schema'] == 'summary': cluster_facts = ClusterFacts(cluster) cluster_info = cluster_facts.all_facts() + cluster_info['tags'] = self._get_tags(cluster) else: - cluster_info = vmware_obj_to_json(cluster, self.params['properties']) + try: + cluster_info = vmware_obj_to_json(cluster, self.params['properties']) + except AttributeError as e: + self.module.fail_json(str(e)) - cluster_info['tags'] = self._get_tags(cluster) all_cluster_info[cluster.name] = cluster_info return all_cluster_info From db4b738f210d6bc7226d657041967eb900d9142c Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Tue, 19 Nov 2024 08:37:57 -0500 Subject: [PATCH 3/6] make drs and dpm rate values constants --- plugins/module_utils/_vmware_facts.py | 14 ++++++++++---- plugins/module_utils/_vmware_rest_client.py | 4 ++-- plugins/modules/cluster_dpm.py | 7 +++++-- plugins/modules/cluster_drs.py | 8 +++++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/_vmware_facts.py b/plugins/module_utils/_vmware_facts.py index 02318f25..aa2c0abe 100644 --- a/plugins/module_utils/_vmware_facts.py +++ b/plugins/module_utils/_vmware_facts.py @@ -282,9 +282,15 @@ def hw_network_device_facts(self): class ClusterFacts(): + DPM_DEFAULT_RATE = 3 + DRS_DEFAULT_RATE = 3 def __init__(self, cluster): self.cluster = cluster + @staticmethod + def reverse_drs_or_dpm_rate(input_rate): + return 6 - int(input_rate) + def all_facts(self): return { **self.host_facts(), @@ -352,9 +358,9 @@ def dpm_facts(self): output_facts["dpm_default_dpm_behavior"] = getattr(dpm_config, "defaultDpmBehavior", None) # dpm host power rate is reversed by the vsphere API. so a 1 in the API is really a 5 in the UI try: - output_facts["dpm_host_power_action_rate"] = 6 - int(dpm_config.hostPowerActionRate) + output_facts["dpm_host_power_action_rate"] = ClusterFacts.reverse_drs_or_dpm_rate(dpm_config.hostPowerActionRate) except (TypeError, AttributeError): - output_facts["dpm_host_power_action_rate"] = 3 + output_facts["dpm_host_power_action_rate"] = ClusterFacts.DPM_DEFAULT return output_facts @@ -375,9 +381,9 @@ def drs_facts(self): # drs vmotion rate is reversed by the vsphere API. so a 1 in the API is really a 5 in the UI try: - output_facts["drs_vmotion_rate"] = 6 - int(drs_config.vmotionRate) + output_facts["drs_vmotion_rate"] = ClusterFacts.reverse_drs_or_dpm_rate(drs_config.vmotionRate) except (TypeError, AttributeError): - output_facts["drs_vmotion_rate"] = 3 + output_facts["drs_vmotion_rate"] = ClusterFacts.DRS_DEFAULT return output_facts diff --git a/plugins/module_utils/_vmware_rest_client.py b/plugins/module_utils/_vmware_rest_client.py index 09d8ac3c..c7b9b5ec 100644 --- a/plugins/module_utils/_vmware_rest_client.py +++ b/plugins/module_utils/_vmware_rest_client.py @@ -432,7 +432,7 @@ def get_tags_by_vm_moid(self, vm_moid): """ Get a list of tag objects attached to a virtual machine Args: - vm_mid: the VM MOID to use to gather tags + vm_moid: the VM MOID to use to gather tags Returns: List of tag object associated with the given virtual machine @@ -444,7 +444,7 @@ def get_tags_by_cluster_moid(self, cluster_moid): """ Get a list of tag objects attached to a cluster Args: - vm_mid: the cluster MOID to use to gather tags + cluster_moid: the cluster MOID to use to gather tags Returns: List of tag object associated with the given cluster diff --git a/plugins/modules/cluster_dpm.py b/plugins/modules/cluster_dpm.py index d713b2db..36437401 100644 --- a/plugins/modules/cluster_dpm.py +++ b/plugins/modules/cluster_dpm.py @@ -117,6 +117,9 @@ TaskError, RunningTaskMonitor ) +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_facts import ( + ClusterFacts +) from ansible.module_utils._text import to_native @@ -148,7 +151,7 @@ def recommendation_priority_threshold(self): We present the scale seen in the docs/UI to the user and then adjust the value here to ensure vCenter behaves as intended. """ - return 6 - self.params['recommendation_priority_threshold'] + return ClusterFacts.reverse_drs_or_dpm_rate(self.params['recommendation_priority_threshold']) def check_dpm_config_diff(self): """ @@ -214,7 +217,7 @@ def main(): choices=['automatic', 'manual'], default='automatic' ), - recommendation_priority_threshold=dict(type='int', choices=[1, 2, 3, 4, 5], default=3) + recommendation_priority_threshold=dict(type='int', choices=[1, 2, 3, 4, 5], default=ClusterFacts.DPM_DEFAULT_RATE) ) }, supports_check_mode=True, diff --git a/plugins/modules/cluster_drs.py b/plugins/modules/cluster_drs.py index 06f96d84..b2b093d8 100644 --- a/plugins/modules/cluster_drs.py +++ b/plugins/modules/cluster_drs.py @@ -143,7 +143,9 @@ TaskError, RunningTaskMonitor ) - +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_facts import ( + ClusterFacts +) from ansible_collections.vmware.vmware.plugins.module_utils._vmware_type_utils import ( diff_dict_and_vmodl_options_set ) @@ -175,7 +177,7 @@ def drs_vmotion_rate(self): We present the scale seen in the docs/UI to the user and then adjust the value here to ensure vCenter behaves as intended. """ - return 6 - self.params.get('drs_vmotion_rate') + return ClusterFacts.reverse_drs_or_dpm_rate(self.params.get('drs_vmotion_rate')) def check_drs_config_diff(self): """ @@ -254,7 +256,7 @@ def main(): choices=['fullyAutomated', 'manual', 'partiallyAutomated'], default='fullyAutomated' ), - drs_vmotion_rate=dict(type='int', choices=[1, 2, 3, 4, 5], default=3), + drs_vmotion_rate=dict(type='int', choices=[1, 2, 3, 4, 5], default=ClusterFacts.DRS_DEFAULT_RATE), advanced_settings=dict(type='dict', required=False, default=dict()), predictive_drs=dict(type='bool', required=False, default=False), ) From bfdcf6ae1f87f9a979fbbca0f23a387704849832 Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Tue, 19 Nov 2024 08:40:19 -0500 Subject: [PATCH 4/6] fix folder path method name --- plugins/module_utils/_vmware_facts.py | 6 +++--- plugins/module_utils/_vmware_folder_paths.py | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/_vmware_facts.py b/plugins/module_utils/_vmware_facts.py index aa2c0abe..76cdc688 100644 --- a/plugins/module_utils/_vmware_facts.py +++ b/plugins/module_utils/_vmware_facts.py @@ -22,7 +22,7 @@ from ansible.module_utils._text import to_text from ansible.module_utils.six import integer_types, string_types, iteritems import ansible.module_utils.common._collections_compat as collections_compat -from ansible_collections.vmware.vmware.plugins.module_utils._vmware_folder_paths import get_folder_path_of_vm +from ansible_collections.vmware.vmware.plugins.module_utils._vmware_folder_paths import get_folder_path_of_vsphere_object class VmFacts(): @@ -167,7 +167,7 @@ def hw_general_facts(self): def hw_folder_facts(self): try: - hw_folder = get_folder_path_of_vm(self.vm) + hw_folder = get_folder_path_of_vsphere_object(self.vm) except Exception: hw_folder = None @@ -313,7 +313,7 @@ def host_facts(self): for host in self.cluster.host: hosts.append({ 'name': host.name, - 'folder': get_folder_path_of_vm(host), + 'folder': get_folder_path_of_vsphere_object(host), }) return {"hosts": hosts} diff --git a/plugins/module_utils/_vmware_folder_paths.py b/plugins/module_utils/_vmware_folder_paths.py index 46393a59..e965514b 100644 --- a/plugins/module_utils/_vmware_folder_paths.py +++ b/plugins/module_utils/_vmware_folder_paths.py @@ -72,17 +72,16 @@ def format_folder_path_as_datastore_fq_path(folder_path, datacenter_name): return __prepend_datacenter_and_folder_type(folder_path, datacenter_name, folder_type='datastore') -def get_folder_path_of_vm(vm): +def get_folder_path_of_vsphere_object(vsphere_obj): """ - Find the path of virtual machine. + Find the path of an object in vsphere. Args: - content: VMware content object - vm_name: virtual machine managed object + vsphere_obj: VMware content object - Returns: Folder of virtual machine if exists, else None + Returns: Folder of object if exists, else None """ - _folder = vm.parent + _folder = vsphere_obj.parent folder_path = [_folder.name] while getattr(_folder, 'parent', None) is not None: _folder = _folder.parent From c9053d0bd25d4003eb8131b6185076176812a654 Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Tue, 19 Nov 2024 09:51:14 -0500 Subject: [PATCH 5/6] sanity --- plugins/module_utils/_vmware_facts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/_vmware_facts.py b/plugins/module_utils/_vmware_facts.py index 76cdc688..d4c72b8a 100644 --- a/plugins/module_utils/_vmware_facts.py +++ b/plugins/module_utils/_vmware_facts.py @@ -360,7 +360,7 @@ def dpm_facts(self): try: output_facts["dpm_host_power_action_rate"] = ClusterFacts.reverse_drs_or_dpm_rate(dpm_config.hostPowerActionRate) except (TypeError, AttributeError): - output_facts["dpm_host_power_action_rate"] = ClusterFacts.DPM_DEFAULT + output_facts["dpm_host_power_action_rate"] = ClusterFacts.DPM_DEFAULT_RATE return output_facts @@ -383,7 +383,7 @@ def drs_facts(self): try: output_facts["drs_vmotion_rate"] = ClusterFacts.reverse_drs_or_dpm_rate(drs_config.vmotionRate) except (TypeError, AttributeError): - output_facts["drs_vmotion_rate"] = ClusterFacts.DRS_DEFAULT + output_facts["drs_vmotion_rate"] = ClusterFacts.DRS_DEFAULT_RATE return output_facts From bd3d8c4b19637ec9e18f387137fd8d1976c7522f Mon Sep 17 00:00:00 2001 From: Mike Morency Date: Tue, 19 Nov 2024 09:56:58 -0500 Subject: [PATCH 6/6] sanity --- plugins/module_utils/_vmware_facts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/module_utils/_vmware_facts.py b/plugins/module_utils/_vmware_facts.py index d4c72b8a..8424dcfc 100644 --- a/plugins/module_utils/_vmware_facts.py +++ b/plugins/module_utils/_vmware_facts.py @@ -284,6 +284,7 @@ def hw_network_device_facts(self): class ClusterFacts(): DPM_DEFAULT_RATE = 3 DRS_DEFAULT_RATE = 3 + def __init__(self, cluster): self.cluster = cluster