#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (c) 2018, Bojan Vitnik # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r''' --- module: xenserver_guest short_description: Manages virtual machines running on Citrix Hypervisor/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. author: - Bojan Vitnik (@bvitnik) notes: - Minimal supported version of XenServer is 5.6. - Module was tested with XenServer 6.5, 7.1, 7.2, 7.6, Citrix Hypervisor 8.0, XCP-ng 7.6 and 8.0. - 'To acquire XenAPI Python library, just run C(pip install XenAPI) on your Ansible Control Node. The library can also be found inside Citrix Hypervisor/XenServer SDK (downloadable from Citrix website). Copy the XenAPI.py file from the SDK to your Python site-packages on your Ansible Control Node to use it. Latest version of the library can also be acquired from GitHub: U(https://raw.githubusercontent.com/xapi-project/xen-api/master/scripts/examples/python/XenAPI/XenAPI.py)' - 'If no scheme is specified in O(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 O(hostname) you have to either import host certificate to your OS certificate store or use O(validate_certs=false) which requires XenAPI library from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.' - 'Network configuration inside a guest OS, by using O(networks[].type), O(networks[].ip), O(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, O(networks[].type) and O(networks[].type6) values V(none) and V(dhcp) have same effect. More info here: U(https://www.citrix.com/community/citrix-developer/citrix-hypervisor-developer/citrix-hypervisor-developing-products/citrix-hypervisor-staticip.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 through 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 through xenstore is most useful for bootstrapping newly deployed VMs, much less for reconfiguring existing ones. More info here: U(https://support.citrix.com/article/CTX226713)' requirements: - XenAPI attributes: check_mode: support: full diff_mode: support: none options: state: description: - Specify the state VM should be in. - If O(state) is set to V(present) and VM exists, ensure the VM configuration conforms to given parameters. - If O(state) is set to V(present) and VM does not exist, then VM is deployed with given parameters. - If O(state) is set to V(absent) and VM exists, then VM is removed with its associated components. - If O(state) is set to V(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on automatically. type: str 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 O(uuid) to uniquely specify VM to manage. - This parameter is case sensitive. type: str aliases: [ name_label ] name_desc: description: - VM description. type: str 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. type: str 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 O(template_uuid) to uniquely specify source template. - If VM already exists, this setting will be ignored. - This parameter is case sensitive. type: str 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. type: str is_template: description: - Convert VM to template. type: bool default: false folder: description: - Destination folder for VM. - This parameter is case sensitive. - 'Example:' - ' folder: /folder1/folder2' type: str hardware: description: - Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters. type: dict suboptions: num_cpus: description: - Number of CPUs. type: int num_cpu_cores_per_socket: description: - Number of Cores Per Socket. O(hardware.num_cpus) has to be a multiple of O(hardware.num_cpu_cores_per_socket). type: int memory_mb: description: - Amount of memory in MB. type: int disks: description: - A list of disks to add to VM. - All parameters are case sensitive. - Removing or detaching existing disks of VM is not supported. - New disks are required to have either a O(disks[].size) or one of O(ignore:disks[].size_[tb,gb,mb,kb,b]) parameters specified. - VM needs to be shut down to reconfigure disk size. type: list elements: dict aliases: [ disk ] suboptions: size: description: - 'Disk size with unit. Unit must be: V(b), V(kb), V(mb), V(gb), V(tb). VM needs to be shut down to reconfigure this parameter.' - If no unit is specified, size is assumed to be in bytes. type: str size_b: description: - Disk size in bytes. type: str size_kb: description: - Disk size in kilobytes. type: str size_mb: description: - Disk size in megabytes. type: str size_gb: description: - Disk size in gigabytes. type: str size_tb: description: - Disk size in terabytes. type: str name: description: - Disk name. type: str aliases: [ name_label ] name_desc: description: - Disk description. type: str sr: description: - Storage Repository to create disk on. If not specified, will use default SR. Cannot be used for moving disk to other SR. type: str sr_uuid: description: - UUID of a SR to create disk on. Use if SR name is not unique. type: str cdrom: description: - A CD-ROM configuration for the VM. - All parameters are case sensitive. type: dict suboptions: type: description: - The type of CD-ROM. With V(none) the CD-ROM device will be present but empty. type: str choices: [ none, iso ] iso_name: description: - 'The file name of an ISO image from one of the XenServer ISO Libraries (implies O(cdrom.type=iso)).' - Required if O(cdrom.type) is set to V(iso). type: str networks: description: - A list of networks (in the order of the NICs). - All parameters are case sensitive. - Name is required for new NICs. Other parameters are optional in all cases. type: list elements: dict aliases: [ network ] suboptions: name: description: - Name of a XenServer network to attach the network interface to. type: str aliases: [ name_label ] mac: description: - Customize MAC address of the interface. type: str type: description: - Type of IPv4 assignment. Value V(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). type: str choices: [ none, dhcp, static ] ip: description: - 'Static IPv4 address (implies O(networks[].type=static)). Can include prefix in format C(/) instead of using C(netmask).' type: str netmask: description: - Static IPv4 netmask required for O(networks[].ip) if prefix is not specified. type: str gateway: description: - Static IPv4 gateway. type: str type6: description: - Type of IPv6 assignment. Value V(none) means whatever is default for OS. type: str choices: [ none, dhcp, static ] ip6: description: - 'Static IPv6 address (implies O(networks[].type6=static)) with prefix in format C(/).' type: str gateway6: description: - Static IPv6 gateway. type: str home_server: description: - Name of a XenServer host that will be a Home Server for the VM. - This parameter is case sensitive. type: str custom_params: description: - Define a list of custom VM params to set on VM. - Useful for advanced users familiar with managing VM params through xe CLI. - A custom value object takes two fields O(custom_params[].key) and O(custom_params[].value) (see example below). type: list elements: dict suboptions: key: description: - VM param name. type: str required: true value: description: - VM param value. type: raw required: true wait_for_ip_address: description: - Wait until XenServer detects an IP address for the VM. If O(state) is set to V(absent), this parameter is ignored. - This requires XenServer Tools to be preinstalled on the VM to work properly. type: bool default: false state_change_timeout: description: - 'By default, module will wait indefinitely for VM to acquire an IP address if O(wait_for_ip_address=true).' - 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. type: int 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. - This is equivalent to C(Use storage-level fast disk clone) option in XenCenter. type: bool default: false 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. type: bool default: false extends_documentation_fragment: - community.general.xenserver.documentation - community.general.attributes ''' EXAMPLES = r''' - name: Create a VM from a template community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" validate_certs: false 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: true delegate_to: localhost register: deploy - name: Create a VM template community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" validate_certs: false folder: /testvms name: testvm_6 is_template: true 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) community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" uuid: 421e4592-c069-924d-ce20-7e7533fab926 name: new_name state: present delegate_to: localhost - name: Remove a VM by UUID community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" uuid: 421e4592-c069-924d-ce20-7e7533fab926 state: absent delegate_to: localhost - name: Modify custom params (boot order) community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" name: testvm_8 state: present custom_params: - key: HVM_boot_params value: { "order": "ndc" } delegate_to: localhost - name: Customize network parameters community.general.xenserver_guest: hostname: "{{ xenserver_hostname }}" username: "{{ xenserver_username }}" password: "{{ xenserver_password }}" 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.common.network import is_mac from ansible.module_utils import six from ansible_collections.community.general.plugins.module_utils.xenserver import ( xenserver_common_argument_spec, XenServerObject, get_object_ref, gather_vm_params, gather_vm_facts, set_vm_power_state, wait_for_vm_ip_address, 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 explicitly.") # 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!") # Support for Ansible check mode. if self.module.check_mode: return # 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' and not self.module.params['force']: 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 if "need_poweredoff" in config_changes and vm_power_state_save != 'halted' and self.module.params['force']: self.set_power_state("shutdownguest") 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) vbd_ref_new = self.xapi_session.xenapi.VBD.create(new_disk_vbd) if self.vm_params['power_state'].lower() == "running": self.xapi_session.xenapi.VBD.plug(vbd_ref_new) 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. through 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' and not self.module.params['force']: 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 # Make sure that VM is poweredoff before we can destroy it. self.set_power_state("poweredoff") 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 existence 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 existence 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 explicitly." % 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 no place was found. if disk_userdevice is None: # Highest occupied place could be a CD-ROM device # so we have to include all devices regardless of # type when calculating out-of-bound position. disk_userdevice = str(int(self.vm_params['VBDs'][-1]['userdevice']) + 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 existence 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 existence 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_mac(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'], "none"): 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'], "none"): 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 if not vif_devices_allowed: self.module.fail_json(msg="VM check networks[%s]: maximum number of network interfaces reached!" % position) # 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_devices_allowed.remove(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 or size < 0: 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, no_log=False), 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'] and module.params['state'] != "absent": 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()