diff --git a/lib/ansible/modules/cloud/vmware/vmware_vm_host_drs_rule.py b/lib/ansible/modules/cloud/vmware/vmware_vm_host_drs_rule.py new file mode 100644 index 00000000000..8f8337a6adf --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_vm_host_drs_rule.py @@ -0,0 +1,460 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Karsten Kaj Jakobsen +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r''' +--- +author: + - "Karsten Kaj Jakobsen (@karstenjakobsen)" +description: + - "This module can be used to create VM-Host rules in a given cluster." +extends_documentation_fragment: vmware.documentation +module: vmware_vm_host_drs_rule +notes: + - "Tested on vSphere 6.5 and 6.7" +options: + affinity_rule: + default: true + description: + - "If set to C(True), the DRS rule will be an Affinity rule." + - "If set to C(False), the DRS rule will be an Anti-Affinity rule." + - "Effective only if C(state) is set to C(present)." + type: bool + datacenter: + aliases: + - datacenter_name + description: + - "Datacenter to search for given cluster. If not set, we use first cluster we encounter with C(cluster_name)." + required: false + cluster_name: + description: + - "Cluster to create VM-Host rule." + required: true + drs_rule_name: + description: + - "Name of rule to create or remove." + required: true + enabled: + default: false + description: + - "If set to C(True), the DRS rule will be enabled." + - "Effective only if C(state) is set to C(present)." + type: bool + host_group_name: + description: + - "Name of Host group to use with rule." + - "Effective only if C(state) is set to C(present)." + required: true + mandatory: + default: false + description: + - "If set to C(True), the DRS rule will be mandatory." + - "Effective only if C(state) is set to C(present)." + type: bool + state: + choices: + - present + - absent + default: present + description: + - "If set to C(present) and the rule doesn't exists then the rule will be created." + - "If set to C(absent) and the rule exists then the rule will be deleted." + required: true + vm_group_name: + description: + - "Name of VM group to use with rule." + - "Effective only if C(state) is set to C(present)." + required: true +requirements: + - "python >= 2.6" + - PyVmomi +short_description: "Creates vm/host group in a given cluster" +version_added: "2.8" + +''' + +EXAMPLES = r''' +--- +- name: "Create mandatory DRS Affinity rule for VM/Host" + vmware_vm_host_drs_rule: + hostname: "{{ vcenter_hostname }}" + password: "{{ vcenter_password }}" + username: "{{ vcenter_username }}" + validate_certs: False + cluster_name: DC0_C0 + drs_rule_name: drs_rule_host_aff_0001 + host_group_name: DC0_C0_HOST_GR1 + vm_group_name: DC0_C0_VM_GR1 + mandatory: True + enabled: True + affinity_rule: True +''' + +RETURN = r''' + +''' + +try: + from pyVmomi import vim +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.vmware import (PyVmomi, vmware_argument_spec, wait_for_task, find_cluster_by_name, + find_datacenter_by_name) + + +class VmwareVmHostRuleDrs(PyVmomi): + """ + Class to manage VM HOST DRS Rules + """ + + def __init__(self, module): + """ + Doctring: Init + """ + + super(VmwareVmHostRuleDrs, self).__init__(module) + + self.__datacenter_name = module.params.get('datacenter', None) + self.__datacenter_obj = None + self.__cluster_name = module.params['cluster_name'] + self.__cluster_obj = None + self.__vm_group_name = module.params.get('vm_group_name', None) + self.__host_group_name = module.params.get('host_group_name', None) + self.__rule_name = module.params['drs_rule_name'] + self.__enabled = module.params['enabled'] + self.__mandatory = module.params['mandatory'] + self.__affinity_rule = module.params['affinity_rule'] + self.__state = module.params['state'] + self.__msg = 'Nothing to see here...' + self.__result = dict() + self.__changed = False + + if self.__datacenter_name is not None: + + self.__datacenter_obj = find_datacenter_by_name(self.content, self.__datacenter_name) + + if self.__datacenter_obj is None and module.check_mode is False: + raise Exception("Datacenter '%s' not found" % self.__datacenter_name) + + self.__cluster_obj = find_cluster_by_name(content=self.content, + cluster_name=self.__cluster_name, + datacenter=self.__datacenter_obj) + + # Throw error if cluster does not exist + if self.__cluster_obj is None and module.check_mode is False: + raise Exception("Cluster '%s' not found" % self.__cluster_name) + + # Dont populate lists if we are deleting group + if self.__state == 'present': + # Get list of vm groups only if state is present + self.__vm_group_obj = self.__get_group_by_name(group_name=self.__vm_group_name) + self.__host_group_obj = self.__get_group_by_name(group_name=self.__host_group_name, host_group=True) + + def get_msg(self): + """ + Returns message for Ansible result + Args: none + + Returns: string + """ + return self.__msg + + def get_result(self): + """ + Returns result for Ansible + Args: none + + Returns: dict + """ + return self.__result + + def get_changed(self): + """ + Returns if anything changed + Args: none + + Returns: boolean + """ + return self.__changed + + def __get_group_by_name(self, group_name, cluster_obj=None, host_group=False): + """ + Return group + Args: + group_name: Group name + cluster_obj: Cluster managed object + + Returns: cluster_obj.configurationEx.group + + """ + if cluster_obj is None: + cluster_obj = self.__cluster_obj + + for group in cluster_obj.configurationEx.group: + + if not host_group and isinstance(group, vim.cluster.VmGroup): + if group.name == group_name: + return group + elif host_group and isinstance(group, vim.cluster.HostGroup): + if group.name == group_name: + return group + + raise Exception("Failed to find the group %s in given cluster %s" % (group_name, cluster_obj.name)) + + def __get_rule_key_by_name(self, cluster_obj=None, rule_name=None): + """ + Function to get a specific VM-Host DRS rule key by name + Args: + rule_name: Name of rule + cluster_obj: Cluster managed object + + Returns: Rule Object if found or None + + """ + + if cluster_obj is None: + cluster_obj = self.__cluster_obj + + if rule_name is None: + rule_name = self.__rule_name + + if rule_name: + rules_list = [rule for rule in cluster_obj.configuration.rule if rule.name == rule_name] + if rules_list: + return rules_list[0] + + # No rule found + return None + + def __normalize_vm_host_rule_spec(self, rule_obj, cluster_obj=None): + """ + Return human readable rule spec + Args: + rule_obj: Rule managed object + cluster_obj: Cluster managed object + + Returns: Dictionary with VM-Host DRS Rule info + + """ + if cluster_obj is None: + cluster_obj = self.__cluster_obj + + if not all([rule_obj, cluster_obj]): + return {} + + return dict(rule_key=rule_obj.key, + rule_enabled=rule_obj.enabled, + rule_name=rule_obj.name, + rule_mandatory=rule_obj.mandatory, + rule_uuid=rule_obj.ruleUuid, + rule_vm_group_name=rule_obj.vmGroupName, + rule_affine_host_group_name=rule_obj.affineHostGroupName, + rule_anti_affine_host_group_name=rule_obj.antiAffineHostGroupName, + rule_vms=self.__get_all_from_group(group_name=rule_obj.vmGroupName, + cluster_obj=cluster_obj), + rule_affine_hosts=self.__get_all_from_group(group_name=rule_obj.affineHostGroupName, + cluster_obj=cluster_obj, + host_group=True), + rule_anti_affine_hosts=self.__get_all_from_group(group_name=rule_obj.antiAffineHostGroupName, + cluster_obj=cluster_obj, + host_group=True), + rule_type="vm_host_rule" + ) + + def __get_all_from_group(self, group_name=None, cluster_obj=None, host_group=False): + """ + Return all VM / Host names using given group name + Args: + group_name: Rule name + cluster_obj: Cluster managed object + host_group: True if we want only host name from group + + Returns: List of VM-Host names belonging to given group object + + """ + obj_name_list = [] + + if not all([group_name, cluster_obj]): + return obj_name_list + + for group in cluster_obj.configurationEx.group: + if group.name == group_name: + if not host_group and isinstance(group, vim.cluster.VmGroup): + obj_name_list = [vm.name for vm in group.vm] + break + elif host_group and isinstance(group, vim.cluster.HostGroup): + obj_name_list = [host.name for host in group.host] + break + + return obj_name_list + + def __check_rule_has_changed(self, rule_obj, cluster_obj=None): + """ + Function to check if the rule being edited has changed + """ + + if cluster_obj is None: + cluster_obj = self.__cluster_obj + + existing_rule = self.__normalize_vm_host_rule_spec(rule_obj=rule_obj, cluster_obj=cluster_obj) + + # Check if anything has changed + if ((existing_rule['rule_enabled'] == self.__enabled) and + (existing_rule['rule_mandatory'] == self.__mandatory) and + (existing_rule['rule_vm_group_name'] == self.__vm_group_name) and + (existing_rule['rule_affine_host_group_name'] == self.__host_group_name or + existing_rule['rule_anti_affine_host_group_name'] == self.__host_group_name)): + + return False + else: + return True + + def create(self): + """ + Function to create a host VM-Host DRS rule if rule does not exist + """ + rule_obj = self.__get_rule_key_by_name(rule_name=self.__rule_name) + + # Check if rule exists + if rule_obj: + + operation = 'edit' + rule_changed = self.__check_rule_has_changed(rule_obj) + + else: + operation = 'add' + + # Check if anything has changed when editing + if operation == 'add' or (operation == 'edit' and rule_changed is True): + + rule = vim.cluster.VmHostRuleInfo() + + # Check if already rule exists + if rule_obj: + # This need to be set in order to edit a existing rule + rule.key = rule_obj.key + + rule.enabled = self.__enabled + rule.mandatory = self.__mandatory + rule.name = self.__rule_name + + if self.__affinity_rule: + rule.affineHostGroupName = self.__host_group_name + else: + rule.antiAffineHostGroupName = self.__host_group_name + + rule.vmGroupName = self.__vm_group_name + + rule_spec = vim.cluster.RuleSpec(info=rule, operation=operation) + config_spec = vim.cluster.ConfigSpecEx(rulesSpec=[rule_spec]) + + if not self.module.check_mode: + + task = self.__cluster_obj.ReconfigureEx(config_spec, modify=True) + wait_for_task(task) + + self.__changed = True + + rule_obj = self.__get_rule_key_by_name(rule_name=self.__rule_name) + self.__result = self.__normalize_vm_host_rule_spec(rule_obj) + + if operation == 'edit': + self.__msg = "Updated DRS rule `%s` successfully" % (self.__rule_name) + else: + self.__msg = "Created DRS rule `%s` successfully" % (self.__rule_name) + + # Delete + def delete(self, rule_name=None): + """ + Function to delete VM-Host DRS rule using name + """ + changed = False + + if rule_name is None: + rule_name = self.__rule_name + + rule_obj = self.__get_rule_key_by_name(rule_name=rule_name) + + if rule_obj is not None: + + rule_key = int(rule_obj.key) + rule_spec = vim.cluster.RuleSpec(removeKey=rule_key, operation='remove') + config_spec = vim.cluster.ConfigSpecEx(rulesSpec=[rule_spec]) + + if not self.module.check_mode: + + task = self.__cluster_obj.ReconfigureEx(config_spec, modify=True) + wait_for_task(task) + + self.__changed = True + + if self.__changed: + self.__msg = "Deleted DRS rule `%s` successfully" % (self.__rule_name) + else: + self.__msg = "DRS Rule `%s` does not exists or already deleted" % (self.__rule_name) + + +def main(): + + argument_spec = vmware_argument_spec() + + argument_spec.update(dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + vm_group_name=dict(type='str', required=True), + host_group_name=dict(type='str', required=True), + cluster_name=dict(type='str', required=True), + datacenter=dict(type='str', required=False, aliases=['datacenter_name']), + drs_rule_name=dict(type='str', required=True), + enabled=dict(type='bool', default=False), + mandatory=dict(type='bool', default=False), + affinity_rule=dict(type='bool', default=True)) + ) + + required_if = [ + ['state', 'present', ['vm_group_name'], ['host_group_name']], + ] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + + try: + # Create instance of VmwareDrsGroupManager + vm_host_drs = VmwareVmHostRuleDrs(module=module) + + if module.params['state'] == 'present': + vm_host_drs.create() + elif module.params['state'] == 'absent': + vm_host_drs.delete() + + # Set results + results = dict(msg=vm_host_drs.get_msg(), + failed=False, + changed=vm_host_drs.get_changed(), + result=vm_host_drs.get_result()) + + except Exception as error: + results = dict(failed=True, msg="Error: `%s`" % error) + + if results['failed']: + module.fail_json(**results) + else: + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/vmware_vm_host_drs_rule/aliases b/test/integration/targets/vmware_vm_host_drs_rule/aliases new file mode 100644 index 00000000000..845e8a6dad5 --- /dev/null +++ b/test/integration/targets/vmware_vm_host_drs_rule/aliases @@ -0,0 +1,2 @@ +cloud/vcenter +unsupported diff --git a/test/integration/targets/vmware_vm_host_drs_rule/tasks/main.yml b/test/integration/targets/vmware_vm_host_drs_rule/tasks/main.yml new file mode 100644 index 00000000000..54e5a1fcebe --- /dev/null +++ b/test/integration/targets/vmware_vm_host_drs_rule/tasks/main.yml @@ -0,0 +1,79 @@ +# Test code for the vmware_vm_vm_drs_rule module. +# Copyright: (c) 2017, Karsten Kaj Jakobsen +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# TODO: vcsim does not support manage DRS rule + +- name: Store the vcenter container ip + set_fact: + vcsim: "{{ lookup('env', 'vcenter_host') }}" +- debug: var=vcsim + +- name: Wait for flask server + wait_for: + host: "{{ vcsim }}" + port: 5000 + state: started + +- name: Kill vcsim + uri: + url: http://{{ vcsim }}:5000/killall + +- name: Start vcsim + uri: + url: http://{{ vcsim }}:5000/spawn?datacenter=2&cluster=1&folder=0&ds=2&pool=2 + register: vcsim_instance + +- name: Wait for vcsim server + wait_for: + host: "{{ vcsim }}" + port: 443 + state: started + +- debug: var=vcsim_instance + +# TODO: create host/vm group when vcsim supports this + +- name: Create mandatory DRS Affinity rule for VM/Host + vmware_vm_host_drs_rule: + validate_certs: False + hostname: "{{ vcsim }}" + username: "{{ vcsim_instance['json']['username'] }}" + password: "{{ vcsim_instance['json']['password'] }}" + drs_rule_name: drs_rule_host_aff_0001 + vm_group_name: DC0_C0_VM_GR1 + host_group_name: DC0_C0_HOST_GR1 + cluster_name: DC0_C0 + enabled: True + affinity_rule: True + mandatory: True + register: drs_rule_host_0001_results + +- debug: var=drs_rule_host_0001_results + +- assert: + that: + - "{{ drs_rule_host_0001_results.changed }}" + +# TODO: create host/vm group when vcsim supports this + +- name: Create non-mandatory DRS Anti-Affinity rule for VM/Host + vmware_vm_host_drs_rule: + validate_certs: False + hostname: "{{ vcsim }}" + username: "{{ vcsim_instance['json']['username'] }}" + password: "{{ vcsim_instance['json']['password'] }}" + drs_rule_name: drs_rule_host_aff_0001 + vm_group_name: DC0_C0_VM_GR1 + host_group_name: DC0_C0_HOST_GR2 + cluster_name: DC0_C0 + enabled: True + affinity_rule: False + mandatory: False + register: drs_rule_host_0002_results + +- debug: var=drs_rule_host_0002_results + +- assert: + that: + - "{{ drs_rule_host_0001_results.changed }}" \ No newline at end of file