From b0cc240665b1d86c163c92b580fe3874c011ca17 Mon Sep 17 00:00:00 2001 From: Bojan Vitnik Date: Mon, 19 Nov 2018 07:50:20 +0100 Subject: [PATCH] New module: xenserver_guest - manages XenServer VMs (#41041) * XenServer related modules - initial commit - New module_util: xenserver. Contains common module arguments, functions and classes useful for future XenServer related modules. - New module_docs_fragment: xenserver. Describes common module arguments. - New module: xenserver_guest. Supports VM deployment, reconfiguration, removal, detection of changes, state management, fact gathering and Ansible check mode. Module is fully documented. - Updated: developing_module_utilities.rst. - Module params, workflow and some functions are based on or taken from vmware_guest module. * Implemented support for configuring custom VM params in xenserver_guest module * Compatibility fixes and documentation update - xenserver module_util: implemented support for XenAPI.py version older than 7.2. - xenserver module_util: PEP8 fixes. - xenserver module_util: Added missing imports. - xenserver module_util: Copyright notice fixes. - xenserver_guest module: updated module documentation with notes regarding module requirements and compatibility. - xenserver_guest module: bumped version_added to 2.7. - xenserver_guest module: minor fixes. * VM power state management refactoring, subargument specs in xenserver_guest module, other fixes - VM power state management code moved from xenserver_guest module to xenserver module_util (set_vm_power_state function). - Code for waiting for VM IP address moved from xenserver_guest module to xenserver module_util (wait_for_vm_ip_address function). - xenserver module_util: implemented get_object_ref function to clean up a lot of repeated code in xenserver_guest module. - xenserver module_util: added additional aliases 'host' and 'pool' for 'hostname' common module argument. They are more in line with what XenServer users are familiar with. - xenserver module_util: minor fixes. - xenserver_guest module: removed VM state management other than 'present', 'absent' and 'poweredon'. Other states are to be managed by separate module. - xenserver_guest module: added subargument specs and cleaned up custom code for subargument validation. - xenserver_guest module: reorganized code for disk and network reconfiguation to minimize code duplication. - xenserver_guest module: renamed 'cdrom.iso' module argument to 'cdrom.iso_name', avoids cryptic error message when parameter is missing. - xenserver_guest module: documentation update. - xenserver_guest module: changes in error messages. - xenserver_guest module: minor fixes. * Implemented guest OS network parameter configuration and other - xenserver module_util: moved code for validating MAC addresses from xenserver_guest module and implemented a range of functions for validating IP addresses and related entities and converting prefixes to netmasks and vice versa. - xenserver module_util: updated fact gathering code to support guest OS network parameters. - xenserver module_util: added docstrings. - xenserver module_util: minor changes. - xenserver_guest module: implemented support for guest OS network parameter configuration. - xenserver_guest module: changed CD-ROM handling code. - xenserver_guest module: changed so that user friendly version of changes list is always returned in module result. - xenserver_guest module: error message changes. - xenserver_guest module: added docstrings. - xenserver_guest module: documentation update. - xenserver_guest module: minor changes and fixes. * Various fixes and code cleanup - xenserver module_util: implemented get_xenserver_version function. - xenserver module_util: moved customization agent detection code to gather_vm_params function. customization_agent variable is now part of vm_params. An exception in customization agent detection code that prevented deployment of new VMs is also fixed. - xenserver module_util: added support for alternative VM state names with dash and underscore in set_power_state function. - xenserver_guest module: removed customization agent detection code as it is now implemented in xenserver module_util. - xenserver_guest module: fixed a bug in xenserver_data update code that occured when "networks.mac" was not specified in module params and other fixes. - xenserver_guest module: some code cleanup. - xenserver_guest module: bumped version_added to 2.8. --- .../dev_guide/developing_module_utilities.rst | 1 + lib/ansible/module_utils/xenserver.py | 867 ++++++++ .../modules/cloud/xenserver/__init__.py | 0 .../cloud/xenserver/xenserver_guest.py | 1901 +++++++++++++++++ .../utils/module_docs_fragments/xenserver.py | 35 + 5 files changed, 2804 insertions(+) create mode 100644 lib/ansible/module_utils/xenserver.py create mode 100644 lib/ansible/modules/cloud/xenserver/__init__.py create mode 100644 lib/ansible/modules/cloud/xenserver/xenserver_guest.py create mode 100644 lib/ansible/utils/module_docs_fragments/xenserver.py diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst index 379370670e8..28f0c9f311f 100644 --- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst +++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst @@ -78,3 +78,4 @@ The following is a list of ``module_utils`` files and a general description. The - utm_utils.py - Contains base class for creating new Sophos UTM Modules and helper functions for handling the rest interface of Sophos UTM - vca.py - Contains utilities for modules that work with VMware vCloud Air - vmware.py - Contains utilities for modules that work with VMware vSphere VMs +- xenserver.py - Contains utilities for modules that work with XenServer. diff --git a/lib/ansible/module_utils/xenserver.py b/lib/ansible/module_utils/xenserver.py new file mode 100644 index 00000000000..1bcfa95e01c --- /dev/null +++ b/lib/ansible/module_utils/xenserver.py @@ -0,0 +1,867 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import atexit +import time +import re + +HAS_XENAPI = False +try: + import XenAPI + HAS_XENAPI = True +except ImportError: + pass + +from ansible.module_utils._text import to_text +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.six import integer_types, iteritems, string_types +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.ansible_release import __version__ as ANSIBLE_VERSION + + +def xenserver_common_argument_spec(): + return dict( + hostname=dict(type='str', + aliases=['host', 'pool'], + required=False, + default='localhost', + fallback=(env_fallback, ['XENSERVER_HOST']), + ), + username=dict(type='str', + aliases=['user', 'admin'], + required=False, + default='root', + fallback=(env_fallback, ['XENSERVER_USER'])), + password=dict(type='str', + aliases=['pass', 'pwd'], + required=False, + no_log=True, + fallback=(env_fallback, ['XENSERVER_PASSWORD'])), + validate_certs=dict(type='bool', + required=False, + default=True, + fallback=(env_fallback, ['XENSERVER_VALIDATE_CERTS'])), + ) + + +def xapi_to_module_vm_power_state(power_state): + """Maps XAPI VM power states to module VM power states.""" + module_power_state_map = { + "running": "poweredon", + "halted": "poweredoff", + "suspended": "suspended", + "paused": "paused" + } + + return module_power_state_map.get(power_state) + + +def module_to_xapi_vm_power_state(power_state): + """Maps module VM power states to XAPI VM power states.""" + vm_power_state_map = { + "poweredon": "running", + "poweredoff": "halted", + "restarted": "running", + "suspended": "suspended", + "shutdownguest": "halted", + "rebootguest": "running", + } + + return vm_power_state_map.get(power_state) + + +def is_valid_mac_addr(mac_addr): + """Validates given string as MAC address. + + Args: + mac_addr (str): string to validate as MAC address. + + Returns: + bool: True if string is valid MAC address, else False. + """ + mac_addr_regex = re.compile('[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$') + return bool(mac_addr_regex.match(mac_addr.lower())) + + +def is_valid_ip_addr(ip_addr): + """Validates given string as IPv4 address for given string. + + Args: + ip_addr (str): string to validate as IPv4 address. + + Returns: + bool: True if string is valid IPv4 address, else False. + """ + ip_addr_split = ip_addr.split('.') + + if len(ip_addr_split) != 4: + return False + + for ip_addr_octet in ip_addr_split: + if not ip_addr_octet.isdigit(): + return False + + ip_addr_octet_int = int(ip_addr_octet) + + if ip_addr_octet_int < 0 or ip_addr_octet_int > 255: + return False + + return True + + +def is_valid_ip_netmask(ip_netmask): + """Validates given string as IPv4 netmask. + + Args: + ip_netmask (str): string to validate as IPv4 netmask. + + Returns: + bool: True if string is valid IPv4 netmask, else False. + """ + ip_netmask_split = ip_netmask.split('.') + + if len(ip_netmask_split) != 4: + return False + + valid_octet_values = ['0', '128', '192', '224', '240', '248', '252', '254', '255'] + + for ip_netmask_octet in ip_netmask_split: + if ip_netmask_octet not in valid_octet_values: + return False + + if ip_netmask_split[0] != '255' and (ip_netmask_split[1] != '0' or ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'): + return False + elif ip_netmask_split[1] != '255' and (ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'): + return False + elif ip_netmask_split[2] != '255' and ip_netmask_split[3] != '0': + return False + + return True + + +def is_valid_ip_prefix(ip_prefix): + """Validates given string as IPv4 prefix. + + Args: + ip_prefix (str): string to validate as IPv4 prefix. + + Returns: + bool: True if string is valid IPv4 prefix, else False. + """ + if not ip_prefix.isdigit(): + return False + + ip_prefix_int = int(ip_prefix) + + if ip_prefix_int < 0 or ip_prefix_int > 32: + return False + + return True + + +def ip_prefix_to_netmask(ip_prefix, skip_check=False): + """Converts IPv4 prefix to netmask. + + Args: + ip_prefix (str): IPv4 prefix to convert. + skip_check (bool): Skip validation of IPv4 prefix + (default: False). Use if you are sure IPv4 prefix is valid. + + Returns: + str: IPv4 netmask equivalent to given IPv4 prefix if + IPv4 prefix is valid, else an empty string. + """ + if skip_check: + ip_prefix_valid = True + else: + ip_prefix_valid = is_valid_ip_prefix(ip_prefix) + + if ip_prefix_valid: + return '.'.join([str((0xffffffff << (32 - int(ip_prefix)) >> i) & 0xff) for i in [24, 16, 8, 0]]) + else: + return "" + + +def ip_netmask_to_prefix(ip_netmask, skip_check=False): + """Converts IPv4 netmask to prefix. + + Args: + ip_netmask (str): IPv4 netmask to convert. + skip_check (bool): Skip validation of IPv4 netmask + (default: False). Use if you are sure IPv4 netmask is valid. + + Returns: + str: IPv4 prefix equivalent to given IPv4 netmask if + IPv4 netmask is valid, else an empty string. + """ + if skip_check: + ip_netmask_valid = True + else: + ip_netmask_valid = is_valid_ip_netmask(ip_netmask) + + if ip_netmask_valid: + return str(sum([bin(int(i)).count("1") for i in ip_netmask.split(".")])) + else: + return "" + + +def is_valid_ip6_addr(ip6_addr): + """Validates given string as IPv6 address. + + Args: + ip6_addr (str): string to validate as IPv6 address. + + Returns: + bool: True if string is valid IPv6 address, else False. + """ + ip6_addr = ip6_addr.lower() + ip6_addr_split = ip6_addr.split(':') + + if ip6_addr_split[0] == "": + ip6_addr_split.pop(0) + + if ip6_addr_split[-1] == "": + ip6_addr_split.pop(-1) + + if len(ip6_addr_split) > 8: + return False + + if ip6_addr_split.count("") > 1: + return False + elif ip6_addr_split.count("") == 1: + ip6_addr_split.remove("") + else: + if len(ip6_addr_split) != 8: + return False + + ip6_addr_hextet_regex = re.compile('^[0-9a-f]{1,4}$') + + for ip6_addr_hextet in ip6_addr_split: + if not bool(ip6_addr_hextet_regex.match(ip6_addr_hextet)): + return False + + return True + + +def is_valid_ip6_prefix(ip6_prefix): + """Validates given string as IPv6 prefix. + + Args: + ip6_prefix (str): string to validate as IPv6 prefix. + + Returns: + bool: True if string is valid IPv6 prefix, else False. + """ + if not ip6_prefix.isdigit(): + return False + + ip6_prefix_int = int(ip6_prefix) + + if ip6_prefix_int < 0 or ip6_prefix_int > 128: + return False + + return True + + +def get_object_ref(module, name, uuid=None, obj_type="VM", fail=True, msg_prefix=""): + """Finds and returns a reference to arbitary XAPI object. + + An object is searched by using either name (name_label) or UUID + with UUID taken precendence over name. + + Args: + module: Reference to Ansible module object. + name (str): Name (name_label) of an object to search for. + uuid (str): UUID of an object to search for. + obj_type (str): Any valid XAPI object type. See XAPI docs. + fail (bool): Should function fail with error message if object + is not found or exit silently (default: True). The function + always fails if multiple objects with same name are found. + msg_prefix (str): A string error messages should be prefixed + with (default: ""). + + Returns: + XAPI reference to found object or None if object is not found + and fail=False. + """ + xapi_session = XAPI.connect(module) + + if obj_type in ["template", "snapshot"]: + real_obj_type = "VM" + elif obj_type == "home server": + real_obj_type = "host" + elif obj_type == "ISO image": + real_obj_type = "VDI" + else: + real_obj_type = obj_type + + obj_ref = None + + # UUID has precendence over name. + if uuid: + try: + # Find object by UUID. If no object is found using given UUID, + # an exception will be generated. + obj_ref = xapi_session.xenapi_request("%s.get_by_uuid" % real_obj_type, (uuid,)) + except XenAPI.Failure as f: + if fail: + module.fail_json(msg="%s%s with UUID '%s' not found!" % (msg_prefix, obj_type, uuid)) + elif name: + try: + # Find object by name (name_label). + obj_ref_list = xapi_session.xenapi_request("%s.get_by_name_label" % real_obj_type, (name,)) + except XenAPI.Failure as f: + module.fail_json(msg="XAPI ERROR: %s" % f.details) + + # If obj_ref_list is empty. + if not obj_ref_list: + if fail: + module.fail_json(msg="%s%s with name '%s' not found!" % (msg_prefix, obj_type, name)) + # If obj_ref_list contains multiple object references. + elif len(obj_ref_list) > 1: + module.fail_json(msg="%smultiple %ss with name '%s' found! Please use UUID." % (msg_prefix, obj_type, name)) + # The obj_ref_list contains only one object reference. + else: + obj_ref = obj_ref_list[0] + else: + module.fail_json(msg="%sno valid name or UUID supplied for %s!" % (msg_prefix, obj_type)) + + return obj_ref + + +def gather_vm_params(module, vm_ref): + """Gathers all VM parameters available in XAPI database. + + Args: + module: Reference to Ansible module object. + vm_ref (str): XAPI reference to VM. + + Returns: + dict: VM parameters. + """ + # We silently return empty vm_params if bad vm_ref was supplied. + if not vm_ref or vm_ref == "OpaqueRef:NULL": + return {} + + xapi_session = XAPI.connect(module) + + try: + vm_params = xapi_session.xenapi.VM.get_record(vm_ref) + + # We need some params like affinity, VBDs, VIFs, VDIs etc. dereferenced. + + # Affinity. + if vm_params['affinity'] != "OpaqueRef:NULL": + vm_affinity = xapi_session.xenapi.host.get_record(vm_params['affinity']) + vm_params['affinity'] = vm_affinity + else: + vm_params['affinity'] = {} + + # VBDs. + vm_vbd_params_list = [xapi_session.xenapi.VBD.get_record(vm_vbd_ref) for vm_vbd_ref in vm_params['VBDs']] + + # List of VBDs is usually sorted by userdevice but we sort just + # in case. We need this list sorted by userdevice so that we can + # make positional pairing with module.params['disks']. + vm_vbd_params_list = sorted(vm_vbd_params_list, key=lambda vm_vbd_params: int(vm_vbd_params['userdevice'])) + vm_params['VBDs'] = vm_vbd_params_list + + # VDIs. + for vm_vbd_params in vm_params['VBDs']: + if vm_vbd_params['VDI'] != "OpaqueRef:NULL": + vm_vdi_params = xapi_session.xenapi.VDI.get_record(vm_vbd_params['VDI']) + else: + vm_vdi_params = {} + + vm_vbd_params['VDI'] = vm_vdi_params + + # VIFs. + vm_vif_params_list = [xapi_session.xenapi.VIF.get_record(vm_vif_ref) for vm_vif_ref in vm_params['VIFs']] + + # List of VIFs is usually sorted by device but we sort just + # in case. We need this list sorted by device so that we can + # make positional pairing with module.params['networks']. + vm_vif_params_list = sorted(vm_vif_params_list, key=lambda vm_vif_params: int(vm_vif_params['device'])) + vm_params['VIFs'] = vm_vif_params_list + + # Networks. + for vm_vif_params in vm_params['VIFs']: + if vm_vif_params['network'] != "OpaqueRef:NULL": + vm_network_params = xapi_session.xenapi.network.get_record(vm_vif_params['network']) + else: + vm_network_params = {} + + vm_vif_params['network'] = vm_network_params + + # Guest metrics. + if vm_params['guest_metrics'] != "OpaqueRef:NULL": + vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_params['guest_metrics']) + vm_params['guest_metrics'] = vm_guest_metrics + else: + vm_params['guest_metrics'] = {} + + # Detect customization agent. + xenserver_version = get_xenserver_version(module) + + if (int(xenserver_version[0]) >= 7 and int(xenserver_version[1]) >= 0 and vm_params.get('guest_metrics') and + "feature-static-ip-setting" in vm_params['guest_metrics']['other']): + vm_params['customization_agent'] = "native" + else: + vm_params['customization_agent'] = "custom" + + except XenAPI.Failure as f: + module.fail_json(msg="XAPI ERROR: %s" % f.details) + + return vm_params + + +def gather_vm_facts(module, vm_params): + """Gathers VM facts. + + Args: + module: Reference to Ansible module object. + vm_params (dict): A dictionary with VM parameters as returned + by gather_vm_params() function. + + Returns: + dict: VM facts. + """ + # We silently return empty vm_facts if no vm_params are available. + if not vm_params: + return {} + + xapi_session = XAPI.connect(module) + + # Gather facts. + vm_facts = { + "state": xapi_to_module_vm_power_state(vm_params['power_state'].lower()), + "name": vm_params['name_label'], + "name_desc": vm_params['name_description'], + "uuid": vm_params['uuid'], + "is_template": vm_params['is_a_template'], + "folder": vm_params['other_config'].get('folder', ''), + "hardware": { + "num_cpus": int(vm_params['VCPUs_max']), + "num_cpu_cores_per_socket": int(vm_params['platform'].get('cores-per-socket', '1')), + "memory_mb": int(int(vm_params['memory_dynamic_max']) / 1048576), + }, + "disks": [], + "cdrom": {}, + "networks": [], + "home_server": vm_params['affinity'].get('name_label', ''), + "domid": vm_params['domid'], + "platform": vm_params['platform'], + "other_config": vm_params['other_config'], + "xenstore_data": vm_params['xenstore_data'], + "customization_agent": vm_params['customization_agent'], + } + + for vm_vbd_params in vm_params['VBDs']: + if vm_vbd_params['type'] == "Disk": + vm_disk_sr_params = xapi_session.xenapi.SR.get_record(vm_vbd_params['VDI']['SR']) + + vm_disk_params = { + "size": int(vm_vbd_params['VDI']['virtual_size']), + "name": vm_vbd_params['VDI']['name_label'], + "name_desc": vm_vbd_params['VDI']['name_description'], + "sr": vm_disk_sr_params['name_label'], + "sr_uuid": vm_disk_sr_params['uuid'], + "os_device": vm_vbd_params['device'], + "vbd_userdevice": vm_vbd_params['userdevice'], + } + + vm_facts['disks'].append(vm_disk_params) + elif vm_vbd_params['type'] == "CD": + if vm_vbd_params['empty']: + vm_facts['cdrom'].update(type="none") + else: + vm_facts['cdrom'].update(type="iso") + vm_facts['cdrom'].update(iso=vm_vbd_params['VDI']['name_label']) + + for vm_vif_params in vm_params['VIFs']: + vm_guest_metrics_networks = vm_params['guest_metrics'].get('networks', {}) + + vm_network_params = { + "name": vm_vif_params['network']['name_label'], + "mac": vm_vif_params['MAC'], + "vif_device": vm_vif_params['device'], + "mtu": vm_vif_params['MTU'], + "ip": vm_guest_metrics_networks.get("%s/ip" % vm_vif_params['device'], ''), + "prefix": "", + "netmask": "", + "gateway": "", + "ip6": [vm_guest_metrics_networks[ipv6] for ipv6 in vm_guest_metrics_networks.keys() if ipv6.startswith("%s/ipv6/" % vm_vif_params['device'])], + "prefix6": "", + "gateway6": "", + } + + if vm_params['customization_agent'] == "native": + if vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]: + vm_network_params['prefix'] = vm_vif_params['ipv4_addresses'][0].split('/')[1] + vm_network_params['netmask'] = ip_prefix_to_netmask(vm_network_params['prefix']) + + vm_network_params['gateway'] = vm_vif_params['ipv4_gateway'] + + if vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]: + vm_network_params['prefix6'] = vm_vif_params['ipv6_addresses'][0].split('/')[1] + + vm_network_params['gateway6'] = vm_vif_params['ipv6_gateway'] + + elif vm_params['customization_agent'] == "custom": + vm_xenstore_data = vm_params['xenstore_data'] + + for f in ['prefix', 'netmask', 'gateway', 'prefix6', 'gateway6']: + vm_network_params[f] = vm_xenstore_data.get("vm-data/networks/%s/%s" % (vm_vif_params['device'], f), "") + + vm_facts['networks'].append(vm_network_params) + + return vm_facts + + +def set_vm_power_state(module, vm_ref, power_state, timeout=300): + """Controls VM power state. + + Args: + module: Reference to Ansible module object. + vm_ref (str): XAPI reference to VM. + power_state (str): Power state to put VM into. Accepted values: + + - poweredon + - poweredoff + - restarted + - suspended + - shutdownguest + - rebootguest + + timeout (int): timeout in seconds (default: 300). + + Returns: + tuple (bool, str): Bool element is True if VM power state has + changed by calling this function, else False. Str element carries + a value of resulting power state as defined by XAPI - 'running', + 'halted' or 'suspended'. + """ + # Fail if we don't have a valid VM reference. + if not vm_ref or vm_ref == "OpaqueRef:NULL": + module.fail_json(msg="Cannot set VM power state. Invalid VM reference supplied!") + + xapi_session = XAPI.connect(module) + + power_state = power_state.replace('_', '').replace('-', '').lower() + vm_power_state_resulting = module_to_xapi_vm_power_state(power_state) + + state_changed = False + + try: + # Get current state of the VM. + vm_power_state_current = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower()) + + if vm_power_state_current != power_state: + if power_state == "poweredon": + if not module.check_mode: + # VM can be in either halted, suspended, paused or running state. + # For VM to be in running state, start has to be called on halted, + # resume on suspended and unpause on paused VM. + if vm_power_state_current == "poweredoff": + xapi_session.xenapi.VM.start(vm_ref, False, False) + elif vm_power_state_current == "suspended": + xapi_session.xenapi.VM.resume(vm_ref, False, False) + elif vm_power_state_current == "paused": + xapi_session.xenapi.VM.unpause(vm_ref) + elif power_state == "poweredoff": + if not module.check_mode: + # hard_shutdown will halt VM regardless of current state. + xapi_session.xenapi.VM.hard_shutdown(vm_ref) + elif power_state == "restarted": + # hard_restart will restart VM only if VM is in paused or running state. + if vm_power_state_current in ["paused", "poweredon"]: + if not module.check_mode: + xapi_session.xenapi.VM.hard_reboot(vm_ref) + else: + module.fail_json(msg="Cannot restart VM in state '%s'!" % vm_power_state_current) + elif power_state == "suspended": + # running state is required for suspend. + if vm_power_state_current == "poweredon": + if not module.check_mode: + xapi_session.xenapi.VM.suspend(vm_ref) + else: + module.fail_json(msg="Cannot suspend VM in state '%s'!" % vm_power_state_current) + elif power_state == "shutdownguest": + # running state is required for guest shutdown. + if vm_power_state_current == "poweredon": + if not module.check_mode: + if timeout == 0: + xapi_session.xenapi.VM.clean_shutdown(vm_ref) + else: + task_ref = xapi_session.xenapi.Async.VM.clean_shutdown(vm_ref) + task_result = wait_for_task(module, task_ref, timeout) + + if task_result: + module.fail_json(msg="Guest shutdown task failed: '%s'!" % task_result) + else: + module.fail_json(msg="Cannot shutdown guest when VM is in state '%s'." % vm_power_state_current) + elif power_state == "rebootguest": + # running state is required for guest reboot. + if vm_power_state_current == "poweredon": + if not module.check_mode: + if timeout == 0: + xapi_session.xenapi.VM.clean_reboot(vm_ref) + else: + task_ref = xapi_session.xenapi.Async.VM.clean_reboot(vm_ref) + task_result = wait_for_task(module, task_ref, timeout) + + if task_result: + module.fail_json(msg="Guest reboot task failed: '%s'!" % task_result) + else: + module.fail_json(msg="Cannot reboot guest when VM is in state '%s'." % vm_power_state_current) + else: + module.fail_json(msg="Requested VM power state '%s' is unsupported!" % power_state) + + state_changed = True + except XenAPI.Failure as f: + module.fail_json(msg="XAPI ERROR: %s" % f.details) + + return (state_changed, vm_power_state_resulting) + + +def wait_for_task(module, task_ref, timeout=300): + """Waits for async XAPI task to finish. + + Args: + module: Reference to Ansible module object. + task_ref (str): XAPI reference to task. + timeout (int): timeout in seconds (default: 300). + + Returns: + str: failure message on failure, else an empty string. + """ + # Fail if we don't have a valid task reference. + if not task_ref or task_ref == "OpaqueRef:NULL": + module.fail_json(msg="Cannot wait for task. Invalid task reference supplied!") + + xapi_session = XAPI.connect(module) + + interval = 2 + + result = "" + + # If we have to wait indefinitely, make timeout larger than 0 so we can + # enter while loop. + if timeout == 0: + timeout = 1 + + try: + while timeout > 0: + task_status = xapi_session.xenapi.task.get_status(task_ref).lower() + + if task_status == "pending": + # Task is still running. + time.sleep(interval) + + # We decrease timeout only if we don't wait indefinitely. + if timeout != 0: + timeout -= interval + + continue + elif task_status == "success": + # Task is done. + break + else: + # Task failed. + result = task_status + break + else: + # We timed out. + result = "timeout" + + xapi_session.xenapi.task.destroy(task_ref) + except XenAPI.Failure as f: + module.fail_json(msg="XAPI ERROR: %s" % f.details) + + return result + + +def wait_for_vm_ip_address(module, vm_ref, timeout=300): + """Waits for VM to acquire an IP address. + + Args: + module: Reference to Ansible module object. + vm_ref (str): XAPI reference to VM. + timeout (int): timeout in seconds (default: 300). + + Returns: + dict: VM guest metrics as retrieved by + VM_guest_metrics.get_record() XAPI method with info + on IP address acquired. + """ + # Fail if we don't have a valid VM reference. + if not vm_ref or vm_ref == "OpaqueRef:NULL": + module.fail_json(msg="Cannot wait for VM IP address. Invalid VM reference supplied!") + + xapi_session = XAPI.connect(module) + + vm_guest_metrics = {} + + try: + # We translate VM power state string so that error message can be + # consistent with module VM power states. + vm_power_state = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower()) + + if vm_power_state != 'poweredon': + module.fail_json(msg="Cannot wait for VM IP address when VM is in state '%s'." % vm_power_state) + + interval = 2 + + # If we have to wait indefinitely, make time_left larger than 0 so we can + # enter while loop. + if timeout == 0: + time_left = 1 + else: + time_left = timeout + + while time_left > 0: + vm_guest_metrics_ref = xapi_session.xenapi.VM.get_guest_metrics(vm_ref) + + if vm_guest_metrics_ref != "OpaqueRef:NULL": + vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_guest_metrics_ref) + vm_ips = vm_guest_metrics['networks'] + + if "0/ip" in vm_ips: + break + + time.sleep(interval) + + # We decrease time_left only if we don't wait indefinitely. + if timeout != 0: + time_left -= interval + else: + # We timed out. + module.fail_json(msg="Timed out waiting for VM IP address!") + + except XenAPI.Failure as f: + module.fail_json(msg="XAPI ERROR: %s" % f.details) + + return vm_guest_metrics + + +def get_xenserver_version(module): + """Returns XenServer version. + + Args: + module: Reference to Ansible module object. + + Returns: + list: Element [0] is major version. Element [1] i minor version. + """ + xapi_session = XAPI.connect(module) + + host_ref = xapi_session.xenapi.session.get_this_host(xapi_session._session) + return xapi_session.xenapi.host.get_software_version(host_ref)['product_version_text_short'].split('.') + + +class XAPI(object): + """Class for XAPI session management.""" + _xapi_session = None + + @classmethod + def connect(cls, module, disconnect_atexit=True): + """Establishes XAPI connection and returns session reference. + + If no existing session is available, establishes a new one + and returns it, else returns existing one. + + Args: + module: Reference to Ansible module object. + disconnect_atexit (bool): Controls if method should + register atexit handler to disconnect from XenServer + on module exit (default: True). + + Returns: + XAPI session reference. + """ + if cls._xapi_session is not None: + return cls._xapi_session + + hostname = module.params['hostname'] + username = module.params['username'] + password = module.params['password'] + ignore_ssl = not module.params['validate_certs'] + + if hostname == 'localhost': + cls._xapi_session = XenAPI.xapi_local() + username = '' + password = '' + else: + # If scheme is not specified we default to http:// because https:// + # is problematic in most setups. + if not hostname.startswith("http://") and not hostname.startswith("https://"): + hostname = "http://%s" % hostname + + try: + # ignore_ssl is supported in XenAPI.py 7.2 onward but there + # is no way to tell which version we are using. TypeError will + # be raised if ignore_ssl is not supported. Additionally, + # ignore_ssl requires Python 2.7.9 or newer. + cls._xapi_session = XenAPI.Session(hostname, ignore_ssl=ignore_ssl) + except TypeError: + # Try without ignore_ssl. + cls._xapi_session = XenAPI.Session(hostname) + + if not password: + password = '' + + try: + cls._xapi_session.login_with_password(username, password, ANSIBLE_VERSION, 'Ansible') + except XenAPI.Failure as f: + module.fail_json(msg="Unable to log on to XenServer at %s as %s: %s" % (hostname, username, f.details)) + + # Disabling atexit should be used in special cases only. + if disconnect_atexit: + atexit.register(cls._xapi_session.logout) + return cls._xapi_session + + +class XenServerObject(object): + """Base class for all XenServer objects. + + This class contains active XAPI session reference and common + attributes with useful info about XenServer host/pool. + + Attributes: + module: Reference to Ansible module object. + xapi_session: Reference to XAPI session. + pool_ref (str): XAPI reference to a pool currently connected to. + default_sr_ref (str): XAPI reference to a pool default + Storage Repository. + host_ref (str): XAPI rerefence to a host currently connected to. + xenserver_version (list of str): Contains XenServer major and + minor version. + """ + + def __init__(self, module): + """Inits XenServerObject using common module parameters. + + Args: + module: Reference to Ansible module object. + """ + if not HAS_XENAPI: + module.fail_json(changed=False, msg="XenAPI.py required for this module! Please download XenServer SDK and copy XenAPI.py to your site-packages.") + + if module: + self.module = module + else: + module.fail_json(msg="XenServerObject: Invalid module object passed!") + + self.xapi_session = XAPI.connect(module) + + try: + self.pool_ref = self.xapi_session.xenapi.pool.get_all()[0] + self.default_sr_ref = self.xapi_session.xenapi.pool.get_default_SR(self.pool_ref) + self.xenserver_version = get_xenserver_version(module) + except XenAPI.Failure as f: + self.module.fail_json(msg="XAPI ERROR: %s" % f.details) diff --git a/lib/ansible/modules/cloud/xenserver/__init__.py b/lib/ansible/modules/cloud/xenserver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/cloud/xenserver/xenserver_guest.py b/lib/ansible/modules/cloud/xenserver/xenserver_guest.py new file mode 100644 index 00000000000..88296ee30d4 --- /dev/null +++ b/lib/ansible/modules/cloud/xenserver/xenserver_guest.py @@ -0,0 +1,1901 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, Bojan Vitnik +# 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''' +--- +module: xenserver_guest +short_description: Manages virtual machines running on Citrix XenServer host or pool +description: > + This module can be used to create new virtual machines from templates or other virtual machines, + modify various virtual machine components like network and disk, rename a virtual machine and + remove a virtual machine with associated components. +version_added: '2.8' +author: +- Bojan Vitnik (@bvitnik) +notes: +- Minimal supported version of XenServer is 5.6 +- Module was tested with XenServer 6.5, 7.1 and 7.2 +- 'If no scheme is specified in C(hostname), module defaults to C(http://) because C(https://) is problematic in most setups. Make sure you are + accessing XenServer host in trusted environment or use C(https://) scheme explicitly.' +- 'To use C(https://) scheme for C(hostname) you have to either import host certificate to your OS certificate store or use C(validate_certs: no) + which requires XenAPI.py from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.' +- 'Network configuration inside a guest OS, by using C(networks.type), C(networks.ip), C(networks.gateway) etc. parameters, is supported on + XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for network configuration. The module will try to + detect if such support is available and utilize it, else it will use a custom method of configuration via xenstore. Since XenServer Guest + agent only support None and Static types of network configuration, where None means DHCP configured interface, C(networks.type) and C(networks.type6) + values C(none) and C(dhcp) have same effect. + More info here: https://xenserver.org/blog/entry/set-windows-guest-vm-static-ip-address-in-xenserver.html' +- 'On platforms without official support for network configuration inside a guest OS, network parameters will be written to xenstore + C(vm-data/networks/) key. Parameters can be inspected by using C(xenstore ls) and C(xenstore read) tools on \*nix guests or trough + WMI interface on Windows guests. They can also be found in VM facts C(instance.xenstore_data) key as returned by the module. It is up to the user + to implement a boot time scripts or custom agent that will read the parameters from xenstore and configure network with given parameters. + Take note that for xenstore data to become available inside a guest, a VM restart is needed hence module will require VM restart if any + parameter is changed. This is a limitation of XenAPI and xenstore. Considering these limitations, network configuration trough xenstore is most + useful for bootstraping newly deployed VMs, much less for reconfiguring existing ones. + More info here: https://support.citrix.com/article/CTX226713' +requirements: +- python >= 2.6 +- XenAPI +options: + state: + description: + - Specify the state VM should be in. + - If C(state) is set to C(present) and VM exists, ensure the VM configuration conforms to given parameters. + - If C(state) is set to C(present) and VM does not exist, then VM is deployed with given parameters. + - If C(state) is set to C(absent) and virtual machine exists, then VM is removed with its associated components. + - If C(state) is set to C(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on automatically. + default: present + choices: [ present, absent, poweredon ] + name: + description: + - Name of the VM to work with. + - VMs running on XenServer do not necessarily have unique names. The module will fail if multiple VMs with same name are found. + - In case of multiple VMs with same name, use C(uuid) to uniquely specify VM to manage. + - This parameter is case sensitive. + required: yes + aliases: [ 'name_label' ] + name_desc: + description: + - VM description. + uuid: + description: + - UUID of the VM to manage if known, this is XenServer's unique identifier. + - It is required if name is not unique. + - Please note that a supplied UUID will be ignored on VM creation, as XenServer creates the UUID internally. + template: + description: + - Name of a template, an existing VM (must be shut down) or a snapshot that should be used to create VM. + - Templates/VMs/snapshots on XenServer do not necessarily have unique names. The module will fail if multiple templates with same name are found. + - In case of multiple templates/VMs/snapshots with same name, use C(template_uuid) to uniquely specify source template. + - If VM already exists, this setting will be ignored. + - This parameter is case sensitive. + aliases: [ 'template_src' ] + template_uuid: + description: + - UUID of a template, an existing VM or a snapshot that should be used to create VM. + - It is required if template name is not unique. + is_template: + description: + - Convert VM to template. + default: 'no' + type: bool + folder: + description: + - Destination folder for VM. + - This parameter is case sensitive. + - 'Example:' + - ' folder: /folder1/folder2' + hardware: + description: + - Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters. + - 'Valid parameters are:' + - ' - C(num_cpus) (integer): Number of CPUs.' + - ' - C(num_cpu_cores_per_socket) (integer): Number of Cores Per Socket. C(num_cpus) has to be a multiple of C(num_cpu_cores_per_socket).' + - ' - C(memory_mb) (integer): Amount of memory in MB.' + disks: + description: + - A list of disks to add to VM. + - All parameters are case sensetive. + - Removing or detaching existing disks of VM is not supported. + - 'Required parameters per entry:' + - ' - C(size_[tb,gb,mb,kb,b]) (integer): Disk storage size in specified unit. VM needs to be shut down to reconfigure this parameter.' + - 'Optional parameters per entry:' + - ' - C(name) (string): Disk name. You can also use C(name_label) as an alias.' + - ' - C(name_desc) (string): Disk description.' + - ' - C(sr) (string): Storage Repository to create disk on. If not specified, will use default SR. Cannot be used for moving disk to other SR.' + - ' - C(sr_uuid) (string): UUID of a SR to create disk on. Use if SR name is not unique.' + aliases: [ 'disk' ] + cdrom: + description: + - A CD-ROM configuration for the VM. + - All parameters case sensitive. + - 'Valid parameters are:' + - ' - C(type) (string): The type of CD-ROM, valid options are C(none) or C(iso). With C(none) the CD-ROM device will be present but empty.' + - ' - C(iso_name) (string): The file name of an ISO image from one of the XenServer ISO Libraries (implies C(type: iso)). + Required if C(type) is set to C(iso).' + networks: + description: + - A list of networks (in the order of the NICs). + - All parameters are case sensetive. + - 'Required parameters per entry:' + - ' - C(name) (string): Name of a XenServer network to attach the network interface to. You can also use C(name_label) as an alias.' + - 'Optional parameters per entry (used for VM hardware):' + - ' - C(mac) (string): Customize MAC address of the interface.' + - 'Optional parameters per entry (used for OS customization):' + - ' - C(type) (string): Type of IPv4 assignment, valid options are C(none), C(dhcp) or C(static). Value C(none) means whatever is default for OS. + On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).' + - ' - C(ip) (string): Static IPv4 address (implies C(type: static)). Can include prefix in format / instead of using C(netmask).' + - ' - C(netmask) (string): Static IPv4 netmask required for C(ip) if prefix is not specified.' + - ' - C(gateway) (string): Static IPv4 gateway.' + - ' - C(type6) (string): Type of IPv6 assignment, valid options are C(none), C(dhcp) or C(static). Value C(none) means whatever is default for OS. + On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).' + - ' - C(ip6) (string): Static IPv6 address (implies C(type6: static)) with prefix in format /.' + - ' - C(gateway6) (string): Static IPv6 gateway.' + aliases: [ 'network' ] + home_server: + description: + - Name of a XenServer host that will be a Home Server for the VM. + - This parameter is case sensitive. + custom_params: + description: + - Define a list of custom VM params to set on VM. + - A custom value object takes two fields C(key) and C(value). + wait_for_ip_address: + description: + - Wait until XenServer detects an IP address for the VM. + - This requires XenServer Tools preinstaled on VM to properly work. + default: 'no' + type: bool + state_change_timeout: + description: + - 'By default, module will wait indefinitely for VM to accquire an IP address if C(wait_for_ip_address: yes).' + - If this parameter is set to positive value, the module will instead wait specified number of seconds for the state change. + - In case of timeout, module will generate an error message. + default: 0 + linked_clone: + description: + - Whether to create a Linked Clone from the template, existing VM or snapshot. If no, will create a full copy. + default: 'no' + type: bool + force: + description: + - Ignore warnings and complete the actions. + - This parameter is useful for removing VM in running state or reconfiguring VM params that require VM to be shut down. + default: 'no' + type: bool +extends_documentation_fragment: xenserver.documentation +''' + +EXAMPLES = r''' +- name: Create a VM from a template + xenserver_guest: + hostname: 192.0.2.44 + username: root + password: xenserver + validate_certs: no + folder: /testvms + name: testvm_2 + state: poweredon + template: CentOS 7 + disks: + - size_gb: 10 + sr: my_sr + hardware: + num_cpus: 6 + num_cpu_cores_per_socket: 3 + memory_mb: 512 + cdrom: + type: iso + iso_name: guest-tools.iso + networks: + - name: VM Network + mac: aa:bb:dd:aa:00:14 + wait_for_ip_address: yes + delegate_to: localhost + register: deploy + +- name: Create a VM template + xenserver_guest: + hostname: 192.0.2.88 + username: root + password: xenserver + validate_certs: no + folder: /testvms + name: testvm_6 + is_template: yes + disk: + - size_gb: 10 + sr: my_sr + hardware: + memory_mb: 512 + num_cpus: 1 + delegate_to: localhost + register: deploy + +- name: Rename a VM (requires the VM's UUID) + xenserver_guest: + hostname: 192.168.1.209 + username: root + password: xenserver + uuid: 421e4592-c069-924d-ce20-7e7533fab926 + name: new_name + state: present + delegate_to: localhost + +- name: Remove a VM by UUID + xenserver_guest: + hostname: 192.168.1.209 + username: root + password: xenserver + uuid: 421e4592-c069-924d-ce20-7e7533fab926 + state: absent + delegate_to: localhost + +- name: Modify custom params + xenserver_guest: + hostname: 192.168.1.210 + username: root + password: xenserver + name: testvm_8 + state: present + custom_params: + - key: HVM_boot_params + value: { "order": "ndc" } + delegate_to: localhost + +- name: Customize network parameters + xenserver_guest: + hostname: 192.168.1.209 + username: root + password: xenserver + name: testvm_10 + networks: + - name: VM Network + ip: 192.168.1.100/24 + gateway: 192.168.1.1 + - type: dhcp + delegate_to: localhost +''' + +RETURN = r''' +instance: + description: Metadata about the VM + returned: always + type: dict + sample: { + "cdrom": { + "type": "none" + }, + "customization_agent": "native", + "disks": [ + { + "name": "testvm_11-0", + "name_desc": "", + "os_device": "xvda", + "size": 42949672960, + "sr": "Local storage", + "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", + "vbd_userdevice": "0" + }, + { + "name": "testvm_11-1", + "name_desc": "", + "os_device": "xvdb", + "size": 42949672960, + "sr": "Local storage", + "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", + "vbd_userdevice": "1" + } + ], + "domid": "56", + "folder": "", + "hardware": { + "memory_mb": 8192, + "num_cpu_cores_per_socket": 2, + "num_cpus": 4 + }, + "home_server": "", + "is_template": false, + "name": "testvm_11", + "name_desc": "", + "networks": [ + { + "gateway": "192.168.0.254", + "gateway6": "fc00::fffe", + "ip": "192.168.0.200", + "ip6": [ + "fe80:0000:0000:0000:e9cb:625a:32c5:c291", + "fc00:0000:0000:0000:0000:0000:0000:0001" + ], + "mac": "ba:91:3a:48:20:76", + "mtu": "1500", + "name": "Pool-wide network associated with eth1", + "netmask": "255.255.255.128", + "prefix": "25", + "prefix6": "64", + "vif_device": "0" + } + ], + "other_config": { + "base_template_name": "Windows Server 2016 (64-bit)", + "import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5", + "install-methods": "cdrom", + "instant": "true", + "mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e" + }, + "platform": { + "acpi": "1", + "apic": "true", + "cores-per-socket": "2", + "device_id": "0002", + "hpet": "true", + "nx": "true", + "pae": "true", + "timeoffset": "-25200", + "vga": "std", + "videoram": "8", + "viridian": "true", + "viridian_reference_tsc": "true", + "viridian_time_ref_count": "true" + }, + "state": "poweredon", + "uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda", + "xenstore_data": { + "vm-data": "" + } + } +changes: + description: Detected or made changes to VM + returned: always + type: list + sample: [ + { + "hardware": [ + "num_cpus" + ] + }, + { + "disks_changed": [ + [], + [ + "size" + ] + ] + }, + { + "disks_new": [ + { + "name": "new-disk", + "name_desc": "", + "position": 2, + "size_gb": "4", + "vbd_userdevice": "2" + } + ] + }, + { + "cdrom": [ + "type", + "iso_name" + ] + }, + { + "networks_changed": [ + [ + "mac" + ], + ] + }, + { + "networks_new": [ + { + "name": "Pool-wide network associated with eth2", + "position": 1, + "vif_device": "1" + } + ] + }, + "need_poweredoff" + ] +''' + +import re + +HAS_XENAPI = False +try: + import XenAPI + HAS_XENAPI = True +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text, to_native +from ansible.module_utils import six +from ansible.module_utils.xenserver import (xenserver_common_argument_spec, XAPI, XenServerObject, get_object_ref, + gather_vm_params, gather_vm_facts, set_vm_power_state, wait_for_vm_ip_address, + is_valid_mac_addr, is_valid_ip_addr, is_valid_ip_netmask, is_valid_ip_prefix, + ip_prefix_to_netmask, ip_netmask_to_prefix, + is_valid_ip6_addr, is_valid_ip6_prefix) + + +class XenServerVM(XenServerObject): + """Class for managing XenServer VM. + + Attributes: + vm_ref (str): XAPI reference to VM. + vm_params (dict): A dictionary with VM parameters as returned + by gather_vm_params() function. + """ + + def __init__(self, module): + """Inits XenServerVM using module parameters. + + Args: + module: Reference to Ansible module object. + """ + super(XenServerVM, self).__init__(module) + + self.vm_ref = get_object_ref(self.module, self.module.params['name'], self.module.params['uuid'], obj_type="VM", fail=False, msg_prefix="VM search: ") + self.gather_params() + + def exists(self): + """Returns True if VM exists, else False.""" + return True if self.vm_ref is not None else False + + def gather_params(self): + """Gathers all VM parameters available in XAPI database.""" + self.vm_params = gather_vm_params(self.module, self.vm_ref) + + def gather_facts(self): + """Gathers and returns VM facts.""" + return gather_vm_facts(self.module, self.vm_params) + + def set_power_state(self, power_state): + """Controls VM power state.""" + state_changed, current_state = set_vm_power_state(self.module, self.vm_ref, power_state, self.module.params['state_change_timeout']) + + # If state has changed, update vm_params. + if state_changed: + self.vm_params['power_state'] = current_state.capitalize() + + return state_changed + + def wait_for_ip_address(self): + """Waits for VM to acquire an IP address.""" + self.vm_params['guest_metrics'] = wait_for_vm_ip_address(self.module, self.vm_ref, self.module.params['state_change_timeout']) + + def deploy(self): + """Deploys new VM from template.""" + # Safety check. + if self.exists(): + self.module.fail_json(msg="Called deploy on existing VM!") + + try: + templ_ref = get_object_ref(self.module, self.module.params['template'], self.module.params['template_uuid'], obj_type="template", fail=True, + msg_prefix="VM deploy: ") + + # Is this an existing running VM? + if self.xapi_session.xenapi.VM.get_power_state(templ_ref).lower() != 'halted': + self.module.fail_json(msg="VM deploy: running VM cannot be used as a template!") + + # Find a SR we can use for VM.copy(). We use SR of the first disk + # if specified or default SR if not specified. + disk_params_list = self.module.params['disks'] + + sr_ref = None + + if disk_params_list: + disk_params = disk_params_list[0] + + disk_sr_uuid = disk_params.get('sr_uuid') + disk_sr = disk_params.get('sr') + + if disk_sr_uuid is not None or disk_sr is not None: + sr_ref = get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, + msg_prefix="VM deploy disks[0]: ") + + if not sr_ref: + if self.default_sr_ref != "OpaqueRef:NULL": + sr_ref = self.default_sr_ref + else: + self.module.fail_json(msg="VM deploy disks[0]: no default SR found! You must specify SR explicitely.") + + # Support for Ansible check mode. + if self.module.check_mode: + return + + # VM name could be an empty string which is bad. + if self.module.params['name'] is not None and not self.module.params['name']: + self.module.fail_json(msg="VM deploy: VM name must not be an empty string!") + + # Now we can instantiate VM. We use VM.clone for linked_clone and + # VM.copy for non linked_clone. + if self.module.params['linked_clone']: + self.vm_ref = self.xapi_session.xenapi.VM.clone(templ_ref, self.module.params['name']) + else: + self.vm_ref = self.xapi_session.xenapi.VM.copy(templ_ref, self.module.params['name'], sr_ref) + + # Description is copied over from template so we reset it. + self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, "") + + # If template is one of built-in XenServer templates, we have to + # do some additional steps. + # Note: VM.get_is_default_template() is supported from XenServer 7.2 + # onward so we use an alternative way. + templ_other_config = self.xapi_session.xenapi.VM.get_other_config(templ_ref) + + if "default_template" in templ_other_config and templ_other_config['default_template']: + # other_config of built-in XenServer templates have a key called + # 'disks' with the following content: + # disks: + # This value of other_data is copied to cloned or copied VM and + # it prevents provisioning of VM because sr is not specified and + # XAPI returns an error. To get around this, we remove the + # 'disks' key and add disks to VM later ourselves. + vm_other_config = self.xapi_session.xenapi.VM.get_other_config(self.vm_ref) + + if "disks" in vm_other_config: + del vm_other_config['disks'] + + self.xapi_session.xenapi.VM.set_other_config(self.vm_ref, vm_other_config) + + # At this point we have VM ready for provisioning. + self.xapi_session.xenapi.VM.provision(self.vm_ref) + + # After provisioning we can prepare vm_params for reconfigure(). + self.gather_params() + + # VM is almost ready. We just need to reconfigure it... + self.reconfigure() + + # Power on VM if needed. + if self.module.params['state'] == "poweredon": + self.set_power_state("poweredon") + + except XenAPI.Failure as f: + self.module.fail_json(msg="XAPI ERROR: %s" % f.details) + + def reconfigure(self): + """Reconfigures an existing VM. + + Returns: + list: parameters that were reconfigured. + """ + # Safety check. + if not self.exists(): + self.module.fail_json(msg="Called reconfigure on non existing VM!") + + config_changes = self.get_changes() + + vm_power_state_save = self.vm_params['power_state'].lower() + + if "need_poweredoff" in config_changes and vm_power_state_save != 'halted': + if self.module.params['force']: + self.set_power_state("shutdownguest") + else: + self.module.fail_json(msg="VM reconfigure: VM has to be in powered off state to reconfigure but force was not specified!") + + # Support for Ansible check mode. + if self.module.check_mode: + return config_changes + + try: + for change in config_changes: + if isinstance(change, six.string_types): + if change == "name": + self.xapi_session.xenapi.VM.set_name_label(self.vm_ref, self.module.params['name']) + elif change == "name_desc": + self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, self.module.params['name_desc']) + elif change == "folder": + self.xapi_session.xenapi.VM.remove_from_other_config(self.vm_ref, 'folder') + + if self.module.params['folder']: + self.xapi_session.xenapi.VM.add_to_other_config(self.vm_ref, 'folder', self.module.params['folder']) + elif change == "home_server": + if self.module.params['home_server']: + host_ref = self.xapi_session.xenapi.host.get_by_name_label(self.module.params['home_server'])[0] + else: + host_ref = "OpaqueRef:NULL" + + self.xapi_session.xenapi.VM.set_affinity(self.vm_ref, host_ref) + elif isinstance(change, dict): + if change.get('hardware'): + for hardware_change in change['hardware']: + if hardware_change == "num_cpus": + num_cpus = int(self.module.params['hardware']['num_cpus']) + + if num_cpus < int(self.vm_params['VCPUs_at_startup']): + self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus)) + self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus)) + else: + self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus)) + self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus)) + elif hardware_change == "num_cpu_cores_per_socket": + self.xapi_session.xenapi.VM.remove_from_platform(self.vm_ref, 'cores-per-socket') + num_cpu_cores_per_socket = int(self.module.params['hardware']['num_cpu_cores_per_socket']) + + if num_cpu_cores_per_socket > 1: + self.xapi_session.xenapi.VM.add_to_platform(self.vm_ref, 'cores-per-socket', str(num_cpu_cores_per_socket)) + elif hardware_change == "memory_mb": + memory_b = str(int(self.module.params['hardware']['memory_mb']) * 1048576) + vm_memory_static_min_b = str(min(int(memory_b), int(self.vm_params['memory_static_min']))) + + self.xapi_session.xenapi.VM.set_memory_limits(self.vm_ref, vm_memory_static_min_b, memory_b, memory_b, memory_b) + elif change.get('disks_changed'): + vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] + position = 0 + + for disk_change_list in change['disks_changed']: + for disk_change in disk_change_list: + vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params_list[position]['VDI']['uuid']) + + if disk_change == "name": + self.xapi_session.xenapi.VDI.set_name_label(vdi_ref, self.module.params['disks'][position]['name']) + elif disk_change == "name_desc": + self.xapi_session.xenapi.VDI.set_name_description(vdi_ref, self.module.params['disks'][position]['name_desc']) + elif disk_change == "size": + self.xapi_session.xenapi.VDI.resize(vdi_ref, str(self.get_normalized_disk_size(self.module.params['disks'][position], + "VM reconfigure disks[%s]: " % position))) + + position += 1 + elif change.get('disks_new'): + for position, disk_userdevice in change['disks_new']: + disk_params = self.module.params['disks'][position] + + disk_name = disk_params['name'] if disk_params.get('name') else "%s-%s" % (self.vm_params['name_label'], position) + disk_name_desc = disk_params['name_desc'] if disk_params.get('name_desc') else "" + + if disk_params.get('sr_uuid'): + sr_ref = self.xapi_session.xenapi.SR.get_by_uuid(disk_params['sr_uuid']) + elif disk_params.get('sr'): + sr_ref = self.xapi_session.xenapi.SR.get_by_name_label(disk_params['sr'])[0] + else: + sr_ref = self.default_sr_ref + + disk_size = str(self.get_normalized_disk_size(self.module.params['disks'][position], "VM reconfigure disks[%s]: " % position)) + + new_disk_vdi = { + "name_label": disk_name, + "name_description": disk_name_desc, + "SR": sr_ref, + "virtual_size": disk_size, + "type": "user", + "sharable": False, + "read_only": False, + "other_config": {}, + } + + new_disk_vbd = { + "VM": self.vm_ref, + "VDI": None, + "userdevice": disk_userdevice, + "bootable": False, + "mode": "RW", + "type": "Disk", + "empty": False, + "other_config": {}, + "qos_algorithm_type": "", + "qos_algorithm_params": {}, + } + + new_disk_vbd['VDI'] = self.xapi_session.xenapi.VDI.create(new_disk_vdi) + self.xapi_session.xenapi.VBD.create(new_disk_vbd) + elif change.get('cdrom'): + vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"] + + # If there is no CD present, we have to create one. + if not vm_cdrom_params_list: + # We will try to place cdrom at userdevice position + # 3 (which is default) if it is not already occupied + # else we will place it at first allowed position. + cdrom_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref) + + if "3" in cdrom_userdevices_allowed: + cdrom_userdevice = "3" + else: + cdrom_userdevice = cdrom_userdevices_allowed[0] + + cdrom_vbd = { + "VM": self.vm_ref, + "VDI": "OpaqueRef:NULL", + "userdevice": cdrom_userdevice, + "bootable": False, + "mode": "RO", + "type": "CD", + "empty": True, + "other_config": {}, + "qos_algorithm_type": "", + "qos_algorithm_params": {}, + } + + cdrom_vbd_ref = self.xapi_session.xenapi.VBD.create(cdrom_vbd) + else: + cdrom_vbd_ref = self.xapi_session.xenapi.VBD.get_by_uuid(vm_cdrom_params_list[0]['uuid']) + + cdrom_is_empty = self.xapi_session.xenapi.VBD.get_empty(cdrom_vbd_ref) + + for cdrom_change in change['cdrom']: + if cdrom_change == "type": + cdrom_type = self.module.params['cdrom']['type'] + + if cdrom_type == "none" and not cdrom_is_empty: + self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref) + elif cdrom_type == "host": + # Unimplemented! + pass + + elif cdrom_change == "iso_name": + if not cdrom_is_empty: + self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref) + + cdrom_vdi_ref = self.xapi_session.xenapi.VDI.get_by_name_label(self.module.params['cdrom']['iso_name'])[0] + self.xapi_session.xenapi.VBD.insert(cdrom_vbd_ref, cdrom_vdi_ref) + elif change.get('networks_changed'): + position = 0 + + for network_change_list in change['networks_changed']: + if network_change_list: + vm_vif_params = self.vm_params['VIFs'][position] + network_params = self.module.params['networks'][position] + + vif_ref = self.xapi_session.xenapi.VIF.get_by_uuid(vm_vif_params['uuid']) + network_ref = self.xapi_session.xenapi.network.get_by_uuid(vm_vif_params['network']['uuid']) + + vif_recreated = False + + if "name" in network_change_list or "mac" in network_change_list: + # To change network or MAC, we destroy old + # VIF and then create a new one with changed + # parameters. That's how XenCenter does it. + + # Copy all old parameters to new VIF record. + vif = { + "device": vm_vif_params['device'], + "network": network_ref, + "VM": vm_vif_params['VM'], + "MAC": vm_vif_params['MAC'], + "MTU": vm_vif_params['MTU'], + "other_config": vm_vif_params['other_config'], + "qos_algorithm_type": vm_vif_params['qos_algorithm_type'], + "qos_algorithm_params": vm_vif_params['qos_algorithm_params'], + "locking_mode": vm_vif_params['locking_mode'], + "ipv4_allowed": vm_vif_params['ipv4_allowed'], + "ipv6_allowed": vm_vif_params['ipv6_allowed'], + } + + if "name" in network_change_list: + network_ref_new = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0] + vif['network'] = network_ref_new + vif['MTU'] = self.xapi_session.xenapi.network.get_MTU(network_ref_new) + + if "mac" in network_change_list: + vif['MAC'] = network_params['mac'].lower() + + if self.vm_params['power_state'].lower() == "running": + self.xapi_session.xenapi.VIF.unplug(vif_ref) + + self.xapi_session.xenapi.VIF.destroy(vif_ref) + vif_ref_new = self.xapi_session.xenapi.VIF.create(vif) + + if self.vm_params['power_state'].lower() == "running": + self.xapi_session.xenapi.VIF.plug(vif_ref_new) + + vif_ref = vif_ref_new + vif_recreated = True + + if self.vm_params['customization_agent'] == "native": + vif_reconfigure_needed = False + + if "type" in network_change_list: + network_type = network_params['type'].capitalize() + vif_reconfigure_needed = True + else: + network_type = vm_vif_params['ipv4_configuration_mode'] + + if "ip" in network_change_list: + network_ip = network_params['ip'] + vif_reconfigure_needed = True + elif vm_vif_params['ipv4_addresses']: + network_ip = vm_vif_params['ipv4_addresses'][0].split('/')[0] + else: + network_ip = "" + + if "prefix" in network_change_list: + network_prefix = "/%s" % network_params['prefix'] + vif_reconfigure_needed = True + elif vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]: + network_prefix = "/%s" % vm_vif_params['ipv4_addresses'][0].split('/')[1] + else: + network_prefix = "" + + if "gateway" in network_change_list: + network_gateway = network_params['gateway'] + vif_reconfigure_needed = True + else: + network_gateway = vm_vif_params['ipv4_gateway'] + + if vif_recreated or vif_reconfigure_needed: + self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref, network_type, + "%s%s" % (network_ip, network_prefix), network_gateway) + + vif_reconfigure_needed = False + + if "type6" in network_change_list: + network_type6 = network_params['type6'].capitalize() + vif_reconfigure_needed = True + else: + network_type6 = vm_vif_params['ipv6_configuration_mode'] + + if "ip6" in network_change_list: + network_ip6 = network_params['ip6'] + vif_reconfigure_needed = True + elif vm_vif_params['ipv6_addresses']: + network_ip6 = vm_vif_params['ipv6_addresses'][0].split('/')[0] + else: + network_ip6 = "" + + if "prefix6" in network_change_list: + network_prefix6 = "/%s" % network_params['prefix6'] + vif_reconfigure_needed = True + elif vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]: + network_prefix6 = "/%s" % vm_vif_params['ipv6_addresses'][0].split('/')[1] + else: + network_prefix6 = "" + + if "gateway6" in network_change_list: + network_gateway6 = network_params['gateway6'] + vif_reconfigure_needed = True + else: + network_gateway6 = vm_vif_params['ipv6_gateway'] + + if vif_recreated or vif_reconfigure_needed: + self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref, network_type6, + "%s%s" % (network_ip6, network_prefix6), network_gateway6) + + elif self.vm_params['customization_agent'] == "custom": + vif_device = vm_vif_params['device'] + + # A user could have manually changed network + # or mac e.g. trough XenCenter and then also + # make those changes in playbook manually. + # In that case, module will not detect any + # changes and info in xenstore_data will + # become stale. For that reason we always + # update name and mac in xenstore_data. + + # Since we handle name and mac differently, + # we have to remove them from + # network_change_list. + network_change_list_tmp = [net_chg for net_chg in network_change_list if net_chg not in ['name', 'mac']] + + for network_change in network_change_list_tmp + ['name', 'mac']: + self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref, + "vm-data/networks/%s/%s" % (vif_device, network_change)) + + if network_params.get('name'): + network_name = network_params['name'] + else: + network_name = vm_vif_params['network']['name_label'] + + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/%s" % (vif_device, 'name'), network_name) + + if network_params.get('mac'): + network_mac = network_params['mac'].lower() + else: + network_mac = vm_vif_params['MAC'].lower() + + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/%s" % (vif_device, 'mac'), network_mac) + + for network_change in network_change_list_tmp: + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/%s" % (vif_device, network_change), + network_params[network_change]) + + position += 1 + elif change.get('networks_new'): + for position, vif_device in change['networks_new']: + network_params = self.module.params['networks'][position] + + network_ref = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0] + + network_name = network_params['name'] + network_mac = network_params['mac'] if network_params.get('mac') else "" + network_type = network_params.get('type') + network_ip = network_params['ip'] if network_params.get('ip') else "" + network_prefix = network_params['prefix'] if network_params.get('prefix') else "" + network_netmask = network_params['netmask'] if network_params.get('netmask') else "" + network_gateway = network_params['gateway'] if network_params.get('gateway') else "" + network_type6 = network_params.get('type6') + network_ip6 = network_params['ip6'] if network_params.get('ip6') else "" + network_prefix6 = network_params['prefix6'] if network_params.get('prefix6') else "" + network_gateway6 = network_params['gateway6'] if network_params.get('gateway6') else "" + + vif = { + "device": vif_device, + "network": network_ref, + "VM": self.vm_ref, + "MAC": network_mac, + "MTU": self.xapi_session.xenapi.network.get_MTU(network_ref), + "other_config": {}, + "qos_algorithm_type": "", + "qos_algorithm_params": {}, + } + + vif_ref_new = self.xapi_session.xenapi.VIF.create(vif) + + if self.vm_params['power_state'].lower() == "running": + self.xapi_session.xenapi.VIF.plug(vif_ref_new) + + if self.vm_params['customization_agent'] == "native": + if network_type and network_type == "static": + self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref_new, "Static", + "%s/%s" % (network_ip, network_prefix), network_gateway) + + if network_type6 and network_type6 == "static": + self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref_new, "Static", + "%s/%s" % (network_ip6, network_prefix6), network_gateway6) + elif self.vm_params['customization_agent'] == "custom": + # We first have to remove any existing data + # from xenstore_data because there could be + # some old leftover data from some interface + # that once occupied same device location as + # our new interface. + for network_param in ['name', 'mac', 'type', 'ip', 'prefix', 'netmask', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']: + self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref, "vm-data/networks/%s/%s" % (vif_device, network_param)) + + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/name" % vif_device, network_name) + + # We get MAC from VIF itself instead of + # networks.mac because it could be + # autogenerated. + vm_vif_mac = self.xapi_session.xenapi.VIF.get_MAC(vif_ref_new) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/mac" % vif_device, vm_vif_mac) + + if network_type: + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type" % vif_device, network_type) + + if network_type == "static": + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/ip" % vif_device, network_ip) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/prefix" % vif_device, network_prefix) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/netmask" % vif_device, network_netmask) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/gateway" % vif_device, network_gateway) + + if network_type6: + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type6" % vif_device, network_type6) + + if network_type6 == "static": + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/ip6" % vif_device, network_ip6) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/prefix6" % vif_device, network_prefix6) + self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, + "vm-data/networks/%s/gateway6" % vif_device, network_gateway6) + + elif change.get('custom_params'): + for position in change['custom_params']: + custom_param_key = self.module.params['custom_params'][position]['key'] + custom_param_value = self.module.params['custom_params'][position]['value'] + self.xapi_session.xenapi_request("VM.set_%s" % custom_param_key, (self.vm_ref, custom_param_value)) + + if self.module.params['is_template']: + self.xapi_session.xenapi.VM.set_is_a_template(self.vm_ref, True) + elif "need_poweredoff" in config_changes and self.module.params['force'] and vm_power_state_save != 'halted': + self.set_power_state("poweredon") + + # Gather new params after reconfiguration. + self.gather_params() + + except XenAPI.Failure as f: + self.module.fail_json(msg="XAPI ERROR: %s" % f.details) + + return config_changes + + def destroy(self): + """Removes an existing VM with associated disks""" + # Safety check. + if not self.exists(): + self.module.fail_json(msg="Called destroy on non existing VM!") + + if self.vm_params['power_state'].lower() != 'halted': + if self.module.params['force']: + self.set_power_state("poweredoff") + else: + self.module.fail_json(msg="VM destroy: VM has to be in powered off state to destroy but force was not specified!") + + # Support for Ansible check mode. + if self.module.check_mode: + return + + try: + # Destroy VM! + self.xapi_session.xenapi.VM.destroy(self.vm_ref) + + vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] + + # Destroy all VDIs associated with VM! + for vm_disk_params in vm_disk_params_list: + vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params['VDI']['uuid']) + + self.xapi_session.xenapi.VDI.destroy(vdi_ref) + + except XenAPI.Failure as f: + self.module.fail_json(msg="XAPI ERROR: %s" % f.details) + + def get_changes(self): + """Finds VM parameters that differ from specified ones. + + This method builds a dictionary with hierarchy of VM parameters + that differ from those specified in module parameters. + + Returns: + list: VM parameters that differ from those specified in + module parameters. + """ + # Safety check. + if not self.exists(): + self.module.fail_json(msg="Called get_changes on non existing VM!") + + need_poweredoff = False + + if self.module.params['is_template']: + need_poweredoff = True + + try: + # This VM could be a template or a snapshot. In that case we fail + # because we can't reconfigure them or it would just be too + # dangerous. + if self.vm_params['is_a_template'] and not self.vm_params['is_a_snapshot']: + self.module.fail_json(msg="VM check: targeted VM is a template! Template reconfiguration is not supported.") + + if self.vm_params['is_a_snapshot']: + self.module.fail_json(msg="VM check: targeted VM is a snapshot! Snapshot reconfiguration is not supported.") + + # Let's build a list of parameters that changed. + config_changes = [] + + # Name could only differ if we found an existing VM by uuid. + if self.module.params['name'] is not None and self.module.params['name'] != self.vm_params['name_label']: + if self.module.params['name']: + config_changes.append('name') + else: + self.module.fail_json(msg="VM check name: VM name cannot be an empty string!") + + if self.module.params['name_desc'] is not None and self.module.params['name_desc'] != self.vm_params['name_description']: + config_changes.append('name_desc') + + # Folder parameter is found in other_config. + vm_other_config = self.vm_params['other_config'] + vm_folder = vm_other_config.get('folder', '') + + if self.module.params['folder'] is not None and self.module.params['folder'] != vm_folder: + config_changes.append('folder') + + if self.module.params['home_server'] is not None: + if (self.module.params['home_server'] and + (not self.vm_params['affinity'] or self.module.params['home_server'] != self.vm_params['affinity']['name_label'])): + + # Check existance only. Ignore return value. + get_object_ref(self.module, self.module.params['home_server'], uuid=None, obj_type="home server", fail=True, + msg_prefix="VM check home_server: ") + + config_changes.append('home_server') + elif not self.module.params['home_server'] and self.vm_params['affinity']: + config_changes.append('home_server') + + config_changes_hardware = [] + + if self.module.params['hardware']: + num_cpus = self.module.params['hardware'].get('num_cpus') + + if num_cpus is not None: + # Kept for compatibility with older Ansible versions that + # do not support subargument specs. + try: + num_cpus = int(num_cpus) + except ValueError as e: + self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be an integer value!") + + if num_cpus < 1: + self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be greater than zero!") + + # We can use VCPUs_at_startup or VCPUs_max parameter. I'd + # say the former is the way to go but this needs + # confirmation and testing. + if num_cpus != int(self.vm_params['VCPUs_at_startup']): + config_changes_hardware.append('num_cpus') + # For now, we don't support hotpluging so VM has to be in + # poweredoff state to reconfigure. + need_poweredoff = True + + num_cpu_cores_per_socket = self.module.params['hardware'].get('num_cpu_cores_per_socket') + + if num_cpu_cores_per_socket is not None: + # Kept for compatibility with older Ansible versions that + # do not support subargument specs. + try: + num_cpu_cores_per_socket = int(num_cpu_cores_per_socket) + except ValueError as e: + self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be an integer value.") + + if num_cpu_cores_per_socket < 1: + self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be greater than zero!") + + if num_cpus and num_cpus % num_cpu_cores_per_socket != 0: + self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be a multiple of hardware.num_cpu_cores_per_socket.") + + vm_platform = self.vm_params['platform'] + vm_cores_per_socket = int(vm_platform.get('cores-per-socket', 1)) + + if num_cpu_cores_per_socket != vm_cores_per_socket: + config_changes_hardware.append('num_cpu_cores_per_socket') + # For now, we don't support hotpluging so VM has to be + # in poweredoff state to reconfigure. + need_poweredoff = True + + memory_mb = self.module.params['hardware'].get('memory_mb') + + if memory_mb is not None: + # Kept for compatibility with older Ansible versions that + # do not support subargument specs. + try: + memory_mb = int(memory_mb) + except ValueError as e: + self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be an integer value.") + + if memory_mb < 1: + self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be greater than zero!") + + # There are multiple memory parameters: + # - memory_dynamic_max + # - memory_dynamic_min + # - memory_static_max + # - memory_static_min + # - memory_target + # + # memory_target seems like a good candidate but it returns 0 for + # halted VMs so we can't use it. + # + # I decided to use memory_dynamic_max and memory_static_max + # and use whichever is larger. This strategy needs validation + # and testing. + # + # XenServer stores memory size in bytes so we need to divide + # it by 1024*1024 = 1048576. + if memory_mb != int(max(int(self.vm_params['memory_dynamic_max']), int(self.vm_params['memory_static_max'])) / 1048576): + config_changes_hardware.append('memory_mb') + # For now, we don't support hotpluging so VM has to be in + # poweredoff state to reconfigure. + need_poweredoff = True + + if config_changes_hardware: + config_changes.append({"hardware": config_changes_hardware}) + + config_changes_disks = [] + config_new_disks = [] + + # Find allowed userdevices. + vbd_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref) + + if self.module.params['disks']: + # Get the list of all disk. Filter out any CDs found. + vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] + + # Number of disks defined in module params have to be same or + # higher than a number of existing disks attached to the VM. + # We don't support removal or detachment of disks. + if len(self.module.params['disks']) < len(vm_disk_params_list): + self.module.fail_json(msg="VM check disks: provided disks configuration has less disks than the target VM (%d < %d)!" % + (len(self.module.params['disks']), len(vm_disk_params_list))) + + # Find the highest disk occupied userdevice. + if not vm_disk_params_list: + vm_disk_userdevice_highest = "-1" + else: + vm_disk_userdevice_highest = vm_disk_params_list[-1]['userdevice'] + + for position in range(len(self.module.params['disks'])): + if position < len(vm_disk_params_list): + vm_disk_params = vm_disk_params_list[position] + else: + vm_disk_params = None + + disk_params = self.module.params['disks'][position] + + disk_size = self.get_normalized_disk_size(self.module.params['disks'][position], "VM check disks[%s]: " % position) + + disk_name = disk_params.get('name') + + if disk_name is not None and not disk_name: + self.module.fail_json(msg="VM check disks[%s]: disk name cannot be an empty string!" % position) + + # If this is an existing disk. + if vm_disk_params and vm_disk_params['VDI']: + disk_changes = [] + + if disk_name and disk_name != vm_disk_params['VDI']['name_label']: + disk_changes.append('name') + + disk_name_desc = disk_params.get('name_desc') + + if disk_name_desc is not None and disk_name_desc != vm_disk_params['VDI']['name_description']: + disk_changes.append('name_desc') + + if disk_size: + if disk_size > int(vm_disk_params['VDI']['virtual_size']): + disk_changes.append('size') + need_poweredoff = True + elif disk_size < int(vm_disk_params['VDI']['virtual_size']): + self.module.fail_json(msg="VM check disks[%s]: disk size is smaller than existing (%d bytes < %s bytes). " + "Reducing disk size is not allowed!" % (position, disk_size, vm_disk_params['VDI']['virtual_size'])) + + config_changes_disks.append(disk_changes) + # If this is a new disk. + else: + if not disk_size: + self.module.fail_json(msg="VM check disks[%s]: no valid disk size specification found!" % position) + + disk_sr_uuid = disk_params.get('sr_uuid') + disk_sr = disk_params.get('sr') + + if disk_sr_uuid is not None or disk_sr is not None: + # Check existance only. Ignore return value. + get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, + msg_prefix="VM check disks[%s]: " % position) + elif self.default_sr_ref == 'OpaqueRef:NULL': + self.module.fail_json(msg="VM check disks[%s]: no default SR found! You must specify SR explicitely." % position) + + if not vbd_userdevices_allowed: + self.module.fail_json(msg="VM check disks[%s]: maximum number of devices reached!" % position) + + disk_userdevice = None + + # We need to place a new disk right above the highest + # placed existing disk to maintain relative disk + # positions pairable with disk specifications in + # module params. That place must not be occupied by + # some other device like CD-ROM. + for userdevice in vbd_userdevices_allowed: + if int(userdevice) > int(vm_disk_userdevice_highest): + disk_userdevice = userdevice + vbd_userdevices_allowed.remove(userdevice) + vm_disk_userdevice_highest = userdevice + break + + if disk_userdevice is None: + disk_userdevice = str(int(vm_disk_userdevice_highest) + 1) + self.module.fail_json(msg="VM check disks[%s]: new disk position %s is out of bounds!" % (position, disk_userdevice)) + + # For new disks we only track their position. + config_new_disks.append((position, disk_userdevice)) + + # We should append config_changes_disks to config_changes only + # if there is at least one changed disk, else skip. + for disk_change in config_changes_disks: + if disk_change: + config_changes.append({"disks_changed": config_changes_disks}) + break + + if config_new_disks: + config_changes.append({"disks_new": config_new_disks}) + + config_changes_cdrom = [] + + if self.module.params['cdrom']: + # Get the list of all CD-ROMs. Filter out any regular disks + # found. If we found no existing CD-ROM, we will create it + # later else take the first one found. + vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"] + + # If no existing CD-ROM is found, we will need to add one. + # We need to check if there is any userdevice allowed. + if not vm_cdrom_params_list and not vbd_userdevices_allowed: + self.module.fail_json(msg="VM check cdrom: maximum number of devices reached!") + + cdrom_type = self.module.params['cdrom'].get('type') + cdrom_iso_name = self.module.params['cdrom'].get('iso_name') + + # If cdrom.iso_name is specified but cdrom.type is not, + # then set cdrom.type to 'iso', unless cdrom.iso_name is + # an empty string, in that case set cdrom.type to 'none'. + if not cdrom_type: + if cdrom_iso_name: + cdrom_type = "iso" + elif cdrom_iso_name is not None: + cdrom_type = "none" + + self.module.params['cdrom']['type'] = cdrom_type + + # If type changed. + if cdrom_type and (not vm_cdrom_params_list or cdrom_type != self.get_cdrom_type(vm_cdrom_params_list[0])): + config_changes_cdrom.append('type') + + if cdrom_type == "iso": + # Check if ISO exists. + # Check existance only. Ignore return value. + get_object_ref(self.module, cdrom_iso_name, uuid=None, obj_type="ISO image", fail=True, + msg_prefix="VM check cdrom.iso_name: ") + + # Is ISO image changed? + if (cdrom_iso_name and + (not vm_cdrom_params_list or + not vm_cdrom_params_list[0]['VDI'] or + cdrom_iso_name != vm_cdrom_params_list[0]['VDI']['name_label'])): + config_changes_cdrom.append('iso_name') + + if config_changes_cdrom: + config_changes.append({"cdrom": config_changes_cdrom}) + + config_changes_networks = [] + config_new_networks = [] + + # Find allowed devices. + vif_devices_allowed = self.xapi_session.xenapi.VM.get_allowed_VIF_devices(self.vm_ref) + + if self.module.params['networks']: + # Number of VIFs defined in module params have to be same or + # higher than a number of existing VIFs attached to the VM. + # We don't support removal of VIFs. + if len(self.module.params['networks']) < len(self.vm_params['VIFs']): + self.module.fail_json(msg="VM check networks: provided networks configuration has less interfaces than the target VM (%d < %d)!" % + (len(self.module.params['networks']), len(self.vm_params['VIFs']))) + + # Find the highest occupied device. + if not self.vm_params['VIFs']: + vif_device_highest = "-1" + else: + vif_device_highest = self.vm_params['VIFs'][-1]['device'] + + for position in range(len(self.module.params['networks'])): + if position < len(self.vm_params['VIFs']): + vm_vif_params = self.vm_params['VIFs'][position] + else: + vm_vif_params = None + + network_params = self.module.params['networks'][position] + + network_name = network_params.get('name') + + if network_name is not None and not network_name: + self.module.fail_json(msg="VM check networks[%s]: network name cannot be an empty string!" % position) + + if network_name: + # Check existance only. Ignore return value. + get_object_ref(self.module, network_name, uuid=None, obj_type="network", fail=True, + msg_prefix="VM check networks[%s]: " % position) + + network_mac = network_params.get('mac') + + if network_mac is not None: + network_mac = network_mac.lower() + + if not is_valid_mac_addr(network_mac): + self.module.fail_json(msg="VM check networks[%s]: specified MAC address '%s' is not valid!" % (position, network_mac)) + + # IPv4 reconfiguration. + network_type = network_params.get('type') + network_ip = network_params.get('ip') + network_netmask = network_params.get('netmask') + network_prefix = None + + # If networks.ip is specified and networks.type is not, + # then set networks.type to 'static'. + if not network_type and network_ip: + network_type = "static" + + # XenServer natively supports only 'none' and 'static' + # type with 'none' being the same as 'dhcp'. + if self.vm_params['customization_agent'] == "native" and network_type and network_type == "dhcp": + network_type = "none" + + if network_type and network_type == "static": + if network_ip is not None: + network_ip_split = network_ip.split('/') + network_ip = network_ip_split[0] + + if network_ip and not is_valid_ip_addr(network_ip): + self.module.fail_json(msg="VM check networks[%s]: specified IPv4 address '%s' is not valid!" % (position, network_ip)) + + if len(network_ip_split) > 1: + network_prefix = network_ip_split[1] + + if not is_valid_ip_prefix(network_prefix): + self.module.fail_json(msg="VM check networks[%s]: specified IPv4 prefix '%s' is not valid!" % (position, network_prefix)) + + if network_netmask is not None: + if not is_valid_ip_netmask(network_netmask): + self.module.fail_json(msg="VM check networks[%s]: specified IPv4 netmask '%s' is not valid!" % (position, network_netmask)) + + network_prefix = ip_netmask_to_prefix(network_netmask, skip_check=True) + elif network_prefix is not None: + network_netmask = ip_prefix_to_netmask(network_prefix, skip_check=True) + + # If any parameter is overridden at this point, update it. + if network_type: + network_params['type'] = network_type + + if network_ip: + network_params['ip'] = network_ip + + if network_netmask: + network_params['netmask'] = network_netmask + + if network_prefix: + network_params['prefix'] = network_prefix + + network_gateway = network_params.get('gateway') + + # Gateway can be an empty string (when removing gateway + # configuration) but if it is not, it should be validated. + if network_gateway and not is_valid_ip_addr(network_gateway): + self.module.fail_json(msg="VM check networks[%s]: specified IPv4 gateway '%s' is not valid!" % (position, network_gateway)) + + # IPv6 reconfiguration. + network_type6 = network_params.get('type6') + network_ip6 = network_params.get('ip6') + network_prefix6 = None + + # If networks.ip6 is specified and networks.type6 is not, + # then set networks.type6 to 'static'. + if not network_type6 and network_ip6: + network_type6 = "static" + + # XenServer natively supports only 'none' and 'static' + # type with 'none' being the same as 'dhcp'. + if self.vm_params['customization_agent'] == "native" and network_type6 and network_type6 == "dhcp": + network_type6 = "none" + + if network_type6 and network_type6 == "static": + if network_ip6 is not None: + network_ip6_split = network_ip6.split('/') + network_ip6 = network_ip6_split[0] + + if network_ip6 and not is_valid_ip6_addr(network_ip6): + self.module.fail_json(msg="VM check networks[%s]: specified IPv6 address '%s' is not valid!" % (position, network_ip6)) + + if len(network_ip6_split) > 1: + network_prefix6 = network_ip6_split[1] + + if not is_valid_ip6_prefix(network_prefix6): + self.module.fail_json(msg="VM check networks[%s]: specified IPv6 prefix '%s' is not valid!" % (position, network_prefix6)) + + # If any parameter is overridden at this point, update it. + if network_type6: + network_params['type6'] = network_type6 + + if network_ip6: + network_params['ip6'] = network_ip6 + + if network_prefix6: + network_params['prefix6'] = network_prefix6 + + network_gateway6 = network_params.get('gateway6') + + # Gateway can be an empty string (when removing gateway + # configuration) but if it is not, it should be validated. + if network_gateway6 and not is_valid_ip6_addr(network_gateway6): + self.module.fail_json(msg="VM check networks[%s]: specified IPv6 gateway '%s' is not valid!" % (position, network_gateway6)) + + # If this is an existing VIF. + if vm_vif_params and vm_vif_params['network']: + network_changes = [] + + if network_name and network_name != vm_vif_params['network']['name_label']: + network_changes.append('name') + + if network_mac and network_mac != vm_vif_params['MAC'].lower(): + network_changes.append('mac') + + if self.vm_params['customization_agent'] == "native": + if network_type and network_type != vm_vif_params['ipv4_configuration_mode'].lower(): + network_changes.append('type') + + if network_type and network_type == "static": + if network_ip and (not vm_vif_params['ipv4_addresses'] or + not vm_vif_params['ipv4_addresses'][0] or + network_ip != vm_vif_params['ipv4_addresses'][0].split('/')[0]): + network_changes.append('ip') + + if network_prefix and (not vm_vif_params['ipv4_addresses'] or + not vm_vif_params['ipv4_addresses'][0] or + network_prefix != vm_vif_params['ipv4_addresses'][0].split('/')[1]): + network_changes.append('prefix') + network_changes.append('netmask') + + if network_gateway is not None and network_gateway != vm_vif_params['ipv4_gateway']: + network_changes.append('gateway') + + if network_type6 and network_type6 != vm_vif_params['ipv6_configuration_mode'].lower(): + network_changes.append('type6') + + if network_type6 and network_type6 == "static": + if network_ip6 and (not vm_vif_params['ipv6_addresses'] or + not vm_vif_params['ipv6_addresses'][0] or + network_ip6 != vm_vif_params['ipv6_addresses'][0].split('/')[0]): + network_changes.append('ip6') + + if network_prefix6 and (not vm_vif_params['ipv6_addresses'] or + not vm_vif_params['ipv6_addresses'][0] or + network_prefix6 != vm_vif_params['ipv6_addresses'][0].split('/')[1]): + network_changes.append('prefix6') + + if network_gateway6 is not None and network_gateway6 != vm_vif_params['ipv6_gateway']: + network_changes.append('gateway6') + + elif self.vm_params['customization_agent'] == "custom": + vm_xenstore_data = self.vm_params['xenstore_data'] + + if network_type and network_type != vm_xenstore_data.get('vm-data/networks/%s/type' % vm_vif_params['device'], ""): + network_changes.append('type') + need_poweredoff = True + + if network_type and network_type == "static": + if network_ip and network_ip != vm_xenstore_data.get('vm-data/networks/%s/ip' % vm_vif_params['device'], ""): + network_changes.append('ip') + need_poweredoff = True + + if network_prefix and network_prefix != vm_xenstore_data.get('vm-data/networks/%s/prefix' % vm_vif_params['device'], ""): + network_changes.append('prefix') + network_changes.append('netmask') + need_poweredoff = True + + if network_gateway is not None and network_gateway != vm_xenstore_data.get('vm-data/networks/%s/gateway' % + vm_vif_params['device'], ""): + network_changes.append('gateway') + need_poweredoff = True + + if network_type6 and network_type6 != vm_xenstore_data.get('vm-data/networks/%s/type6' % vm_vif_params['device'], ""): + network_changes.append('type6') + need_poweredoff = True + + if network_type6 and network_type6 == "static": + if network_ip6 and network_ip6 != vm_xenstore_data.get('vm-data/networks/%s/ip6' % vm_vif_params['device'], ""): + network_changes.append('ip6') + need_poweredoff = True + + if network_prefix6 and network_prefix6 != vm_xenstore_data.get('vm-data/networks/%s/prefix6' % vm_vif_params['device'], ""): + network_changes.append('prefix6') + need_poweredoff = True + + if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get('vm-data/networks/%s/gateway6' % + vm_vif_params['device'], ""): + network_changes.append('gateway6') + need_poweredoff = True + + config_changes_networks.append(network_changes) + # If this is a new VIF. + else: + if not network_name: + self.module.fail_json(msg="VM check networks[%s]: network name is required for new network interface!" % position) + + if network_type and network_type == "static" and network_ip and not network_netmask: + self.module.fail_json(msg="VM check networks[%s]: IPv4 netmask or prefix is required for new network interface!" % position) + + if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6: + self.module.fail_json(msg="VM check networks[%s]: IPv6 prefix is required for new network interface!" % position) + + # Restart is needed if we are adding new network + # interface with IP/gateway parameters specified + # and custom agent is used. + if self.vm_params['customization_agent'] == "custom": + for parameter in ['type', 'ip', 'prefix', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']: + if network_params.get(parameter): + need_poweredoff = True + break + + # We need to place a new network interface right above the + # highest placed existing interface to maintain relative + # positions pairable with network interface specifications + # in module params. + vif_device = str(int(vif_device_highest) + 1) + + if vif_device not in vif_devices_allowed: + self.module.fail_json(msg="VM check networks[%s]: new network interface position %s is out of bounds!" % (position, vif_device)) + + vif_device_highest = vif_device + + # For new VIFs we only track their position. + config_new_networks.append((position, vif_device)) + + # We should append config_changes_networks to config_changes only + # if there is at least one changed network, else skip. + for network_change in config_changes_networks: + if network_change: + config_changes.append({"networks_changed": config_changes_networks}) + break + + if config_new_networks: + config_changes.append({"networks_new": config_new_networks}) + + config_changes_custom_params = [] + + if self.module.params['custom_params']: + for position in range(len(self.module.params['custom_params'])): + custom_param = self.module.params['custom_params'][position] + + custom_param_key = custom_param['key'] + custom_param_value = custom_param['value'] + + if custom_param_key not in self.vm_params: + self.module.fail_json(msg="VM check custom_params[%s]: unknown VM param '%s'!" % (position, custom_param_key)) + + if custom_param_value != self.vm_params[custom_param_key]: + # We only need to track custom param position. + config_changes_custom_params.append(position) + + if config_changes_custom_params: + config_changes.append({"custom_params": config_changes_custom_params}) + + if need_poweredoff: + config_changes.append('need_poweredoff') + + return config_changes + + except XenAPI.Failure as f: + self.module.fail_json(msg="XAPI ERROR: %s" % f.details) + + def get_normalized_disk_size(self, disk_params, msg_prefix=""): + """Parses disk size parameters and returns disk size in bytes. + + This method tries to parse disk size module parameters. It fails + with an error message if size cannot be parsed. + + Args: + disk_params (dist): A dictionary with disk parameters. + msg_prefix (str): A string error messages should be prefixed + with (default: ""). + + Returns: + int: disk size in bytes if disk size is successfully parsed or + None if no disk size parameters were found. + """ + # There should be only single size spec but we make a list of all size + # specs just in case. Priority is given to 'size' but if not found, we + # check for 'size_tb', 'size_gb', 'size_mb' etc. and use first one + # found. + disk_size_spec = [x for x in disk_params.keys() if disk_params[x] is not None and (x.startswith('size_') or x == 'size')] + + if disk_size_spec: + try: + # size + if "size" in disk_size_spec: + size_regex = re.compile(r'(\d+(?:\.\d+)?)\s*(.*)') + disk_size_m = size_regex.match(disk_params['size']) + + if disk_size_m: + size = disk_size_m.group(1) + unit = disk_size_m.group(2) + else: + raise ValueError + # size_tb, size_gb, size_mb, size_kb, size_b + else: + size = disk_params[disk_size_spec[0]] + unit = disk_size_spec[0].split('_')[-1] + + if not unit: + unit = "b" + else: + unit = unit.lower() + + if re.match(r'\d+\.\d+', size): + # We found float value in string, let's typecast it. + if unit == "b": + # If we found float but unit is bytes, we get the integer part only. + size = int(float(size)) + else: + size = float(size) + else: + # We found int value in string, let's typecast it. + size = int(size) + + if not size: + raise ValueError + + except (TypeError, ValueError, NameError): + # Common failure + self.module.fail_json(msg="%sfailed to parse disk size. Please review value provided using documentation." % msg_prefix) + + disk_units = dict(tb=4, gb=3, mb=2, kb=1, b=0) + + if unit in disk_units: + return int(size * (1024 ** disk_units[unit])) + else: + self.module.fail_json(msg="%s'%s' is not a supported unit for disk size. Supported units are ['%s']." % + (msg_prefix, unit, "', '".join(sorted(disk_units.keys(), key=lambda key: disk_units[key])))) + else: + return None + + @staticmethod + def get_cdrom_type(vm_cdrom_params): + """Returns VM CD-ROM type.""" + # TODO: implement support for detecting type host. No server to test + # this on at the moment. + if vm_cdrom_params['empty']: + return "none" + else: + return "iso" + + +def main(): + argument_spec = xenserver_common_argument_spec() + argument_spec.update( + state=dict(type='str', default='present', + choices=['present', 'absent', 'poweredon']), + name=dict(type='str', aliases=['name_label']), + name_desc=dict(type='str'), + uuid=dict(type='str'), + template=dict(type='str', aliases=['template_src']), + template_uuid=dict(type='str'), + is_template=dict(type='bool', default=False), + folder=dict(type='str'), + hardware=dict( + type='dict', + options=dict( + num_cpus=dict(type='int'), + num_cpu_cores_per_socket=dict(type='int'), + memory_mb=dict(type='int'), + ), + ), + disks=dict( + type='list', + elements='dict', + options=dict( + size=dict(type='str'), + size_tb=dict(type='str'), + size_gb=dict(type='str'), + size_mb=dict(type='str'), + size_kb=dict(type='str'), + size_b=dict(type='str'), + name=dict(type='str', aliases=['name_label']), + name_desc=dict(type='str'), + sr=dict(type='str'), + sr_uuid=dict(type='str'), + ), + aliases=['disk'], + mutually_exclusive=[ + ['size', 'size_tb', 'size_gb', 'size_mb', 'size_kb', 'size_b'], + ['sr', 'sr_uuid'], + ], + ), + cdrom=dict( + type='dict', + options=dict( + type=dict(type='str', choices=['none', 'iso']), + iso_name=dict(type='str'), + ), + required_if=[ + ['type', 'iso', ['iso_name']], + ], + ), + networks=dict( + type='list', + elements='dict', + options=dict( + name=dict(type='str', aliases=['name_label']), + mac=dict(type='str'), + type=dict(type='str', choices=['none', 'dhcp', 'static']), + ip=dict(type='str'), + netmask=dict(type='str'), + gateway=dict(type='str'), + type6=dict(type='str', choices=['none', 'dhcp', 'static']), + ip6=dict(type='str'), + gateway6=dict(type='str'), + ), + aliases=['network'], + required_if=[ + ['type', 'static', ['ip']], + ['type6', 'static', ['ip6']], + ], + ), + home_server=dict(type='str'), + custom_params=dict( + type='list', + elements='dict', + options=dict( + key=dict(type='str', required=True), + value=dict(type='raw', required=True), + ), + ), + wait_for_ip_address=dict(type='bool', default=False), + state_change_timeout=dict(type='int', default=0), + linked_clone=dict(type='bool', default=False), + force=dict(type='bool', default=False), + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[ + ['name', 'uuid'], + ], + mutually_exclusive=[ + ['template', 'template_uuid'], + ], + ) + + result = {'failed': False, 'changed': False} + + vm = XenServerVM(module) + + # Find existing VM + if vm.exists(): + if module.params['state'] == "absent": + vm.destroy() + result['changed'] = True + elif module.params['state'] == "present": + config_changes = vm.reconfigure() + + if config_changes: + result['changed'] = True + + # Make new disk and network changes more user friendly + # and informative. + for change in config_changes: + if isinstance(change, dict): + if change.get('disks_new'): + disks_new = [] + + for position, userdevice in change['disks_new']: + disk_new_params = {"position": position, "vbd_userdevice": userdevice} + disk_params = module.params['disks'][position] + + for k in disk_params.keys(): + if disk_params[k] is not None: + disk_new_params[k] = disk_params[k] + + disks_new.append(disk_new_params) + + if disks_new: + change['disks_new'] = disks_new + + elif change.get('networks_new'): + networks_new = [] + + for position, device in change['networks_new']: + network_new_params = {"position": position, "vif_device": device} + network_params = module.params['networks'][position] + + for k in network_params.keys(): + if network_params[k] is not None: + network_new_params[k] = network_params[k] + + networks_new.append(network_new_params) + + if networks_new: + change['networks_new'] = networks_new + + result['changes'] = config_changes + + elif module.params['state'] in ["poweredon", "poweredoff", "restarted", "shutdownguest", "rebootguest", "suspended"]: + result['changed'] = vm.set_power_state(module.params['state']) + elif module.params['state'] != "absent": + vm.deploy() + result['changed'] = True + + if module.params['wait_for_ip_address']: + vm.wait_for_ip_address() + + result['instance'] = vm.gather_facts() + + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/xenserver.py b/lib/ansible/utils/module_docs_fragments/xenserver.py new file mode 100644 index 00000000000..af6736f3316 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/xenserver.py @@ -0,0 +1,35 @@ +# Copyright: (c) 2018, Bojan Vitnik +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + # Parameters for XenServer modules + DOCUMENTATION = ''' +options: + hostname: + description: + - The hostname or IP address of the XenServer host or XenServer pool master. + - If the value is not specified in the task, the value of environment variable C(XENSERVER_HOST) will be used instead. + required: False + default: 'localhost' + aliases: ['host', 'pool'] + username: + description: + - The username to use for connecting to XenServer. + - If the value is not specified in the task, the value of environment variable C(XENSERVER_USER) will be used instead. + required: False + default: 'root' + aliases: ['user', 'admin'] + password: + description: + - The password to use for connecting to XenServer. + - If the value is not specified in the task, the value of environment variable C(XENSERVER_PASSWORD) will be used instead. + required: False + aliases: ['pass', 'pwd'] + validate_certs: + description: + - Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted. + - If the value is not specified in the task, the value of environment variable C(XENSERVER_VALIDATE_CERTS) will be used instead. + default: 'yes' + type: bool +'''