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