1# -*- coding: utf-8 -*-
2#
3# Copyright: (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9import atexit
10import time
11import re
12import traceback
13
14XENAPI_IMP_ERR = None
15try:
16    import XenAPI
17    HAS_XENAPI = True
18except ImportError:
19    HAS_XENAPI = False
20    XENAPI_IMP_ERR = traceback.format_exc()
21
22from ansible.module_utils.basic import env_fallback, missing_required_lib
23from ansible.module_utils.ansible_release import __version__ as ANSIBLE_VERSION
24
25
26def xenserver_common_argument_spec():
27    return dict(
28        hostname=dict(type='str',
29                      aliases=['host', 'pool'],
30                      required=False,
31                      default='localhost',
32                      fallback=(env_fallback, ['XENSERVER_HOST']),
33                      ),
34        username=dict(type='str',
35                      aliases=['user', 'admin'],
36                      required=False,
37                      default='root',
38                      fallback=(env_fallback, ['XENSERVER_USER'])),
39        password=dict(type='str',
40                      aliases=['pass', 'pwd'],
41                      required=False,
42                      no_log=True,
43                      fallback=(env_fallback, ['XENSERVER_PASSWORD'])),
44        validate_certs=dict(type='bool',
45                            required=False,
46                            default=True,
47                            fallback=(env_fallback, ['XENSERVER_VALIDATE_CERTS'])),
48    )
49
50
51def xapi_to_module_vm_power_state(power_state):
52    """Maps XAPI VM power states to module VM power states."""
53    module_power_state_map = {
54        "running": "poweredon",
55        "halted": "poweredoff",
56        "suspended": "suspended",
57        "paused": "paused"
58    }
59
60    return module_power_state_map.get(power_state)
61
62
63def module_to_xapi_vm_power_state(power_state):
64    """Maps module VM power states to XAPI VM power states."""
65    vm_power_state_map = {
66        "poweredon": "running",
67        "poweredoff": "halted",
68        "restarted": "running",
69        "suspended": "suspended",
70        "shutdownguest": "halted",
71        "rebootguest": "running",
72    }
73
74    return vm_power_state_map.get(power_state)
75
76
77def is_valid_ip_addr(ip_addr):
78    """Validates given string as IPv4 address for given string.
79
80    Args:
81        ip_addr (str): string to validate as IPv4 address.
82
83    Returns:
84        bool: True if string is valid IPv4 address, else False.
85    """
86    ip_addr_split = ip_addr.split('.')
87
88    if len(ip_addr_split) != 4:
89        return False
90
91    for ip_addr_octet in ip_addr_split:
92        if not ip_addr_octet.isdigit():
93            return False
94
95        ip_addr_octet_int = int(ip_addr_octet)
96
97        if ip_addr_octet_int < 0 or ip_addr_octet_int > 255:
98            return False
99
100    return True
101
102
103def is_valid_ip_netmask(ip_netmask):
104    """Validates given string as IPv4 netmask.
105
106    Args:
107        ip_netmask (str): string to validate as IPv4 netmask.
108
109    Returns:
110        bool: True if string is valid IPv4 netmask, else False.
111    """
112    ip_netmask_split = ip_netmask.split('.')
113
114    if len(ip_netmask_split) != 4:
115        return False
116
117    valid_octet_values = ['0', '128', '192', '224', '240', '248', '252', '254', '255']
118
119    for ip_netmask_octet in ip_netmask_split:
120        if ip_netmask_octet not in valid_octet_values:
121            return False
122
123    if ip_netmask_split[0] != '255' and (ip_netmask_split[1] != '0' or ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'):
124        return False
125    elif ip_netmask_split[1] != '255' and (ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'):
126        return False
127    elif ip_netmask_split[2] != '255' and ip_netmask_split[3] != '0':
128        return False
129
130    return True
131
132
133def is_valid_ip_prefix(ip_prefix):
134    """Validates given string as IPv4 prefix.
135
136    Args:
137        ip_prefix (str): string to validate as IPv4 prefix.
138
139    Returns:
140        bool: True if string is valid IPv4 prefix, else False.
141    """
142    if not ip_prefix.isdigit():
143        return False
144
145    ip_prefix_int = int(ip_prefix)
146
147    if ip_prefix_int < 0 or ip_prefix_int > 32:
148        return False
149
150    return True
151
152
153def ip_prefix_to_netmask(ip_prefix, skip_check=False):
154    """Converts IPv4 prefix to netmask.
155
156    Args:
157        ip_prefix (str): IPv4 prefix to convert.
158        skip_check (bool): Skip validation of IPv4 prefix
159            (default: False). Use if you are sure IPv4 prefix is valid.
160
161    Returns:
162        str: IPv4 netmask equivalent to given IPv4 prefix if
163        IPv4 prefix is valid, else an empty string.
164    """
165    if skip_check:
166        ip_prefix_valid = True
167    else:
168        ip_prefix_valid = is_valid_ip_prefix(ip_prefix)
169
170    if ip_prefix_valid:
171        return '.'.join([str((0xffffffff << (32 - int(ip_prefix)) >> i) & 0xff) for i in [24, 16, 8, 0]])
172    else:
173        return ""
174
175
176def ip_netmask_to_prefix(ip_netmask, skip_check=False):
177    """Converts IPv4 netmask to prefix.
178
179    Args:
180        ip_netmask (str): IPv4 netmask to convert.
181        skip_check (bool): Skip validation of IPv4 netmask
182            (default: False). Use if you are sure IPv4 netmask is valid.
183
184    Returns:
185        str: IPv4 prefix equivalent to given IPv4 netmask if
186        IPv4 netmask is valid, else an empty string.
187    """
188    if skip_check:
189        ip_netmask_valid = True
190    else:
191        ip_netmask_valid = is_valid_ip_netmask(ip_netmask)
192
193    if ip_netmask_valid:
194        return str(sum([bin(int(i)).count("1") for i in ip_netmask.split(".")]))
195    else:
196        return ""
197
198
199def is_valid_ip6_addr(ip6_addr):
200    """Validates given string as IPv6 address.
201
202    Args:
203        ip6_addr (str): string to validate as IPv6 address.
204
205    Returns:
206        bool: True if string is valid IPv6 address, else False.
207    """
208    ip6_addr = ip6_addr.lower()
209    ip6_addr_split = ip6_addr.split(':')
210
211    if ip6_addr_split[0] == "":
212        ip6_addr_split.pop(0)
213
214    if ip6_addr_split[-1] == "":
215        ip6_addr_split.pop(-1)
216
217    if len(ip6_addr_split) > 8:
218        return False
219
220    if ip6_addr_split.count("") > 1:
221        return False
222    elif ip6_addr_split.count("") == 1:
223        ip6_addr_split.remove("")
224    else:
225        if len(ip6_addr_split) != 8:
226            return False
227
228    ip6_addr_hextet_regex = re.compile('^[0-9a-f]{1,4}$')
229
230    for ip6_addr_hextet in ip6_addr_split:
231        if not bool(ip6_addr_hextet_regex.match(ip6_addr_hextet)):
232            return False
233
234    return True
235
236
237def is_valid_ip6_prefix(ip6_prefix):
238    """Validates given string as IPv6 prefix.
239
240    Args:
241        ip6_prefix (str): string to validate as IPv6 prefix.
242
243    Returns:
244        bool: True if string is valid IPv6 prefix, else False.
245    """
246    if not ip6_prefix.isdigit():
247        return False
248
249    ip6_prefix_int = int(ip6_prefix)
250
251    if ip6_prefix_int < 0 or ip6_prefix_int > 128:
252        return False
253
254    return True
255
256
257def get_object_ref(module, name, uuid=None, obj_type="VM", fail=True, msg_prefix=""):
258    """Finds and returns a reference to arbitrary XAPI object.
259
260    An object is searched by using either name (name_label) or UUID
261    with UUID taken precedence over name.
262
263    Args:
264        module: Reference to Ansible module object.
265        name (str): Name (name_label) of an object to search for.
266        uuid (str): UUID of an object to search for.
267        obj_type (str): Any valid XAPI object type. See XAPI docs.
268        fail (bool): Should function fail with error message if object
269            is not found or exit silently (default: True). The function
270            always fails if multiple objects with same name are found.
271        msg_prefix (str): A string error messages should be prefixed
272            with (default: "").
273
274    Returns:
275        XAPI reference to found object or None if object is not found
276        and fail=False.
277    """
278    xapi_session = XAPI.connect(module)
279
280    if obj_type in ["template", "snapshot"]:
281        real_obj_type = "VM"
282    elif obj_type == "home server":
283        real_obj_type = "host"
284    elif obj_type == "ISO image":
285        real_obj_type = "VDI"
286    else:
287        real_obj_type = obj_type
288
289    obj_ref = None
290
291    # UUID has precedence over name.
292    if uuid:
293        try:
294            # Find object by UUID. If no object is found using given UUID,
295            # an exception will be generated.
296            obj_ref = xapi_session.xenapi_request("%s.get_by_uuid" % real_obj_type, (uuid,))
297        except XenAPI.Failure as f:
298            if fail:
299                module.fail_json(msg="%s%s with UUID '%s' not found!" % (msg_prefix, obj_type, uuid))
300    elif name:
301        try:
302            # Find object by name (name_label).
303            obj_ref_list = xapi_session.xenapi_request("%s.get_by_name_label" % real_obj_type, (name,))
304        except XenAPI.Failure as f:
305            module.fail_json(msg="XAPI ERROR: %s" % f.details)
306
307        # If obj_ref_list is empty.
308        if not obj_ref_list:
309            if fail:
310                module.fail_json(msg="%s%s with name '%s' not found!" % (msg_prefix, obj_type, name))
311        # If obj_ref_list contains multiple object references.
312        elif len(obj_ref_list) > 1:
313            module.fail_json(msg="%smultiple %ss with name '%s' found! Please use UUID." % (msg_prefix, obj_type, name))
314        # The obj_ref_list contains only one object reference.
315        else:
316            obj_ref = obj_ref_list[0]
317    else:
318        module.fail_json(msg="%sno valid name or UUID supplied for %s!" % (msg_prefix, obj_type))
319
320    return obj_ref
321
322
323def gather_vm_params(module, vm_ref):
324    """Gathers all VM parameters available in XAPI database.
325
326    Args:
327        module: Reference to Ansible module object.
328        vm_ref (str): XAPI reference to VM.
329
330    Returns:
331        dict: VM parameters.
332    """
333    # We silently return empty vm_params if bad vm_ref was supplied.
334    if not vm_ref or vm_ref == "OpaqueRef:NULL":
335        return {}
336
337    xapi_session = XAPI.connect(module)
338
339    try:
340        vm_params = xapi_session.xenapi.VM.get_record(vm_ref)
341
342        # We need some params like affinity, VBDs, VIFs, VDIs etc. dereferenced.
343
344        # Affinity.
345        if vm_params['affinity'] != "OpaqueRef:NULL":
346            vm_affinity = xapi_session.xenapi.host.get_record(vm_params['affinity'])
347            vm_params['affinity'] = vm_affinity
348        else:
349            vm_params['affinity'] = {}
350
351        # VBDs.
352        vm_vbd_params_list = [xapi_session.xenapi.VBD.get_record(vm_vbd_ref) for vm_vbd_ref in vm_params['VBDs']]
353
354        # List of VBDs is usually sorted by userdevice but we sort just
355        # in case. We need this list sorted by userdevice so that we can
356        # make positional pairing with module.params['disks'].
357        vm_vbd_params_list = sorted(vm_vbd_params_list, key=lambda vm_vbd_params: int(vm_vbd_params['userdevice']))
358        vm_params['VBDs'] = vm_vbd_params_list
359
360        # VDIs.
361        for vm_vbd_params in vm_params['VBDs']:
362            if vm_vbd_params['VDI'] != "OpaqueRef:NULL":
363                vm_vdi_params = xapi_session.xenapi.VDI.get_record(vm_vbd_params['VDI'])
364            else:
365                vm_vdi_params = {}
366
367            vm_vbd_params['VDI'] = vm_vdi_params
368
369        # VIFs.
370        vm_vif_params_list = [xapi_session.xenapi.VIF.get_record(vm_vif_ref) for vm_vif_ref in vm_params['VIFs']]
371
372        # List of VIFs is usually sorted by device but we sort just
373        # in case. We need this list sorted by device so that we can
374        # make positional pairing with module.params['networks'].
375        vm_vif_params_list = sorted(vm_vif_params_list, key=lambda vm_vif_params: int(vm_vif_params['device']))
376        vm_params['VIFs'] = vm_vif_params_list
377
378        # Networks.
379        for vm_vif_params in vm_params['VIFs']:
380            if vm_vif_params['network'] != "OpaqueRef:NULL":
381                vm_network_params = xapi_session.xenapi.network.get_record(vm_vif_params['network'])
382            else:
383                vm_network_params = {}
384
385            vm_vif_params['network'] = vm_network_params
386
387        # Guest metrics.
388        if vm_params['guest_metrics'] != "OpaqueRef:NULL":
389            vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_params['guest_metrics'])
390            vm_params['guest_metrics'] = vm_guest_metrics
391        else:
392            vm_params['guest_metrics'] = {}
393
394        # Detect customization agent.
395        xenserver_version = get_xenserver_version(module)
396
397        if (xenserver_version[0] >= 7 and xenserver_version[1] >= 0 and vm_params.get('guest_metrics') and
398                "feature-static-ip-setting" in vm_params['guest_metrics']['other']):
399            vm_params['customization_agent'] = "native"
400        else:
401            vm_params['customization_agent'] = "custom"
402
403    except XenAPI.Failure as f:
404        module.fail_json(msg="XAPI ERROR: %s" % f.details)
405
406    return vm_params
407
408
409def gather_vm_facts(module, vm_params):
410    """Gathers VM facts.
411
412    Args:
413        module: Reference to Ansible module object.
414        vm_params (dict): A dictionary with VM parameters as returned
415            by gather_vm_params() function.
416
417    Returns:
418        dict: VM facts.
419    """
420    # We silently return empty vm_facts if no vm_params are available.
421    if not vm_params:
422        return {}
423
424    xapi_session = XAPI.connect(module)
425
426    # Gather facts.
427    vm_facts = {
428        "state": xapi_to_module_vm_power_state(vm_params['power_state'].lower()),
429        "name": vm_params['name_label'],
430        "name_desc": vm_params['name_description'],
431        "uuid": vm_params['uuid'],
432        "is_template": vm_params['is_a_template'],
433        "folder": vm_params['other_config'].get('folder', ''),
434        "hardware": {
435            "num_cpus": int(vm_params['VCPUs_max']),
436            "num_cpu_cores_per_socket": int(vm_params['platform'].get('cores-per-socket', '1')),
437            "memory_mb": int(int(vm_params['memory_dynamic_max']) / 1048576),
438        },
439        "disks": [],
440        "cdrom": {},
441        "networks": [],
442        "home_server": vm_params['affinity'].get('name_label', ''),
443        "domid": vm_params['domid'],
444        "platform": vm_params['platform'],
445        "other_config": vm_params['other_config'],
446        "xenstore_data": vm_params['xenstore_data'],
447        "customization_agent": vm_params['customization_agent'],
448    }
449
450    for vm_vbd_params in vm_params['VBDs']:
451        if vm_vbd_params['type'] == "Disk":
452            vm_disk_sr_params = xapi_session.xenapi.SR.get_record(vm_vbd_params['VDI']['SR'])
453
454            vm_disk_params = {
455                "size": int(vm_vbd_params['VDI']['virtual_size']),
456                "name": vm_vbd_params['VDI']['name_label'],
457                "name_desc": vm_vbd_params['VDI']['name_description'],
458                "sr": vm_disk_sr_params['name_label'],
459                "sr_uuid": vm_disk_sr_params['uuid'],
460                "os_device": vm_vbd_params['device'],
461                "vbd_userdevice": vm_vbd_params['userdevice'],
462            }
463
464            vm_facts['disks'].append(vm_disk_params)
465        elif vm_vbd_params['type'] == "CD":
466            if vm_vbd_params['empty']:
467                vm_facts['cdrom'].update(type="none")
468            else:
469                vm_facts['cdrom'].update(type="iso")
470                vm_facts['cdrom'].update(iso_name=vm_vbd_params['VDI']['name_label'])
471
472    for vm_vif_params in vm_params['VIFs']:
473        vm_guest_metrics_networks = vm_params['guest_metrics'].get('networks', {})
474
475        vm_network_params = {
476            "name": vm_vif_params['network']['name_label'],
477            "mac": vm_vif_params['MAC'],
478            "vif_device": vm_vif_params['device'],
479            "mtu": vm_vif_params['MTU'],
480            "ip": vm_guest_metrics_networks.get("%s/ip" % vm_vif_params['device'], ''),
481            "prefix": "",
482            "netmask": "",
483            "gateway": "",
484            "ip6": [vm_guest_metrics_networks[ipv6] for ipv6 in sorted(vm_guest_metrics_networks.keys()) if ipv6.startswith("%s/ipv6/" %
485                                                                                                                            vm_vif_params['device'])],
486            "prefix6": "",
487            "gateway6": "",
488        }
489
490        if vm_params['customization_agent'] == "native":
491            if vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]:
492                vm_network_params['prefix'] = vm_vif_params['ipv4_addresses'][0].split('/')[1]
493                vm_network_params['netmask'] = ip_prefix_to_netmask(vm_network_params['prefix'])
494
495            vm_network_params['gateway'] = vm_vif_params['ipv4_gateway']
496
497            if vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]:
498                vm_network_params['prefix6'] = vm_vif_params['ipv6_addresses'][0].split('/')[1]
499
500            vm_network_params['gateway6'] = vm_vif_params['ipv6_gateway']
501
502        elif vm_params['customization_agent'] == "custom":
503            vm_xenstore_data = vm_params['xenstore_data']
504
505            for f in ['prefix', 'netmask', 'gateway', 'prefix6', 'gateway6']:
506                vm_network_params[f] = vm_xenstore_data.get("vm-data/networks/%s/%s" % (vm_vif_params['device'], f), "")
507
508        vm_facts['networks'].append(vm_network_params)
509
510    return vm_facts
511
512
513def set_vm_power_state(module, vm_ref, power_state, timeout=300):
514    """Controls VM power state.
515
516    Args:
517        module: Reference to Ansible module object.
518        vm_ref (str): XAPI reference to VM.
519        power_state (str): Power state to put VM into. Accepted values:
520
521            - poweredon
522            - poweredoff
523            - restarted
524            - suspended
525            - shutdownguest
526            - rebootguest
527
528        timeout (int): timeout in seconds (default: 300).
529
530    Returns:
531        tuple (bool, str): Bool element is True if VM power state has
532        changed by calling this function, else False. Str element carries
533        a value of resulting power state as defined by XAPI - 'running',
534        'halted' or 'suspended'.
535    """
536    # Fail if we don't have a valid VM reference.
537    if not vm_ref or vm_ref == "OpaqueRef:NULL":
538        module.fail_json(msg="Cannot set VM power state. Invalid VM reference supplied!")
539
540    xapi_session = XAPI.connect(module)
541
542    power_state = power_state.replace('_', '').replace('-', '').lower()
543    vm_power_state_resulting = module_to_xapi_vm_power_state(power_state)
544
545    state_changed = False
546
547    try:
548        # Get current state of the VM.
549        vm_power_state_current = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower())
550
551        if vm_power_state_current != power_state:
552            if power_state == "poweredon":
553                if not module.check_mode:
554                    # VM can be in either halted, suspended, paused or running state.
555                    # For VM to be in running state, start has to be called on halted,
556                    # resume on suspended and unpause on paused VM.
557                    if vm_power_state_current == "poweredoff":
558                        xapi_session.xenapi.VM.start(vm_ref, False, False)
559                    elif vm_power_state_current == "suspended":
560                        xapi_session.xenapi.VM.resume(vm_ref, False, False)
561                    elif vm_power_state_current == "paused":
562                        xapi_session.xenapi.VM.unpause(vm_ref)
563            elif power_state == "poweredoff":
564                if not module.check_mode:
565                    # hard_shutdown will halt VM regardless of current state.
566                    xapi_session.xenapi.VM.hard_shutdown(vm_ref)
567            elif power_state == "restarted":
568                # hard_reboot will restart VM only if VM is in paused or running state.
569                if vm_power_state_current in ["paused", "poweredon"]:
570                    if not module.check_mode:
571                        xapi_session.xenapi.VM.hard_reboot(vm_ref)
572                else:
573                    module.fail_json(msg="Cannot restart VM in state '%s'!" % vm_power_state_current)
574            elif power_state == "suspended":
575                # running state is required for suspend.
576                if vm_power_state_current == "poweredon":
577                    if not module.check_mode:
578                        xapi_session.xenapi.VM.suspend(vm_ref)
579                else:
580                    module.fail_json(msg="Cannot suspend VM in state '%s'!" % vm_power_state_current)
581            elif power_state == "shutdownguest":
582                # running state is required for guest shutdown.
583                if vm_power_state_current == "poweredon":
584                    if not module.check_mode:
585                        if timeout == 0:
586                            xapi_session.xenapi.VM.clean_shutdown(vm_ref)
587                        else:
588                            task_ref = xapi_session.xenapi.Async.VM.clean_shutdown(vm_ref)
589                            task_result = wait_for_task(module, task_ref, timeout)
590
591                            if task_result:
592                                module.fail_json(msg="Guest shutdown task failed: '%s'!" % task_result)
593                else:
594                    module.fail_json(msg="Cannot shutdown guest when VM is in state '%s'!" % vm_power_state_current)
595            elif power_state == "rebootguest":
596                # running state is required for guest reboot.
597                if vm_power_state_current == "poweredon":
598                    if not module.check_mode:
599                        if timeout == 0:
600                            xapi_session.xenapi.VM.clean_reboot(vm_ref)
601                        else:
602                            task_ref = xapi_session.xenapi.Async.VM.clean_reboot(vm_ref)
603                            task_result = wait_for_task(module, task_ref, timeout)
604
605                            if task_result:
606                                module.fail_json(msg="Guest reboot task failed: '%s'!" % task_result)
607                else:
608                    module.fail_json(msg="Cannot reboot guest when VM is in state '%s'!" % vm_power_state_current)
609            else:
610                module.fail_json(msg="Requested VM power state '%s' is unsupported!" % power_state)
611
612            state_changed = True
613    except XenAPI.Failure as f:
614        module.fail_json(msg="XAPI ERROR: %s" % f.details)
615
616    return (state_changed, vm_power_state_resulting)
617
618
619def wait_for_task(module, task_ref, timeout=300):
620    """Waits for async XAPI task to finish.
621
622    Args:
623        module: Reference to Ansible module object.
624        task_ref (str): XAPI reference to task.
625        timeout (int): timeout in seconds (default: 300).
626
627    Returns:
628        str: failure message on failure, else an empty string.
629    """
630    # Fail if we don't have a valid task reference.
631    if not task_ref or task_ref == "OpaqueRef:NULL":
632        module.fail_json(msg="Cannot wait for task. Invalid task reference supplied!")
633
634    xapi_session = XAPI.connect(module)
635
636    interval = 2
637
638    result = ""
639
640    # If we have to wait indefinitely, make time_left larger than 0 so we can
641    # enter while loop.
642    if timeout == 0:
643        time_left = 1
644    else:
645        time_left = timeout
646
647    try:
648        while time_left > 0:
649            task_status = xapi_session.xenapi.task.get_status(task_ref).lower()
650
651            if task_status == "pending":
652                # Task is still running.
653                time.sleep(interval)
654
655                # We decrease time_left only if we don't wait indefinitely.
656                if timeout != 0:
657                    time_left -= interval
658
659                continue
660            elif task_status == "success":
661                # Task is done.
662                break
663            else:
664                # Task failed.
665                result = task_status
666                break
667        else:
668            # We timed out.
669            result = "timeout"
670
671        xapi_session.xenapi.task.destroy(task_ref)
672    except XenAPI.Failure as f:
673        module.fail_json(msg="XAPI ERROR: %s" % f.details)
674
675    return result
676
677
678def wait_for_vm_ip_address(module, vm_ref, timeout=300):
679    """Waits for VM to acquire an IP address.
680
681    Args:
682        module: Reference to Ansible module object.
683        vm_ref (str): XAPI reference to VM.
684        timeout (int): timeout in seconds (default: 300).
685
686    Returns:
687        dict: VM guest metrics as retrieved by
688        VM_guest_metrics.get_record() XAPI method with info
689        on IP address acquired.
690    """
691    # Fail if we don't have a valid VM reference.
692    if not vm_ref or vm_ref == "OpaqueRef:NULL":
693        module.fail_json(msg="Cannot wait for VM IP address. Invalid VM reference supplied!")
694
695    xapi_session = XAPI.connect(module)
696
697    vm_guest_metrics = {}
698
699    try:
700        # We translate VM power state string so that error message can be
701        # consistent with module VM power states.
702        vm_power_state = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower())
703
704        if vm_power_state != 'poweredon':
705            module.fail_json(msg="Cannot wait for VM IP address when VM is in state '%s'!" % vm_power_state)
706
707        interval = 2
708
709        # If we have to wait indefinitely, make time_left larger than 0 so we can
710        # enter while loop.
711        if timeout == 0:
712            time_left = 1
713        else:
714            time_left = timeout
715
716        while time_left > 0:
717            vm_guest_metrics_ref = xapi_session.xenapi.VM.get_guest_metrics(vm_ref)
718
719            if vm_guest_metrics_ref != "OpaqueRef:NULL":
720                vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_guest_metrics_ref)
721                vm_ips = vm_guest_metrics['networks']
722
723                if "0/ip" in vm_ips:
724                    break
725
726            time.sleep(interval)
727
728            # We decrease time_left only if we don't wait indefinitely.
729            if timeout != 0:
730                time_left -= interval
731        else:
732            # We timed out.
733            module.fail_json(msg="Timed out waiting for VM IP address!")
734
735    except XenAPI.Failure as f:
736        module.fail_json(msg="XAPI ERROR: %s" % f.details)
737
738    return vm_guest_metrics
739
740
741def get_xenserver_version(module):
742    """Returns XenServer version.
743
744    Args:
745        module: Reference to Ansible module object.
746
747    Returns:
748        list: Element [0] is major version. Element [1] is minor version.
749        Element [2] is update number.
750    """
751    xapi_session = XAPI.connect(module)
752
753    host_ref = xapi_session.xenapi.session.get_this_host(xapi_session._session)
754
755    try:
756        xenserver_version = [int(version_number) for version_number in xapi_session.xenapi.host.get_software_version(host_ref)['product_version'].split('.')]
757    except ValueError:
758        xenserver_version = [0, 0, 0]
759
760    return xenserver_version
761
762
763class XAPI(object):
764    """Class for XAPI session management."""
765    _xapi_session = None
766
767    @classmethod
768    def connect(cls, module, disconnect_atexit=True):
769        """Establishes XAPI connection and returns session reference.
770
771        If no existing session is available, establishes a new one
772        and returns it, else returns existing one.
773
774        Args:
775            module: Reference to Ansible module object.
776            disconnect_atexit (bool): Controls if method should
777                register atexit handler to disconnect from XenServer
778                on module exit (default: True).
779
780        Returns:
781            XAPI session reference.
782        """
783        if cls._xapi_session is not None:
784            return cls._xapi_session
785
786        hostname = module.params['hostname']
787        username = module.params['username']
788        password = module.params['password']
789        ignore_ssl = not module.params['validate_certs']
790
791        if hostname == 'localhost':
792            cls._xapi_session = XenAPI.xapi_local()
793            username = ''
794            password = ''
795        else:
796            # If scheme is not specified we default to http:// because https://
797            # is problematic in most setups.
798            if not hostname.startswith("http://") and not hostname.startswith("https://"):
799                hostname = "http://%s" % hostname
800
801            try:
802                # ignore_ssl is supported in XenAPI library from XenServer 7.2
803                # SDK onward but there is no way to tell which version we
804                # are using. TypeError will be raised if ignore_ssl is not
805                # supported. Additionally, ignore_ssl requires Python 2.7.9
806                # or newer.
807                cls._xapi_session = XenAPI.Session(hostname, ignore_ssl=ignore_ssl)
808            except TypeError:
809                # Try without ignore_ssl.
810                cls._xapi_session = XenAPI.Session(hostname)
811
812            if not password:
813                password = ''
814
815        try:
816            cls._xapi_session.login_with_password(username, password, ANSIBLE_VERSION, 'Ansible')
817        except XenAPI.Failure as f:
818            module.fail_json(msg="Unable to log on to XenServer at %s as %s: %s" % (hostname, username, f.details))
819
820        # Disabling atexit should be used in special cases only.
821        if disconnect_atexit:
822            atexit.register(cls._xapi_session.logout)
823
824        return cls._xapi_session
825
826
827class XenServerObject(object):
828    """Base class for all XenServer objects.
829
830    This class contains active XAPI session reference and common
831    attributes with useful info about XenServer host/pool.
832
833    Attributes:
834        module: Reference to Ansible module object.
835        xapi_session: Reference to XAPI session.
836        pool_ref (str): XAPI reference to a pool currently connected to.
837        default_sr_ref (str): XAPI reference to a pool default
838            Storage Repository.
839        host_ref (str): XAPI rerefence to a host currently connected to.
840        xenserver_version (list of str): Contains XenServer major and
841            minor version.
842    """
843
844    def __init__(self, module):
845        """Inits XenServerObject using common module parameters.
846
847        Args:
848            module: Reference to Ansible module object.
849        """
850        if not HAS_XENAPI:
851            module.fail_json(changed=False, msg=missing_required_lib("XenAPI"), exception=XENAPI_IMP_ERR)
852
853        self.module = module
854        self.xapi_session = XAPI.connect(module)
855
856        try:
857            self.pool_ref = self.xapi_session.xenapi.pool.get_all()[0]
858            self.default_sr_ref = self.xapi_session.xenapi.pool.get_default_SR(self.pool_ref)
859            self.xenserver_version = get_xenserver_version(module)
860        except XenAPI.Failure as f:
861            self.module.fail_json(msg="XAPI ERROR: %s" % f.details)
862