1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright: (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = r'''
15---
16module: xenserver_guest
17short_description: Manages virtual machines running on Citrix Hypervisor/XenServer host or pool
18description: >
19   This module can be used to create new virtual machines from templates or other virtual machines,
20   modify various virtual machine components like network and disk, rename a virtual machine and
21   remove a virtual machine with associated components.
22version_added: '2.8'
23author:
24- Bojan Vitnik (@bvitnik) <bvitnik@mainstream.rs>
25notes:
26- Minimal supported version of XenServer is 5.6.
27- Module was tested with XenServer 6.5, 7.1, 7.2, 7.6, Citrix Hypervisor 8.0, XCP-ng 7.6 and 8.0.
28- 'To acquire XenAPI Python library, just run C(pip install XenAPI) on your Ansible Control Node. The library can also be found inside
29   Citrix Hypervisor/XenServer SDK (downloadable from Citrix website). Copy the XenAPI.py file from the SDK to your Python site-packages on your
30   Ansible Control Node to use it. Latest version of the library can also be acquired from GitHub:
31   https://raw.githubusercontent.com/xapi-project/xen-api/master/scripts/examples/python/XenAPI.py'
32- '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
33   accessing XenServer host in trusted environment or use C(https://) scheme explicitly.'
34- '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)
35   which requires XenAPI library from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.'
36- 'Network configuration inside a guest OS, by using C(networks.type), C(networks.ip), C(networks.gateway) etc. parameters, is supported on
37  XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for network configuration. The module will try to
38  detect if such support is available and utilize it, else it will use a custom method of configuration via xenstore. Since XenServer Guest
39  agent only support None and Static types of network configuration, where None means DHCP configured interface, C(networks.type) and C(networks.type6)
40  values C(none) and C(dhcp) have same effect. More info here:
41  https://www.citrix.com/community/citrix-developer/citrix-hypervisor-developer/citrix-hypervisor-developing-products/citrix-hypervisor-staticip.html'
42- 'On platforms without official support for network configuration inside a guest OS, network parameters will be written to xenstore
43  C(vm-data/networks/<vif_device>) key. Parameters can be inspected by using C(xenstore ls) and C(xenstore read) tools on \*nix guests or trough
44  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
45  to implement a boot time scripts or custom agent that will read the parameters from xenstore and configure network with given parameters.
46  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
47  parameter is changed. This is a limitation of XenAPI and xenstore. Considering these limitations, network configuration trough xenstore is most
48  useful for bootstraping newly deployed VMs, much less for reconfiguring existing ones. More info here:
49  https://support.citrix.com/article/CTX226713'
50requirements:
51- python >= 2.6
52- XenAPI
53options:
54  state:
55    description:
56    - Specify the state VM should be in.
57    - If C(state) is set to C(present) and VM exists, ensure the VM configuration conforms to given parameters.
58    - If C(state) is set to C(present) and VM does not exist, then VM is deployed with given parameters.
59    - If C(state) is set to C(absent) and VM exists, then VM is removed with its associated components.
60    - If C(state) is set to C(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on automatically.
61    type: str
62    default: present
63    choices: [ present, absent, poweredon ]
64  name:
65    description:
66    - Name of the VM to work with.
67    - VMs running on XenServer do not necessarily have unique names. The module will fail if multiple VMs with same name are found.
68    - In case of multiple VMs with same name, use C(uuid) to uniquely specify VM to manage.
69    - This parameter is case sensitive.
70    type: str
71    required: yes
72    aliases: [ name_label ]
73  name_desc:
74    description:
75    - VM description.
76    type: str
77  uuid:
78    description:
79    - UUID of the VM to manage if known. This is XenServer's unique identifier.
80    - It is required if name is not unique.
81    - Please note that a supplied UUID will be ignored on VM creation, as XenServer creates the UUID internally.
82    type: str
83  template:
84    description:
85    - Name of a template, an existing VM (must be shut down) or a snapshot that should be used to create VM.
86    - Templates/VMs/snapshots on XenServer do not necessarily have unique names. The module will fail if multiple templates with same name are found.
87    - In case of multiple templates/VMs/snapshots with same name, use C(template_uuid) to uniquely specify source template.
88    - If VM already exists, this setting will be ignored.
89    - This parameter is case sensitive.
90    type: str
91    aliases: [ template_src ]
92  template_uuid:
93    description:
94    - UUID of a template, an existing VM or a snapshot that should be used to create VM.
95    - It is required if template name is not unique.
96    type: str
97  is_template:
98    description:
99    - Convert VM to template.
100    type: bool
101    default: no
102  folder:
103    description:
104    - Destination folder for VM.
105    - This parameter is case sensitive.
106    - 'Example:'
107    - '  folder: /folder1/folder2'
108    type: str
109  hardware:
110    description:
111    - Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters.
112    - 'Valid parameters are:'
113    - ' - C(num_cpus) (integer): Number of CPUs.'
114    - ' - 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).'
115    - ' - C(memory_mb) (integer): Amount of memory in MB.'
116    type: dict
117  disks:
118    description:
119    - A list of disks to add to VM.
120    - All parameters are case sensitive.
121    - Removing or detaching existing disks of VM is not supported.
122    - 'Required parameters per entry:'
123    - ' - C(size_[tb,gb,mb,kb,b]) (integer): Disk storage size in specified unit. VM needs to be shut down to reconfigure this parameter.'
124    - 'Optional parameters per entry:'
125    - ' - C(name) (string): Disk name. You can also use C(name_label) as an alias.'
126    - ' - C(name_desc) (string): Disk description.'
127    - ' - 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.'
128    - ' - C(sr_uuid) (string): UUID of a SR to create disk on. Use if SR name is not unique.'
129    type: list
130    aliases: [ disk ]
131  cdrom:
132    description:
133    - A CD-ROM configuration for the VM.
134    - All parameters are case sensitive.
135    - 'Valid parameters are:'
136    - ' - 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.'
137    - ' - C(iso_name) (string): The file name of an ISO image from one of the XenServer ISO Libraries (implies C(type: iso)).
138          Required if C(type) is set to C(iso).'
139    type: dict
140  networks:
141    description:
142    - A list of networks (in the order of the NICs).
143    - All parameters are case sensitive.
144    - 'Required parameters per entry:'
145    - ' - C(name) (string): Name of a XenServer network to attach the network interface to. You can also use C(name_label) as an alias.'
146    - 'Optional parameters per entry (used for VM hardware):'
147    - ' - C(mac) (string): Customize MAC address of the interface.'
148    - 'Optional parameters per entry (used for OS customization):'
149    - ' - 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.
150          On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).'
151    - ' - C(ip) (string): Static IPv4 address (implies C(type: static)). Can include prefix in format <IPv4 address>/<prefix> instead of using C(netmask).'
152    - ' - C(netmask) (string): Static IPv4 netmask required for C(ip) if prefix is not specified.'
153    - ' - C(gateway) (string): Static IPv4 gateway.'
154    - ' - 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.
155          On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).'
156    - ' - C(ip6) (string): Static IPv6 address (implies C(type6: static)) with prefix in format <IPv6 address>/<prefix>.'
157    - ' - C(gateway6) (string): Static IPv6 gateway.'
158    type: list
159    aliases: [ network ]
160  home_server:
161    description:
162    - Name of a XenServer host that will be a Home Server for the VM.
163    - This parameter is case sensitive.
164    type: str
165  custom_params:
166    description:
167    - Define a list of custom VM params to set on VM.
168    - Useful for advanced users familiar with managing VM params trough xe CLI.
169    - A custom value object takes two fields C(key) and C(value) (see example below).
170    type: list
171  wait_for_ip_address:
172    description:
173    - Wait until XenServer detects an IP address for the VM. If C(state) is set to C(absent), this parameter is ignored.
174    - This requires XenServer Tools to be preinstalled on the VM to work properly.
175    type: bool
176    default: no
177  state_change_timeout:
178    description:
179    - 'By default, module will wait indefinitely for VM to accquire an IP address if C(wait_for_ip_address: yes).'
180    - If this parameter is set to positive value, the module will instead wait specified number of seconds for the state change.
181    - In case of timeout, module will generate an error message.
182    type: int
183    default: 0
184  linked_clone:
185    description:
186    - Whether to create a Linked Clone from the template, existing VM or snapshot. If no, will create a full copy.
187    - This is equivalent to C(Use storage-level fast disk clone) option in XenCenter.
188    type: bool
189    default: no
190  force:
191    description:
192    - Ignore warnings and complete the actions.
193    - This parameter is useful for removing VM in running state or reconfiguring VM params that require VM to be shut down.
194    type: bool
195    default: no
196extends_documentation_fragment: xenserver.documentation
197'''
198
199EXAMPLES = r'''
200- name: Create a VM from a template
201  xenserver_guest:
202    hostname: "{{ xenserver_hostname }}"
203    username: "{{ xenserver_username }}"
204    password: "{{ xenserver_password }}"
205    validate_certs: no
206    folder: /testvms
207    name: testvm_2
208    state: poweredon
209    template: CentOS 7
210    disks:
211    - size_gb: 10
212      sr: my_sr
213    hardware:
214      num_cpus: 6
215      num_cpu_cores_per_socket: 3
216      memory_mb: 512
217    cdrom:
218      type: iso
219      iso_name: guest-tools.iso
220    networks:
221    - name: VM Network
222      mac: aa:bb:dd:aa:00:14
223    wait_for_ip_address: yes
224  delegate_to: localhost
225  register: deploy
226
227- name: Create a VM template
228  xenserver_guest:
229    hostname: "{{ xenserver_hostname }}"
230    username: "{{ xenserver_username }}"
231    password: "{{ xenserver_password }}"
232    validate_certs: no
233    folder: /testvms
234    name: testvm_6
235    is_template: yes
236    disk:
237    - size_gb: 10
238      sr: my_sr
239    hardware:
240      memory_mb: 512
241      num_cpus: 1
242  delegate_to: localhost
243  register: deploy
244
245- name: Rename a VM (requires the VM's UUID)
246  xenserver_guest:
247    hostname: "{{ xenserver_hostname }}"
248    username: "{{ xenserver_username }}"
249    password: "{{ xenserver_password }}"
250    uuid: 421e4592-c069-924d-ce20-7e7533fab926
251    name: new_name
252    state: present
253  delegate_to: localhost
254
255- name: Remove a VM by UUID
256  xenserver_guest:
257    hostname: "{{ xenserver_hostname }}"
258    username: "{{ xenserver_username }}"
259    password: "{{ xenserver_password }}"
260    uuid: 421e4592-c069-924d-ce20-7e7533fab926
261    state: absent
262  delegate_to: localhost
263
264- name: Modify custom params (boot order)
265  xenserver_guest:
266    hostname: "{{ xenserver_hostname }}"
267    username: "{{ xenserver_username }}"
268    password: "{{ xenserver_password }}"
269    name: testvm_8
270    state: present
271    custom_params:
272    - key: HVM_boot_params
273      value: { "order": "ndc" }
274  delegate_to: localhost
275
276- name: Customize network parameters
277  xenserver_guest:
278    hostname: "{{ xenserver_hostname }}"
279    username: "{{ xenserver_username }}"
280    password: "{{ xenserver_password }}"
281    name: testvm_10
282    networks:
283    - name: VM Network
284      ip: 192.168.1.100/24
285      gateway: 192.168.1.1
286    - type: dhcp
287  delegate_to: localhost
288'''
289
290RETURN = r'''
291instance:
292    description: Metadata about the VM
293    returned: always
294    type: dict
295    sample: {
296        "cdrom": {
297            "type": "none"
298        },
299        "customization_agent": "native",
300        "disks": [
301            {
302                "name": "testvm_11-0",
303                "name_desc": "",
304                "os_device": "xvda",
305                "size": 42949672960,
306                "sr": "Local storage",
307                "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
308                "vbd_userdevice": "0"
309            },
310            {
311                "name": "testvm_11-1",
312                "name_desc": "",
313                "os_device": "xvdb",
314                "size": 42949672960,
315                "sr": "Local storage",
316                "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
317                "vbd_userdevice": "1"
318            }
319        ],
320        "domid": "56",
321        "folder": "",
322        "hardware": {
323            "memory_mb": 8192,
324            "num_cpu_cores_per_socket": 2,
325            "num_cpus": 4
326        },
327        "home_server": "",
328        "is_template": false,
329        "name": "testvm_11",
330        "name_desc": "",
331        "networks": [
332            {
333                "gateway": "192.168.0.254",
334                "gateway6": "fc00::fffe",
335                "ip": "192.168.0.200",
336                "ip6": [
337                    "fe80:0000:0000:0000:e9cb:625a:32c5:c291",
338                    "fc00:0000:0000:0000:0000:0000:0000:0001"
339                ],
340                "mac": "ba:91:3a:48:20:76",
341                "mtu": "1500",
342                "name": "Pool-wide network associated with eth1",
343                "netmask": "255.255.255.128",
344                "prefix": "25",
345                "prefix6": "64",
346                "vif_device": "0"
347            }
348        ],
349        "other_config": {
350            "base_template_name": "Windows Server 2016 (64-bit)",
351            "import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5",
352            "install-methods": "cdrom",
353            "instant": "true",
354            "mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e"
355        },
356        "platform": {
357            "acpi": "1",
358            "apic": "true",
359            "cores-per-socket": "2",
360            "device_id": "0002",
361            "hpet": "true",
362            "nx": "true",
363            "pae": "true",
364            "timeoffset": "-25200",
365            "vga": "std",
366            "videoram": "8",
367            "viridian": "true",
368            "viridian_reference_tsc": "true",
369            "viridian_time_ref_count": "true"
370        },
371        "state": "poweredon",
372        "uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda",
373        "xenstore_data": {
374            "vm-data": ""
375        }
376    }
377changes:
378    description: Detected or made changes to VM
379    returned: always
380    type: list
381    sample: [
382        {
383            "hardware": [
384                "num_cpus"
385            ]
386        },
387        {
388            "disks_changed": [
389                [],
390                [
391                    "size"
392                ]
393            ]
394        },
395        {
396            "disks_new": [
397                {
398                    "name": "new-disk",
399                    "name_desc": "",
400                    "position": 2,
401                    "size_gb": "4",
402                    "vbd_userdevice": "2"
403                }
404            ]
405        },
406        {
407            "cdrom": [
408                "type",
409                "iso_name"
410            ]
411        },
412        {
413            "networks_changed": [
414                [
415                    "mac"
416                ],
417            ]
418        },
419        {
420            "networks_new": [
421                {
422                    "name": "Pool-wide network associated with eth2",
423                    "position": 1,
424                    "vif_device": "1"
425                }
426            ]
427        },
428        "need_poweredoff"
429    ]
430'''
431
432import re
433
434HAS_XENAPI = False
435try:
436    import XenAPI
437    HAS_XENAPI = True
438except ImportError:
439    pass
440
441from ansible.module_utils.basic import AnsibleModule
442from ansible.module_utils.common.network import is_mac
443from ansible.module_utils import six
444from ansible.module_utils.xenserver import (xenserver_common_argument_spec, XAPI, XenServerObject, get_object_ref,
445                                            gather_vm_params, gather_vm_facts, set_vm_power_state, wait_for_vm_ip_address,
446                                            is_valid_ip_addr, is_valid_ip_netmask, is_valid_ip_prefix,
447                                            ip_prefix_to_netmask, ip_netmask_to_prefix,
448                                            is_valid_ip6_addr, is_valid_ip6_prefix)
449
450
451class XenServerVM(XenServerObject):
452    """Class for managing XenServer VM.
453
454    Attributes:
455        vm_ref (str): XAPI reference to VM.
456        vm_params (dict): A dictionary with VM parameters as returned
457            by gather_vm_params() function.
458    """
459
460    def __init__(self, module):
461        """Inits XenServerVM using module parameters.
462
463        Args:
464            module: Reference to Ansible module object.
465        """
466        super(XenServerVM, self).__init__(module)
467
468        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: ")
469        self.gather_params()
470
471    def exists(self):
472        """Returns True if VM exists, else False."""
473        return True if self.vm_ref is not None else False
474
475    def gather_params(self):
476        """Gathers all VM parameters available in XAPI database."""
477        self.vm_params = gather_vm_params(self.module, self.vm_ref)
478
479    def gather_facts(self):
480        """Gathers and returns VM facts."""
481        return gather_vm_facts(self.module, self.vm_params)
482
483    def set_power_state(self, power_state):
484        """Controls VM power state."""
485        state_changed, current_state = set_vm_power_state(self.module, self.vm_ref, power_state, self.module.params['state_change_timeout'])
486
487        # If state has changed, update vm_params.
488        if state_changed:
489            self.vm_params['power_state'] = current_state.capitalize()
490
491        return state_changed
492
493    def wait_for_ip_address(self):
494        """Waits for VM to acquire an IP address."""
495        self.vm_params['guest_metrics'] = wait_for_vm_ip_address(self.module, self.vm_ref, self.module.params['state_change_timeout'])
496
497    def deploy(self):
498        """Deploys new VM from template."""
499        # Safety check.
500        if self.exists():
501            self.module.fail_json(msg="Called deploy on existing VM!")
502
503        try:
504            templ_ref = get_object_ref(self.module, self.module.params['template'], self.module.params['template_uuid'], obj_type="template", fail=True,
505                                       msg_prefix="VM deploy: ")
506
507            # Is this an existing running VM?
508            if self.xapi_session.xenapi.VM.get_power_state(templ_ref).lower() != 'halted':
509                self.module.fail_json(msg="VM deploy: running VM cannot be used as a template!")
510
511            # Find a SR we can use for VM.copy(). We use SR of the first disk
512            # if specified or default SR if not specified.
513            disk_params_list = self.module.params['disks']
514
515            sr_ref = None
516
517            if disk_params_list:
518                disk_params = disk_params_list[0]
519
520                disk_sr_uuid = disk_params.get('sr_uuid')
521                disk_sr = disk_params.get('sr')
522
523                if disk_sr_uuid is not None or disk_sr is not None:
524                    sr_ref = get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True,
525                                            msg_prefix="VM deploy disks[0]: ")
526
527            if not sr_ref:
528                if self.default_sr_ref != "OpaqueRef:NULL":
529                    sr_ref = self.default_sr_ref
530                else:
531                    self.module.fail_json(msg="VM deploy disks[0]: no default SR found! You must specify SR explicitly.")
532
533            # VM name could be an empty string which is bad.
534            if self.module.params['name'] is not None and not self.module.params['name']:
535                self.module.fail_json(msg="VM deploy: VM name must not be an empty string!")
536
537            # Support for Ansible check mode.
538            if self.module.check_mode:
539                return
540
541            # Now we can instantiate VM. We use VM.clone for linked_clone and
542            # VM.copy for non linked_clone.
543            if self.module.params['linked_clone']:
544                self.vm_ref = self.xapi_session.xenapi.VM.clone(templ_ref, self.module.params['name'])
545            else:
546                self.vm_ref = self.xapi_session.xenapi.VM.copy(templ_ref, self.module.params['name'], sr_ref)
547
548            # Description is copied over from template so we reset it.
549            self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, "")
550
551            # If template is one of built-in XenServer templates, we have to
552            # do some additional steps.
553            # Note: VM.get_is_default_template() is supported from XenServer 7.2
554            #       onward so we use an alternative way.
555            templ_other_config = self.xapi_session.xenapi.VM.get_other_config(templ_ref)
556
557            if "default_template" in templ_other_config and templ_other_config['default_template']:
558                # other_config of built-in XenServer templates have a key called
559                # 'disks' with the following content:
560                #   disks: <provision><disk bootable="true" device="0" size="10737418240" sr="" type="system"/></provision>
561                # This value of other_data is copied to cloned or copied VM and
562                # it prevents provisioning of VM because sr is not specified and
563                # XAPI returns an error. To get around this, we remove the
564                # 'disks' key and add disks to VM later ourselves.
565                vm_other_config = self.xapi_session.xenapi.VM.get_other_config(self.vm_ref)
566
567                if "disks" in vm_other_config:
568                    del vm_other_config['disks']
569
570                self.xapi_session.xenapi.VM.set_other_config(self.vm_ref, vm_other_config)
571
572            # At this point we have VM ready for provisioning.
573            self.xapi_session.xenapi.VM.provision(self.vm_ref)
574
575            # After provisioning we can prepare vm_params for reconfigure().
576            self.gather_params()
577
578            # VM is almost ready. We just need to reconfigure it...
579            self.reconfigure()
580
581            # Power on VM if needed.
582            if self.module.params['state'] == "poweredon":
583                self.set_power_state("poweredon")
584
585        except XenAPI.Failure as f:
586            self.module.fail_json(msg="XAPI ERROR: %s" % f.details)
587
588    def reconfigure(self):
589        """Reconfigures an existing VM.
590
591        Returns:
592            list: parameters that were reconfigured.
593        """
594        # Safety check.
595        if not self.exists():
596            self.module.fail_json(msg="Called reconfigure on non existing VM!")
597
598        config_changes = self.get_changes()
599
600        vm_power_state_save = self.vm_params['power_state'].lower()
601
602        if "need_poweredoff" in config_changes and vm_power_state_save != 'halted' and not self.module.params['force']:
603            self.module.fail_json(msg="VM reconfigure: VM has to be in powered off state to reconfigure but force was not specified!")
604
605        # Support for Ansible check mode.
606        if self.module.check_mode:
607            return config_changes
608
609        if "need_poweredoff" in config_changes and vm_power_state_save != 'halted' and self.module.params['force']:
610            self.set_power_state("shutdownguest")
611
612        try:
613            for change in config_changes:
614                if isinstance(change, six.string_types):
615                    if change == "name":
616                        self.xapi_session.xenapi.VM.set_name_label(self.vm_ref, self.module.params['name'])
617                    elif change == "name_desc":
618                        self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, self.module.params['name_desc'])
619                    elif change == "folder":
620                        self.xapi_session.xenapi.VM.remove_from_other_config(self.vm_ref, 'folder')
621
622                        if self.module.params['folder']:
623                            self.xapi_session.xenapi.VM.add_to_other_config(self.vm_ref, 'folder', self.module.params['folder'])
624                    elif change == "home_server":
625                        if self.module.params['home_server']:
626                            host_ref = self.xapi_session.xenapi.host.get_by_name_label(self.module.params['home_server'])[0]
627                        else:
628                            host_ref = "OpaqueRef:NULL"
629
630                        self.xapi_session.xenapi.VM.set_affinity(self.vm_ref, host_ref)
631                elif isinstance(change, dict):
632                    if change.get('hardware'):
633                        for hardware_change in change['hardware']:
634                            if hardware_change == "num_cpus":
635                                num_cpus = int(self.module.params['hardware']['num_cpus'])
636
637                                if num_cpus < int(self.vm_params['VCPUs_at_startup']):
638                                    self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
639                                    self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
640                                else:
641                                    self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
642                                    self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
643                            elif hardware_change == "num_cpu_cores_per_socket":
644                                self.xapi_session.xenapi.VM.remove_from_platform(self.vm_ref, 'cores-per-socket')
645                                num_cpu_cores_per_socket = int(self.module.params['hardware']['num_cpu_cores_per_socket'])
646
647                                if num_cpu_cores_per_socket > 1:
648                                    self.xapi_session.xenapi.VM.add_to_platform(self.vm_ref, 'cores-per-socket', str(num_cpu_cores_per_socket))
649                            elif hardware_change == "memory_mb":
650                                memory_b = str(int(self.module.params['hardware']['memory_mb']) * 1048576)
651                                vm_memory_static_min_b = str(min(int(memory_b), int(self.vm_params['memory_static_min'])))
652
653                                self.xapi_session.xenapi.VM.set_memory_limits(self.vm_ref, vm_memory_static_min_b, memory_b, memory_b, memory_b)
654                    elif change.get('disks_changed'):
655                        vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"]
656                        position = 0
657
658                        for disk_change_list in change['disks_changed']:
659                            for disk_change in disk_change_list:
660                                vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params_list[position]['VDI']['uuid'])
661
662                                if disk_change == "name":
663                                    self.xapi_session.xenapi.VDI.set_name_label(vdi_ref, self.module.params['disks'][position]['name'])
664                                elif disk_change == "name_desc":
665                                    self.xapi_session.xenapi.VDI.set_name_description(vdi_ref, self.module.params['disks'][position]['name_desc'])
666                                elif disk_change == "size":
667                                    self.xapi_session.xenapi.VDI.resize(vdi_ref, str(self.get_normalized_disk_size(self.module.params['disks'][position],
668                                                                                                                   "VM reconfigure disks[%s]: " % position)))
669
670                            position += 1
671                    elif change.get('disks_new'):
672                        for position, disk_userdevice in change['disks_new']:
673                            disk_params = self.module.params['disks'][position]
674
675                            disk_name = disk_params['name'] if disk_params.get('name') else "%s-%s" % (self.vm_params['name_label'], position)
676                            disk_name_desc = disk_params['name_desc'] if disk_params.get('name_desc') else ""
677
678                            if disk_params.get('sr_uuid'):
679                                sr_ref = self.xapi_session.xenapi.SR.get_by_uuid(disk_params['sr_uuid'])
680                            elif disk_params.get('sr'):
681                                sr_ref = self.xapi_session.xenapi.SR.get_by_name_label(disk_params['sr'])[0]
682                            else:
683                                sr_ref = self.default_sr_ref
684
685                            disk_size = str(self.get_normalized_disk_size(self.module.params['disks'][position], "VM reconfigure disks[%s]: " % position))
686
687                            new_disk_vdi = {
688                                "name_label": disk_name,
689                                "name_description": disk_name_desc,
690                                "SR": sr_ref,
691                                "virtual_size": disk_size,
692                                "type": "user",
693                                "sharable": False,
694                                "read_only": False,
695                                "other_config": {},
696                            }
697
698                            new_disk_vbd = {
699                                "VM": self.vm_ref,
700                                "VDI": None,
701                                "userdevice": disk_userdevice,
702                                "bootable": False,
703                                "mode": "RW",
704                                "type": "Disk",
705                                "empty": False,
706                                "other_config": {},
707                                "qos_algorithm_type": "",
708                                "qos_algorithm_params": {},
709                            }
710
711                            new_disk_vbd['VDI'] = self.xapi_session.xenapi.VDI.create(new_disk_vdi)
712                            vbd_ref_new = self.xapi_session.xenapi.VBD.create(new_disk_vbd)
713
714                            if self.vm_params['power_state'].lower() == "running":
715                                self.xapi_session.xenapi.VBD.plug(vbd_ref_new)
716
717                    elif change.get('cdrom'):
718                        vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"]
719
720                        # If there is no CD present, we have to create one.
721                        if not vm_cdrom_params_list:
722                            # We will try to place cdrom at userdevice position
723                            # 3 (which is default) if it is not already occupied
724                            # else we will place it at first allowed position.
725                            cdrom_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)
726
727                            if "3" in cdrom_userdevices_allowed:
728                                cdrom_userdevice = "3"
729                            else:
730                                cdrom_userdevice = cdrom_userdevices_allowed[0]
731
732                            cdrom_vbd = {
733                                "VM": self.vm_ref,
734                                "VDI": "OpaqueRef:NULL",
735                                "userdevice": cdrom_userdevice,
736                                "bootable": False,
737                                "mode": "RO",
738                                "type": "CD",
739                                "empty": True,
740                                "other_config": {},
741                                "qos_algorithm_type": "",
742                                "qos_algorithm_params": {},
743                            }
744
745                            cdrom_vbd_ref = self.xapi_session.xenapi.VBD.create(cdrom_vbd)
746                        else:
747                            cdrom_vbd_ref = self.xapi_session.xenapi.VBD.get_by_uuid(vm_cdrom_params_list[0]['uuid'])
748
749                        cdrom_is_empty = self.xapi_session.xenapi.VBD.get_empty(cdrom_vbd_ref)
750
751                        for cdrom_change in change['cdrom']:
752                            if cdrom_change == "type":
753                                cdrom_type = self.module.params['cdrom']['type']
754
755                                if cdrom_type == "none" and not cdrom_is_empty:
756                                    self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)
757                                elif cdrom_type == "host":
758                                    # Unimplemented!
759                                    pass
760
761                            elif cdrom_change == "iso_name":
762                                if not cdrom_is_empty:
763                                    self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)
764
765                                cdrom_vdi_ref = self.xapi_session.xenapi.VDI.get_by_name_label(self.module.params['cdrom']['iso_name'])[0]
766                                self.xapi_session.xenapi.VBD.insert(cdrom_vbd_ref, cdrom_vdi_ref)
767                    elif change.get('networks_changed'):
768                        position = 0
769
770                        for network_change_list in change['networks_changed']:
771                            if network_change_list:
772                                vm_vif_params = self.vm_params['VIFs'][position]
773                                network_params = self.module.params['networks'][position]
774
775                                vif_ref = self.xapi_session.xenapi.VIF.get_by_uuid(vm_vif_params['uuid'])
776                                network_ref = self.xapi_session.xenapi.network.get_by_uuid(vm_vif_params['network']['uuid'])
777
778                                vif_recreated = False
779
780                                if "name" in network_change_list or "mac" in network_change_list:
781                                    # To change network or MAC, we destroy old
782                                    # VIF and then create a new one with changed
783                                    # parameters. That's how XenCenter does it.
784
785                                    # Copy all old parameters to new VIF record.
786                                    vif = {
787                                        "device": vm_vif_params['device'],
788                                        "network": network_ref,
789                                        "VM": vm_vif_params['VM'],
790                                        "MAC": vm_vif_params['MAC'],
791                                        "MTU": vm_vif_params['MTU'],
792                                        "other_config": vm_vif_params['other_config'],
793                                        "qos_algorithm_type": vm_vif_params['qos_algorithm_type'],
794                                        "qos_algorithm_params": vm_vif_params['qos_algorithm_params'],
795                                        "locking_mode": vm_vif_params['locking_mode'],
796                                        "ipv4_allowed": vm_vif_params['ipv4_allowed'],
797                                        "ipv6_allowed": vm_vif_params['ipv6_allowed'],
798                                    }
799
800                                    if "name" in network_change_list:
801                                        network_ref_new = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0]
802                                        vif['network'] = network_ref_new
803                                        vif['MTU'] = self.xapi_session.xenapi.network.get_MTU(network_ref_new)
804
805                                    if "mac" in network_change_list:
806                                        vif['MAC'] = network_params['mac'].lower()
807
808                                    if self.vm_params['power_state'].lower() == "running":
809                                        self.xapi_session.xenapi.VIF.unplug(vif_ref)
810
811                                    self.xapi_session.xenapi.VIF.destroy(vif_ref)
812                                    vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)
813
814                                    if self.vm_params['power_state'].lower() == "running":
815                                        self.xapi_session.xenapi.VIF.plug(vif_ref_new)
816
817                                    vif_ref = vif_ref_new
818                                    vif_recreated = True
819
820                                if self.vm_params['customization_agent'] == "native":
821                                    vif_reconfigure_needed = False
822
823                                    if "type" in network_change_list:
824                                        network_type = network_params['type'].capitalize()
825                                        vif_reconfigure_needed = True
826                                    else:
827                                        network_type = vm_vif_params['ipv4_configuration_mode']
828
829                                    if "ip" in network_change_list:
830                                        network_ip = network_params['ip']
831                                        vif_reconfigure_needed = True
832                                    elif vm_vif_params['ipv4_addresses']:
833                                        network_ip = vm_vif_params['ipv4_addresses'][0].split('/')[0]
834                                    else:
835                                        network_ip = ""
836
837                                    if "prefix" in network_change_list:
838                                        network_prefix = "/%s" % network_params['prefix']
839                                        vif_reconfigure_needed = True
840                                    elif vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]:
841                                        network_prefix = "/%s" % vm_vif_params['ipv4_addresses'][0].split('/')[1]
842                                    else:
843                                        network_prefix = ""
844
845                                    if "gateway" in network_change_list:
846                                        network_gateway = network_params['gateway']
847                                        vif_reconfigure_needed = True
848                                    else:
849                                        network_gateway = vm_vif_params['ipv4_gateway']
850
851                                    if vif_recreated or vif_reconfigure_needed:
852                                        self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref, network_type,
853                                                                                    "%s%s" % (network_ip, network_prefix), network_gateway)
854
855                                    vif_reconfigure_needed = False
856
857                                    if "type6" in network_change_list:
858                                        network_type6 = network_params['type6'].capitalize()
859                                        vif_reconfigure_needed = True
860                                    else:
861                                        network_type6 = vm_vif_params['ipv6_configuration_mode']
862
863                                    if "ip6" in network_change_list:
864                                        network_ip6 = network_params['ip6']
865                                        vif_reconfigure_needed = True
866                                    elif vm_vif_params['ipv6_addresses']:
867                                        network_ip6 = vm_vif_params['ipv6_addresses'][0].split('/')[0]
868                                    else:
869                                        network_ip6 = ""
870
871                                    if "prefix6" in network_change_list:
872                                        network_prefix6 = "/%s" % network_params['prefix6']
873                                        vif_reconfigure_needed = True
874                                    elif vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]:
875                                        network_prefix6 = "/%s" % vm_vif_params['ipv6_addresses'][0].split('/')[1]
876                                    else:
877                                        network_prefix6 = ""
878
879                                    if "gateway6" in network_change_list:
880                                        network_gateway6 = network_params['gateway6']
881                                        vif_reconfigure_needed = True
882                                    else:
883                                        network_gateway6 = vm_vif_params['ipv6_gateway']
884
885                                    if vif_recreated or vif_reconfigure_needed:
886                                        self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref, network_type6,
887                                                                                    "%s%s" % (network_ip6, network_prefix6), network_gateway6)
888
889                                elif self.vm_params['customization_agent'] == "custom":
890                                    vif_device = vm_vif_params['device']
891
892                                    # A user could have manually changed network
893                                    # or mac e.g. trough XenCenter and then also
894                                    # make those changes in playbook manually.
895                                    # In that case, module will not detect any
896                                    # changes and info in xenstore_data will
897                                    # become stale. For that reason we always
898                                    # update name and mac in xenstore_data.
899
900                                    # Since we handle name and mac differently,
901                                    # we have to remove them from
902                                    # network_change_list.
903                                    network_change_list_tmp = [net_chg for net_chg in network_change_list if net_chg not in ['name', 'mac']]
904
905                                    for network_change in network_change_list_tmp + ['name', 'mac']:
906                                        self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref,
907                                                                                              "vm-data/networks/%s/%s" % (vif_device, network_change))
908
909                                    if network_params.get('name'):
910                                        network_name = network_params['name']
911                                    else:
912                                        network_name = vm_vif_params['network']['name_label']
913
914                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
915                                                                                     "vm-data/networks/%s/%s" % (vif_device, 'name'), network_name)
916
917                                    if network_params.get('mac'):
918                                        network_mac = network_params['mac'].lower()
919                                    else:
920                                        network_mac = vm_vif_params['MAC'].lower()
921
922                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
923                                                                                     "vm-data/networks/%s/%s" % (vif_device, 'mac'), network_mac)
924
925                                    for network_change in network_change_list_tmp:
926                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
927                                                                                         "vm-data/networks/%s/%s" % (vif_device, network_change),
928                                                                                         network_params[network_change])
929
930                            position += 1
931                    elif change.get('networks_new'):
932                        for position, vif_device in change['networks_new']:
933                            network_params = self.module.params['networks'][position]
934
935                            network_ref = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0]
936
937                            network_name = network_params['name']
938                            network_mac = network_params['mac'] if network_params.get('mac') else ""
939                            network_type = network_params.get('type')
940                            network_ip = network_params['ip'] if network_params.get('ip') else ""
941                            network_prefix = network_params['prefix'] if network_params.get('prefix') else ""
942                            network_netmask = network_params['netmask'] if network_params.get('netmask') else ""
943                            network_gateway = network_params['gateway'] if network_params.get('gateway') else ""
944                            network_type6 = network_params.get('type6')
945                            network_ip6 = network_params['ip6'] if network_params.get('ip6') else ""
946                            network_prefix6 = network_params['prefix6'] if network_params.get('prefix6') else ""
947                            network_gateway6 = network_params['gateway6'] if network_params.get('gateway6') else ""
948
949                            vif = {
950                                "device": vif_device,
951                                "network": network_ref,
952                                "VM": self.vm_ref,
953                                "MAC": network_mac,
954                                "MTU": self.xapi_session.xenapi.network.get_MTU(network_ref),
955                                "other_config": {},
956                                "qos_algorithm_type": "",
957                                "qos_algorithm_params": {},
958                            }
959
960                            vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)
961
962                            if self.vm_params['power_state'].lower() == "running":
963                                self.xapi_session.xenapi.VIF.plug(vif_ref_new)
964
965                            if self.vm_params['customization_agent'] == "native":
966                                if network_type and network_type == "static":
967                                    self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref_new, "Static",
968                                                                                "%s/%s" % (network_ip, network_prefix), network_gateway)
969
970                                if network_type6 and network_type6 == "static":
971                                    self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref_new, "Static",
972                                                                                "%s/%s" % (network_ip6, network_prefix6), network_gateway6)
973                            elif self.vm_params['customization_agent'] == "custom":
974                                # We first have to remove any existing data
975                                # from xenstore_data because there could be
976                                # some old leftover data from some interface
977                                # that once occupied same device location as
978                                # our new interface.
979                                for network_param in ['name', 'mac', 'type', 'ip', 'prefix', 'netmask', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']:
980                                    self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref, "vm-data/networks/%s/%s" % (vif_device, network_param))
981
982                                self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/name" % vif_device, network_name)
983
984                                # We get MAC from VIF itself instead of
985                                # networks.mac because it could be
986                                # autogenerated.
987                                vm_vif_mac = self.xapi_session.xenapi.VIF.get_MAC(vif_ref_new)
988                                self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/mac" % vif_device, vm_vif_mac)
989
990                                if network_type:
991                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type" % vif_device, network_type)
992
993                                    if network_type == "static":
994                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
995                                                                                         "vm-data/networks/%s/ip" % vif_device, network_ip)
996                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
997                                                                                         "vm-data/networks/%s/prefix" % vif_device, network_prefix)
998                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
999                                                                                         "vm-data/networks/%s/netmask" % vif_device, network_netmask)
1000                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
1001                                                                                         "vm-data/networks/%s/gateway" % vif_device, network_gateway)
1002
1003                                if network_type6:
1004                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type6" % vif_device, network_type6)
1005
1006                                    if network_type6 == "static":
1007                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
1008                                                                                         "vm-data/networks/%s/ip6" % vif_device, network_ip6)
1009                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
1010                                                                                         "vm-data/networks/%s/prefix6" % vif_device, network_prefix6)
1011                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
1012                                                                                         "vm-data/networks/%s/gateway6" % vif_device, network_gateway6)
1013
1014                    elif change.get('custom_params'):
1015                        for position in change['custom_params']:
1016                            custom_param_key = self.module.params['custom_params'][position]['key']
1017                            custom_param_value = self.module.params['custom_params'][position]['value']
1018                            self.xapi_session.xenapi_request("VM.set_%s" % custom_param_key, (self.vm_ref, custom_param_value))
1019
1020            if self.module.params['is_template']:
1021                self.xapi_session.xenapi.VM.set_is_a_template(self.vm_ref, True)
1022            elif "need_poweredoff" in config_changes and self.module.params['force'] and vm_power_state_save != 'halted':
1023                self.set_power_state("poweredon")
1024
1025            # Gather new params after reconfiguration.
1026            self.gather_params()
1027
1028        except XenAPI.Failure as f:
1029            self.module.fail_json(msg="XAPI ERROR: %s" % f.details)
1030
1031        return config_changes
1032
1033    def destroy(self):
1034        """Removes an existing VM with associated disks"""
1035        # Safety check.
1036        if not self.exists():
1037            self.module.fail_json(msg="Called destroy on non existing VM!")
1038
1039        if self.vm_params['power_state'].lower() != 'halted' and not self.module.params['force']:
1040            self.module.fail_json(msg="VM destroy: VM has to be in powered off state to destroy but force was not specified!")
1041
1042        # Support for Ansible check mode.
1043        if self.module.check_mode:
1044            return
1045
1046        # Make sure that VM is poweredoff before we can destroy it.
1047        self.set_power_state("poweredoff")
1048
1049        try:
1050            # Destroy VM!
1051            self.xapi_session.xenapi.VM.destroy(self.vm_ref)
1052
1053            vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"]
1054
1055            # Destroy all VDIs associated with VM!
1056            for vm_disk_params in vm_disk_params_list:
1057                vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params['VDI']['uuid'])
1058
1059                self.xapi_session.xenapi.VDI.destroy(vdi_ref)
1060
1061        except XenAPI.Failure as f:
1062            self.module.fail_json(msg="XAPI ERROR: %s" % f.details)
1063
1064    def get_changes(self):
1065        """Finds VM parameters that differ from specified ones.
1066
1067        This method builds a dictionary with hierarchy of VM parameters
1068        that differ from those specified in module parameters.
1069
1070        Returns:
1071            list: VM parameters that differ from those specified in
1072            module parameters.
1073        """
1074        # Safety check.
1075        if not self.exists():
1076            self.module.fail_json(msg="Called get_changes on non existing VM!")
1077
1078        need_poweredoff = False
1079
1080        if self.module.params['is_template']:
1081            need_poweredoff = True
1082
1083        try:
1084            # This VM could be a template or a snapshot. In that case we fail
1085            # because we can't reconfigure them or it would just be too
1086            # dangerous.
1087            if self.vm_params['is_a_template'] and not self.vm_params['is_a_snapshot']:
1088                self.module.fail_json(msg="VM check: targeted VM is a template! Template reconfiguration is not supported.")
1089
1090            if self.vm_params['is_a_snapshot']:
1091                self.module.fail_json(msg="VM check: targeted VM is a snapshot! Snapshot reconfiguration is not supported.")
1092
1093            # Let's build a list of parameters that changed.
1094            config_changes = []
1095
1096            # Name could only differ if we found an existing VM by uuid.
1097            if self.module.params['name'] is not None and self.module.params['name'] != self.vm_params['name_label']:
1098                if self.module.params['name']:
1099                    config_changes.append('name')
1100                else:
1101                    self.module.fail_json(msg="VM check name: VM name cannot be an empty string!")
1102
1103            if self.module.params['name_desc'] is not None and self.module.params['name_desc'] != self.vm_params['name_description']:
1104                config_changes.append('name_desc')
1105
1106            # Folder parameter is found in other_config.
1107            vm_other_config = self.vm_params['other_config']
1108            vm_folder = vm_other_config.get('folder', '')
1109
1110            if self.module.params['folder'] is not None and self.module.params['folder'] != vm_folder:
1111                config_changes.append('folder')
1112
1113            if self.module.params['home_server'] is not None:
1114                if (self.module.params['home_server'] and
1115                        (not self.vm_params['affinity'] or self.module.params['home_server'] != self.vm_params['affinity']['name_label'])):
1116
1117                    # Check existance only. Ignore return value.
1118                    get_object_ref(self.module, self.module.params['home_server'], uuid=None, obj_type="home server", fail=True,
1119                                   msg_prefix="VM check home_server: ")
1120
1121                    config_changes.append('home_server')
1122                elif not self.module.params['home_server'] and self.vm_params['affinity']:
1123                    config_changes.append('home_server')
1124
1125            config_changes_hardware = []
1126
1127            if self.module.params['hardware']:
1128                num_cpus = self.module.params['hardware'].get('num_cpus')
1129
1130                if num_cpus is not None:
1131                    # Kept for compatibility with older Ansible versions that
1132                    # do not support subargument specs.
1133                    try:
1134                        num_cpus = int(num_cpus)
1135                    except ValueError as e:
1136                        self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be an integer value!")
1137
1138                    if num_cpus < 1:
1139                        self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be greater than zero!")
1140
1141                    # We can use VCPUs_at_startup or VCPUs_max parameter. I'd
1142                    # say the former is the way to go but this needs
1143                    # confirmation and testing.
1144                    if num_cpus != int(self.vm_params['VCPUs_at_startup']):
1145                        config_changes_hardware.append('num_cpus')
1146                        # For now, we don't support hotpluging so VM has to be in
1147                        # poweredoff state to reconfigure.
1148                        need_poweredoff = True
1149
1150                num_cpu_cores_per_socket = self.module.params['hardware'].get('num_cpu_cores_per_socket')
1151
1152                if num_cpu_cores_per_socket is not None:
1153                    # Kept for compatibility with older Ansible versions that
1154                    # do not support subargument specs.
1155                    try:
1156                        num_cpu_cores_per_socket = int(num_cpu_cores_per_socket)
1157                    except ValueError as e:
1158                        self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be an integer value!")
1159
1160                    if num_cpu_cores_per_socket < 1:
1161                        self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be greater than zero!")
1162
1163                    if num_cpus and num_cpus % num_cpu_cores_per_socket != 0:
1164                        self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be a multiple of hardware.num_cpu_cores_per_socket!")
1165
1166                    vm_platform = self.vm_params['platform']
1167                    vm_cores_per_socket = int(vm_platform.get('cores-per-socket', 1))
1168
1169                    if num_cpu_cores_per_socket != vm_cores_per_socket:
1170                        config_changes_hardware.append('num_cpu_cores_per_socket')
1171                        # For now, we don't support hotpluging so VM has to be
1172                        # in poweredoff state to reconfigure.
1173                        need_poweredoff = True
1174
1175                memory_mb = self.module.params['hardware'].get('memory_mb')
1176
1177                if memory_mb is not None:
1178                    # Kept for compatibility with older Ansible versions that
1179                    # do not support subargument specs.
1180                    try:
1181                        memory_mb = int(memory_mb)
1182                    except ValueError as e:
1183                        self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be an integer value!")
1184
1185                    if memory_mb < 1:
1186                        self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be greater than zero!")
1187
1188                    # There are multiple memory parameters:
1189                    #     - memory_dynamic_max
1190                    #     - memory_dynamic_min
1191                    #     - memory_static_max
1192                    #     - memory_static_min
1193                    #     - memory_target
1194                    #
1195                    # memory_target seems like a good candidate but it returns 0 for
1196                    # halted VMs so we can't use it.
1197                    #
1198                    # I decided to use memory_dynamic_max and memory_static_max
1199                    # and use whichever is larger. This strategy needs validation
1200                    # and testing.
1201                    #
1202                    # XenServer stores memory size in bytes so we need to divide
1203                    # it by 1024*1024 = 1048576.
1204                    if memory_mb != int(max(int(self.vm_params['memory_dynamic_max']), int(self.vm_params['memory_static_max'])) / 1048576):
1205                        config_changes_hardware.append('memory_mb')
1206                        # For now, we don't support hotpluging so VM has to be in
1207                        # poweredoff state to reconfigure.
1208                        need_poweredoff = True
1209
1210            if config_changes_hardware:
1211                config_changes.append({"hardware": config_changes_hardware})
1212
1213            config_changes_disks = []
1214            config_new_disks = []
1215
1216            # Find allowed userdevices.
1217            vbd_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)
1218
1219            if self.module.params['disks']:
1220                # Get the list of all disk. Filter out any CDs found.
1221                vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"]
1222
1223                # Number of disks defined in module params have to be same or
1224                # higher than a number of existing disks attached to the VM.
1225                # We don't support removal or detachment of disks.
1226                if len(self.module.params['disks']) < len(vm_disk_params_list):
1227                    self.module.fail_json(msg="VM check disks: provided disks configuration has less disks than the target VM (%d < %d)!" %
1228                                          (len(self.module.params['disks']), len(vm_disk_params_list)))
1229
1230                # Find the highest disk occupied userdevice.
1231                if not vm_disk_params_list:
1232                    vm_disk_userdevice_highest = "-1"
1233                else:
1234                    vm_disk_userdevice_highest = vm_disk_params_list[-1]['userdevice']
1235
1236                for position in range(len(self.module.params['disks'])):
1237                    if position < len(vm_disk_params_list):
1238                        vm_disk_params = vm_disk_params_list[position]
1239                    else:
1240                        vm_disk_params = None
1241
1242                    disk_params = self.module.params['disks'][position]
1243
1244                    disk_size = self.get_normalized_disk_size(self.module.params['disks'][position], "VM check disks[%s]: " % position)
1245
1246                    disk_name = disk_params.get('name')
1247
1248                    if disk_name is not None and not disk_name:
1249                        self.module.fail_json(msg="VM check disks[%s]: disk name cannot be an empty string!" % position)
1250
1251                    # If this is an existing disk.
1252                    if vm_disk_params and vm_disk_params['VDI']:
1253                        disk_changes = []
1254
1255                        if disk_name and disk_name != vm_disk_params['VDI']['name_label']:
1256                            disk_changes.append('name')
1257
1258                        disk_name_desc = disk_params.get('name_desc')
1259
1260                        if disk_name_desc is not None and disk_name_desc != vm_disk_params['VDI']['name_description']:
1261                            disk_changes.append('name_desc')
1262
1263                        if disk_size:
1264                            if disk_size > int(vm_disk_params['VDI']['virtual_size']):
1265                                disk_changes.append('size')
1266                                need_poweredoff = True
1267                            elif disk_size < int(vm_disk_params['VDI']['virtual_size']):
1268                                self.module.fail_json(msg="VM check disks[%s]: disk size is smaller than existing (%d bytes < %s bytes). "
1269                                                      "Reducing disk size is not allowed!" % (position, disk_size, vm_disk_params['VDI']['virtual_size']))
1270
1271                        config_changes_disks.append(disk_changes)
1272                    # If this is a new disk.
1273                    else:
1274                        if not disk_size:
1275                            self.module.fail_json(msg="VM check disks[%s]: no valid disk size specification found!" % position)
1276
1277                        disk_sr_uuid = disk_params.get('sr_uuid')
1278                        disk_sr = disk_params.get('sr')
1279
1280                        if disk_sr_uuid is not None or disk_sr is not None:
1281                            # Check existance only. Ignore return value.
1282                            get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True,
1283                                           msg_prefix="VM check disks[%s]: " % position)
1284                        elif self.default_sr_ref == 'OpaqueRef:NULL':
1285                            self.module.fail_json(msg="VM check disks[%s]: no default SR found! You must specify SR explicitly." % position)
1286
1287                        if not vbd_userdevices_allowed:
1288                            self.module.fail_json(msg="VM check disks[%s]: maximum number of devices reached!" % position)
1289
1290                        disk_userdevice = None
1291
1292                        # We need to place a new disk right above the highest
1293                        # placed existing disk to maintain relative disk
1294                        # positions pairable with disk specifications in
1295                        # module params. That place must not be occupied by
1296                        # some other device like CD-ROM.
1297                        for userdevice in vbd_userdevices_allowed:
1298                            if int(userdevice) > int(vm_disk_userdevice_highest):
1299                                disk_userdevice = userdevice
1300                                vbd_userdevices_allowed.remove(userdevice)
1301                                vm_disk_userdevice_highest = userdevice
1302                                break
1303
1304                        # If no place was found.
1305                        if disk_userdevice is None:
1306                            # Highest occupied place could be a CD-ROM device
1307                            # so we have to include all devices regardless of
1308                            # type when calculating out-of-bound position.
1309                            disk_userdevice = str(int(self.vm_params['VBDs'][-1]['userdevice']) + 1)
1310                            self.module.fail_json(msg="VM check disks[%s]: new disk position %s is out of bounds!" % (position, disk_userdevice))
1311
1312                        # For new disks we only track their position.
1313                        config_new_disks.append((position, disk_userdevice))
1314
1315            # We should append config_changes_disks to config_changes only
1316            # if there is at least one changed disk, else skip.
1317            for disk_change in config_changes_disks:
1318                if disk_change:
1319                    config_changes.append({"disks_changed": config_changes_disks})
1320                    break
1321
1322            if config_new_disks:
1323                config_changes.append({"disks_new": config_new_disks})
1324
1325            config_changes_cdrom = []
1326
1327            if self.module.params['cdrom']:
1328                # Get the list of all CD-ROMs. Filter out any regular disks
1329                # found. If we found no existing CD-ROM, we will create it
1330                # later else take the first one found.
1331                vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"]
1332
1333                # If no existing CD-ROM is found, we will need to add one.
1334                # We need to check if there is any userdevice allowed.
1335                if not vm_cdrom_params_list and not vbd_userdevices_allowed:
1336                    self.module.fail_json(msg="VM check cdrom: maximum number of devices reached!")
1337
1338                cdrom_type = self.module.params['cdrom'].get('type')
1339                cdrom_iso_name = self.module.params['cdrom'].get('iso_name')
1340
1341                # If cdrom.iso_name is specified but cdrom.type is not,
1342                # then set cdrom.type to 'iso', unless cdrom.iso_name is
1343                # an empty string, in that case set cdrom.type to 'none'.
1344                if not cdrom_type:
1345                    if cdrom_iso_name:
1346                        cdrom_type = "iso"
1347                    elif cdrom_iso_name is not None:
1348                        cdrom_type = "none"
1349
1350                    self.module.params['cdrom']['type'] = cdrom_type
1351
1352                # If type changed.
1353                if cdrom_type and (not vm_cdrom_params_list or cdrom_type != self.get_cdrom_type(vm_cdrom_params_list[0])):
1354                    config_changes_cdrom.append('type')
1355
1356                if cdrom_type == "iso":
1357                    # Check if ISO exists.
1358                    # Check existance only. Ignore return value.
1359                    get_object_ref(self.module, cdrom_iso_name, uuid=None, obj_type="ISO image", fail=True,
1360                                   msg_prefix="VM check cdrom.iso_name: ")
1361
1362                    # Is ISO image changed?
1363                    if (cdrom_iso_name and
1364                            (not vm_cdrom_params_list or
1365                             not vm_cdrom_params_list[0]['VDI'] or
1366                             cdrom_iso_name != vm_cdrom_params_list[0]['VDI']['name_label'])):
1367                        config_changes_cdrom.append('iso_name')
1368
1369            if config_changes_cdrom:
1370                config_changes.append({"cdrom": config_changes_cdrom})
1371
1372            config_changes_networks = []
1373            config_new_networks = []
1374
1375            # Find allowed devices.
1376            vif_devices_allowed = self.xapi_session.xenapi.VM.get_allowed_VIF_devices(self.vm_ref)
1377
1378            if self.module.params['networks']:
1379                # Number of VIFs defined in module params have to be same or
1380                # higher than a number of existing VIFs attached to the VM.
1381                # We don't support removal of VIFs.
1382                if len(self.module.params['networks']) < len(self.vm_params['VIFs']):
1383                    self.module.fail_json(msg="VM check networks: provided networks configuration has less interfaces than the target VM (%d < %d)!" %
1384                                          (len(self.module.params['networks']), len(self.vm_params['VIFs'])))
1385
1386                # Find the highest occupied device.
1387                if not self.vm_params['VIFs']:
1388                    vif_device_highest = "-1"
1389                else:
1390                    vif_device_highest = self.vm_params['VIFs'][-1]['device']
1391
1392                for position in range(len(self.module.params['networks'])):
1393                    if position < len(self.vm_params['VIFs']):
1394                        vm_vif_params = self.vm_params['VIFs'][position]
1395                    else:
1396                        vm_vif_params = None
1397
1398                    network_params = self.module.params['networks'][position]
1399
1400                    network_name = network_params.get('name')
1401
1402                    if network_name is not None and not network_name:
1403                        self.module.fail_json(msg="VM check networks[%s]: network name cannot be an empty string!" % position)
1404
1405                    if network_name:
1406                        # Check existance only. Ignore return value.
1407                        get_object_ref(self.module, network_name, uuid=None, obj_type="network", fail=True,
1408                                       msg_prefix="VM check networks[%s]: " % position)
1409
1410                    network_mac = network_params.get('mac')
1411
1412                    if network_mac is not None:
1413                        network_mac = network_mac.lower()
1414
1415                        if not is_mac(network_mac):
1416                            self.module.fail_json(msg="VM check networks[%s]: specified MAC address '%s' is not valid!" % (position, network_mac))
1417
1418                    # IPv4 reconfiguration.
1419                    network_type = network_params.get('type')
1420                    network_ip = network_params.get('ip')
1421                    network_netmask = network_params.get('netmask')
1422                    network_prefix = None
1423
1424                    # If networks.ip is specified and networks.type is not,
1425                    # then set networks.type to 'static'.
1426                    if not network_type and network_ip:
1427                        network_type = "static"
1428
1429                    # XenServer natively supports only 'none' and 'static'
1430                    # type with 'none' being the same as 'dhcp'.
1431                    if self.vm_params['customization_agent'] == "native" and network_type and network_type == "dhcp":
1432                        network_type = "none"
1433
1434                    if network_type and network_type == "static":
1435                        if network_ip is not None:
1436                            network_ip_split = network_ip.split('/')
1437                            network_ip = network_ip_split[0]
1438
1439                            if network_ip and not is_valid_ip_addr(network_ip):
1440                                self.module.fail_json(msg="VM check networks[%s]: specified IPv4 address '%s' is not valid!" % (position, network_ip))
1441
1442                            if len(network_ip_split) > 1:
1443                                network_prefix = network_ip_split[1]
1444
1445                                if not is_valid_ip_prefix(network_prefix):
1446                                    self.module.fail_json(msg="VM check networks[%s]: specified IPv4 prefix '%s' is not valid!" % (position, network_prefix))
1447
1448                        if network_netmask is not None:
1449                            if not is_valid_ip_netmask(network_netmask):
1450                                self.module.fail_json(msg="VM check networks[%s]: specified IPv4 netmask '%s' is not valid!" % (position, network_netmask))
1451
1452                            network_prefix = ip_netmask_to_prefix(network_netmask, skip_check=True)
1453                        elif network_prefix is not None:
1454                            network_netmask = ip_prefix_to_netmask(network_prefix, skip_check=True)
1455
1456                    # If any parameter is overridden at this point, update it.
1457                    if network_type:
1458                        network_params['type'] = network_type
1459
1460                    if network_ip:
1461                        network_params['ip'] = network_ip
1462
1463                    if network_netmask:
1464                        network_params['netmask'] = network_netmask
1465
1466                    if network_prefix:
1467                        network_params['prefix'] = network_prefix
1468
1469                    network_gateway = network_params.get('gateway')
1470
1471                    # Gateway can be an empty string (when removing gateway
1472                    # configuration) but if it is not, it should be validated.
1473                    if network_gateway and not is_valid_ip_addr(network_gateway):
1474                        self.module.fail_json(msg="VM check networks[%s]: specified IPv4 gateway '%s' is not valid!" % (position, network_gateway))
1475
1476                    # IPv6 reconfiguration.
1477                    network_type6 = network_params.get('type6')
1478                    network_ip6 = network_params.get('ip6')
1479                    network_prefix6 = None
1480
1481                    # If networks.ip6 is specified and networks.type6 is not,
1482                    # then set networks.type6 to 'static'.
1483                    if not network_type6 and network_ip6:
1484                        network_type6 = "static"
1485
1486                    # XenServer natively supports only 'none' and 'static'
1487                    # type with 'none' being the same as 'dhcp'.
1488                    if self.vm_params['customization_agent'] == "native" and network_type6 and network_type6 == "dhcp":
1489                        network_type6 = "none"
1490
1491                    if network_type6 and network_type6 == "static":
1492                        if network_ip6 is not None:
1493                            network_ip6_split = network_ip6.split('/')
1494                            network_ip6 = network_ip6_split[0]
1495
1496                            if network_ip6 and not is_valid_ip6_addr(network_ip6):
1497                                self.module.fail_json(msg="VM check networks[%s]: specified IPv6 address '%s' is not valid!" % (position, network_ip6))
1498
1499                            if len(network_ip6_split) > 1:
1500                                network_prefix6 = network_ip6_split[1]
1501
1502                                if not is_valid_ip6_prefix(network_prefix6):
1503                                    self.module.fail_json(msg="VM check networks[%s]: specified IPv6 prefix '%s' is not valid!" % (position, network_prefix6))
1504
1505                    # If any parameter is overridden at this point, update it.
1506                    if network_type6:
1507                        network_params['type6'] = network_type6
1508
1509                    if network_ip6:
1510                        network_params['ip6'] = network_ip6
1511
1512                    if network_prefix6:
1513                        network_params['prefix6'] = network_prefix6
1514
1515                    network_gateway6 = network_params.get('gateway6')
1516
1517                    # Gateway can be an empty string (when removing gateway
1518                    # configuration) but if it is not, it should be validated.
1519                    if network_gateway6 and not is_valid_ip6_addr(network_gateway6):
1520                        self.module.fail_json(msg="VM check networks[%s]: specified IPv6 gateway '%s' is not valid!" % (position, network_gateway6))
1521
1522                    # If this is an existing VIF.
1523                    if vm_vif_params and vm_vif_params['network']:
1524                        network_changes = []
1525
1526                        if network_name and network_name != vm_vif_params['network']['name_label']:
1527                            network_changes.append('name')
1528
1529                        if network_mac and network_mac != vm_vif_params['MAC'].lower():
1530                            network_changes.append('mac')
1531
1532                        if self.vm_params['customization_agent'] == "native":
1533                            if network_type and network_type != vm_vif_params['ipv4_configuration_mode'].lower():
1534                                network_changes.append('type')
1535
1536                            if network_type and network_type == "static":
1537                                if network_ip and (not vm_vif_params['ipv4_addresses'] or
1538                                                   not vm_vif_params['ipv4_addresses'][0] or
1539                                                   network_ip != vm_vif_params['ipv4_addresses'][0].split('/')[0]):
1540                                    network_changes.append('ip')
1541
1542                                if network_prefix and (not vm_vif_params['ipv4_addresses'] or
1543                                                       not vm_vif_params['ipv4_addresses'][0] or
1544                                                       network_prefix != vm_vif_params['ipv4_addresses'][0].split('/')[1]):
1545                                    network_changes.append('prefix')
1546                                    network_changes.append('netmask')
1547
1548                                if network_gateway is not None and network_gateway != vm_vif_params['ipv4_gateway']:
1549                                    network_changes.append('gateway')
1550
1551                            if network_type6 and network_type6 != vm_vif_params['ipv6_configuration_mode'].lower():
1552                                network_changes.append('type6')
1553
1554                            if network_type6 and network_type6 == "static":
1555                                if network_ip6 and (not vm_vif_params['ipv6_addresses'] or
1556                                                    not vm_vif_params['ipv6_addresses'][0] or
1557                                                    network_ip6 != vm_vif_params['ipv6_addresses'][0].split('/')[0]):
1558                                    network_changes.append('ip6')
1559
1560                                if network_prefix6 and (not vm_vif_params['ipv6_addresses'] or
1561                                                        not vm_vif_params['ipv6_addresses'][0] or
1562                                                        network_prefix6 != vm_vif_params['ipv6_addresses'][0].split('/')[1]):
1563                                    network_changes.append('prefix6')
1564
1565                                if network_gateway6 is not None and network_gateway6 != vm_vif_params['ipv6_gateway']:
1566                                    network_changes.append('gateway6')
1567
1568                        elif self.vm_params['customization_agent'] == "custom":
1569                            vm_xenstore_data = self.vm_params['xenstore_data']
1570
1571                            if network_type and network_type != vm_xenstore_data.get('vm-data/networks/%s/type' % vm_vif_params['device'], "none"):
1572                                network_changes.append('type')
1573                                need_poweredoff = True
1574
1575                            if network_type and network_type == "static":
1576                                if network_ip and network_ip != vm_xenstore_data.get('vm-data/networks/%s/ip' % vm_vif_params['device'], ""):
1577                                    network_changes.append('ip')
1578                                    need_poweredoff = True
1579
1580                                if network_prefix and network_prefix != vm_xenstore_data.get('vm-data/networks/%s/prefix' % vm_vif_params['device'], ""):
1581                                    network_changes.append('prefix')
1582                                    network_changes.append('netmask')
1583                                    need_poweredoff = True
1584
1585                                if network_gateway is not None and network_gateway != vm_xenstore_data.get('vm-data/networks/%s/gateway' %
1586                                                                                                           vm_vif_params['device'], ""):
1587                                    network_changes.append('gateway')
1588                                    need_poweredoff = True
1589
1590                            if network_type6 and network_type6 != vm_xenstore_data.get('vm-data/networks/%s/type6' % vm_vif_params['device'], "none"):
1591                                network_changes.append('type6')
1592                                need_poweredoff = True
1593
1594                            if network_type6 and network_type6 == "static":
1595                                if network_ip6 and network_ip6 != vm_xenstore_data.get('vm-data/networks/%s/ip6' % vm_vif_params['device'], ""):
1596                                    network_changes.append('ip6')
1597                                    need_poweredoff = True
1598
1599                                if network_prefix6 and network_prefix6 != vm_xenstore_data.get('vm-data/networks/%s/prefix6' % vm_vif_params['device'], ""):
1600                                    network_changes.append('prefix6')
1601                                    need_poweredoff = True
1602
1603                                if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get('vm-data/networks/%s/gateway6' %
1604                                                                                                             vm_vif_params['device'], ""):
1605                                    network_changes.append('gateway6')
1606                                    need_poweredoff = True
1607
1608                        config_changes_networks.append(network_changes)
1609                    # If this is a new VIF.
1610                    else:
1611                        if not network_name:
1612                            self.module.fail_json(msg="VM check networks[%s]: network name is required for new network interface!" % position)
1613
1614                        if network_type and network_type == "static" and network_ip and not network_netmask:
1615                            self.module.fail_json(msg="VM check networks[%s]: IPv4 netmask or prefix is required for new network interface!" % position)
1616
1617                        if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6:
1618                            self.module.fail_json(msg="VM check networks[%s]: IPv6 prefix is required for new network interface!" % position)
1619
1620                        # Restart is needed if we are adding new network
1621                        # interface with IP/gateway parameters specified
1622                        # and custom agent is used.
1623                        if self.vm_params['customization_agent'] == "custom":
1624                            for parameter in ['type', 'ip', 'prefix', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']:
1625                                if network_params.get(parameter):
1626                                    need_poweredoff = True
1627                                    break
1628
1629                        if not vif_devices_allowed:
1630                            self.module.fail_json(msg="VM check networks[%s]: maximum number of network interfaces reached!" % position)
1631
1632                        # We need to place a new network interface right above the
1633                        # highest placed existing interface to maintain relative
1634                        # positions pairable with network interface specifications
1635                        # in module params.
1636                        vif_device = str(int(vif_device_highest) + 1)
1637
1638                        if vif_device not in vif_devices_allowed:
1639                            self.module.fail_json(msg="VM check networks[%s]: new network interface position %s is out of bounds!" % (position, vif_device))
1640
1641                        vif_devices_allowed.remove(vif_device)
1642                        vif_device_highest = vif_device
1643
1644                        # For new VIFs we only track their position.
1645                        config_new_networks.append((position, vif_device))
1646
1647            # We should append config_changes_networks to config_changes only
1648            # if there is at least one changed network, else skip.
1649            for network_change in config_changes_networks:
1650                if network_change:
1651                    config_changes.append({"networks_changed": config_changes_networks})
1652                    break
1653
1654            if config_new_networks:
1655                config_changes.append({"networks_new": config_new_networks})
1656
1657            config_changes_custom_params = []
1658
1659            if self.module.params['custom_params']:
1660                for position in range(len(self.module.params['custom_params'])):
1661                    custom_param = self.module.params['custom_params'][position]
1662
1663                    custom_param_key = custom_param['key']
1664                    custom_param_value = custom_param['value']
1665
1666                    if custom_param_key not in self.vm_params:
1667                        self.module.fail_json(msg="VM check custom_params[%s]: unknown VM param '%s'!" % (position, custom_param_key))
1668
1669                    if custom_param_value != self.vm_params[custom_param_key]:
1670                        # We only need to track custom param position.
1671                        config_changes_custom_params.append(position)
1672
1673            if config_changes_custom_params:
1674                config_changes.append({"custom_params": config_changes_custom_params})
1675
1676            if need_poweredoff:
1677                config_changes.append('need_poweredoff')
1678
1679            return config_changes
1680
1681        except XenAPI.Failure as f:
1682            self.module.fail_json(msg="XAPI ERROR: %s" % f.details)
1683
1684    def get_normalized_disk_size(self, disk_params, msg_prefix=""):
1685        """Parses disk size parameters and returns disk size in bytes.
1686
1687        This method tries to parse disk size module parameters. It fails
1688        with an error message if size cannot be parsed.
1689
1690        Args:
1691            disk_params (dist): A dictionary with disk parameters.
1692            msg_prefix (str): A string error messages should be prefixed
1693                with (default: "").
1694
1695        Returns:
1696            int: disk size in bytes if disk size is successfully parsed or
1697            None if no disk size parameters were found.
1698        """
1699        # There should be only single size spec but we make a list of all size
1700        # specs just in case. Priority is given to 'size' but if not found, we
1701        # check for 'size_tb', 'size_gb', 'size_mb' etc. and use first one
1702        # found.
1703        disk_size_spec = [x for x in disk_params.keys() if disk_params[x] is not None and (x.startswith('size_') or x == 'size')]
1704
1705        if disk_size_spec:
1706            try:
1707                # size
1708                if "size" in disk_size_spec:
1709                    size_regex = re.compile(r'(\d+(?:\.\d+)?)\s*(.*)')
1710                    disk_size_m = size_regex.match(disk_params['size'])
1711
1712                    if disk_size_m:
1713                        size = disk_size_m.group(1)
1714                        unit = disk_size_m.group(2)
1715                    else:
1716                        raise ValueError
1717                # size_tb, size_gb, size_mb, size_kb, size_b
1718                else:
1719                    size = disk_params[disk_size_spec[0]]
1720                    unit = disk_size_spec[0].split('_')[-1]
1721
1722                if not unit:
1723                    unit = "b"
1724                else:
1725                    unit = unit.lower()
1726
1727                if re.match(r'\d+\.\d+', size):
1728                    # We found float value in string, let's typecast it.
1729                    if unit == "b":
1730                        # If we found float but unit is bytes, we get the integer part only.
1731                        size = int(float(size))
1732                    else:
1733                        size = float(size)
1734                else:
1735                    # We found int value in string, let's typecast it.
1736                    size = int(size)
1737
1738                if not size or size < 0:
1739                    raise ValueError
1740
1741            except (TypeError, ValueError, NameError):
1742                # Common failure
1743                self.module.fail_json(msg="%sfailed to parse disk size! Please review value provided using documentation." % msg_prefix)
1744
1745            disk_units = dict(tb=4, gb=3, mb=2, kb=1, b=0)
1746
1747            if unit in disk_units:
1748                return int(size * (1024 ** disk_units[unit]))
1749            else:
1750                self.module.fail_json(msg="%s'%s' is not a supported unit for disk size! Supported units are ['%s']." %
1751                                      (msg_prefix, unit, "', '".join(sorted(disk_units.keys(), key=lambda key: disk_units[key]))))
1752        else:
1753            return None
1754
1755    @staticmethod
1756    def get_cdrom_type(vm_cdrom_params):
1757        """Returns VM CD-ROM type."""
1758        # TODO: implement support for detecting type host. No server to test
1759        # this on at the moment.
1760        if vm_cdrom_params['empty']:
1761            return "none"
1762        else:
1763            return "iso"
1764
1765
1766def main():
1767    argument_spec = xenserver_common_argument_spec()
1768    argument_spec.update(
1769        state=dict(type='str', default='present',
1770                   choices=['present', 'absent', 'poweredon']),
1771        name=dict(type='str', aliases=['name_label']),
1772        name_desc=dict(type='str'),
1773        uuid=dict(type='str'),
1774        template=dict(type='str', aliases=['template_src']),
1775        template_uuid=dict(type='str'),
1776        is_template=dict(type='bool', default=False),
1777        folder=dict(type='str'),
1778        hardware=dict(
1779            type='dict',
1780            options=dict(
1781                num_cpus=dict(type='int'),
1782                num_cpu_cores_per_socket=dict(type='int'),
1783                memory_mb=dict(type='int'),
1784            ),
1785        ),
1786        disks=dict(
1787            type='list',
1788            elements='dict',
1789            options=dict(
1790                size=dict(type='str'),
1791                size_tb=dict(type='str'),
1792                size_gb=dict(type='str'),
1793                size_mb=dict(type='str'),
1794                size_kb=dict(type='str'),
1795                size_b=dict(type='str'),
1796                name=dict(type='str', aliases=['name_label']),
1797                name_desc=dict(type='str'),
1798                sr=dict(type='str'),
1799                sr_uuid=dict(type='str'),
1800            ),
1801            aliases=['disk'],
1802            mutually_exclusive=[
1803                ['size', 'size_tb', 'size_gb', 'size_mb', 'size_kb', 'size_b'],
1804                ['sr', 'sr_uuid'],
1805            ],
1806        ),
1807        cdrom=dict(
1808            type='dict',
1809            options=dict(
1810                type=dict(type='str', choices=['none', 'iso']),
1811                iso_name=dict(type='str'),
1812            ),
1813            required_if=[
1814                ['type', 'iso', ['iso_name']],
1815            ],
1816        ),
1817        networks=dict(
1818            type='list',
1819            elements='dict',
1820            options=dict(
1821                name=dict(type='str', aliases=['name_label']),
1822                mac=dict(type='str'),
1823                type=dict(type='str', choices=['none', 'dhcp', 'static']),
1824                ip=dict(type='str'),
1825                netmask=dict(type='str'),
1826                gateway=dict(type='str'),
1827                type6=dict(type='str', choices=['none', 'dhcp', 'static']),
1828                ip6=dict(type='str'),
1829                gateway6=dict(type='str'),
1830            ),
1831            aliases=['network'],
1832            required_if=[
1833                ['type', 'static', ['ip']],
1834                ['type6', 'static', ['ip6']],
1835            ],
1836        ),
1837        home_server=dict(type='str'),
1838        custom_params=dict(
1839            type='list',
1840            elements='dict',
1841            options=dict(
1842                key=dict(type='str', required=True),
1843                value=dict(type='raw', required=True),
1844            ),
1845        ),
1846        wait_for_ip_address=dict(type='bool', default=False),
1847        state_change_timeout=dict(type='int', default=0),
1848        linked_clone=dict(type='bool', default=False),
1849        force=dict(type='bool', default=False),
1850    )
1851
1852    module = AnsibleModule(argument_spec=argument_spec,
1853                           supports_check_mode=True,
1854                           required_one_of=[
1855                               ['name', 'uuid'],
1856                           ],
1857                           mutually_exclusive=[
1858                               ['template', 'template_uuid'],
1859                           ],
1860                           )
1861
1862    result = {'failed': False, 'changed': False}
1863
1864    vm = XenServerVM(module)
1865
1866    # Find existing VM
1867    if vm.exists():
1868        if module.params['state'] == "absent":
1869            vm.destroy()
1870            result['changed'] = True
1871        elif module.params['state'] == "present":
1872            config_changes = vm.reconfigure()
1873
1874            if config_changes:
1875                result['changed'] = True
1876
1877                # Make new disk and network changes more user friendly
1878                # and informative.
1879                for change in config_changes:
1880                    if isinstance(change, dict):
1881                        if change.get('disks_new'):
1882                            disks_new = []
1883
1884                            for position, userdevice in change['disks_new']:
1885                                disk_new_params = {"position": position, "vbd_userdevice": userdevice}
1886                                disk_params = module.params['disks'][position]
1887
1888                                for k in disk_params.keys():
1889                                    if disk_params[k] is not None:
1890                                        disk_new_params[k] = disk_params[k]
1891
1892                                disks_new.append(disk_new_params)
1893
1894                            if disks_new:
1895                                change['disks_new'] = disks_new
1896
1897                        elif change.get('networks_new'):
1898                            networks_new = []
1899
1900                            for position, device in change['networks_new']:
1901                                network_new_params = {"position": position, "vif_device": device}
1902                                network_params = module.params['networks'][position]
1903
1904                                for k in network_params.keys():
1905                                    if network_params[k] is not None:
1906                                        network_new_params[k] = network_params[k]
1907
1908                                networks_new.append(network_new_params)
1909
1910                            if networks_new:
1911                                change['networks_new'] = networks_new
1912
1913            result['changes'] = config_changes
1914
1915        elif module.params['state'] in ["poweredon", "poweredoff", "restarted", "shutdownguest", "rebootguest", "suspended"]:
1916            result['changed'] = vm.set_power_state(module.params['state'])
1917    elif module.params['state'] != "absent":
1918        vm.deploy()
1919        result['changed'] = True
1920
1921    if module.params['wait_for_ip_address'] and module.params['state'] != "absent":
1922        vm.wait_for_ip_address()
1923
1924    result['instance'] = vm.gather_facts()
1925
1926    if result['failed']:
1927        module.fail_json(**result)
1928    else:
1929        module.exit_json(**result)
1930
1931
1932if __name__ == '__main__':
1933    main()
1934