1# -*- coding: utf-8 -*- 2# Copyright: (c) 2015, Joseph Callen <jcallen () csc.com> 3# Copyright: (c) 2018, Ansible Project 4# Copyright: (c) 2018, James E. King III (@jeking3) <jking@apache.org> 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 10import atexit 11import ansible.module_utils.common._collections_compat as collections_compat 12import json 13import os 14import re 15import ssl 16import time 17import traceback 18import datetime 19from collections import OrderedDict 20from distutils.version import StrictVersion 21from random import randint 22 23REQUESTS_IMP_ERR = None 24try: 25 # requests is required for exception handling of the ConnectionError 26 import requests 27 HAS_REQUESTS = True 28except ImportError: 29 REQUESTS_IMP_ERR = traceback.format_exc() 30 HAS_REQUESTS = False 31 32PYVMOMI_IMP_ERR = None 33try: 34 from pyVim import connect 35 from pyVmomi import vim, vmodl, VmomiSupport 36 HAS_PYVMOMI = True 37 HAS_PYVMOMIJSON = hasattr(VmomiSupport, 'VmomiJSONEncoder') 38except ImportError: 39 PYVMOMI_IMP_ERR = traceback.format_exc() 40 HAS_PYVMOMI = False 41 HAS_PYVMOMIJSON = False 42 43from ansible.module_utils._text import to_text, to_native 44from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from 45from ansible.module_utils.basic import env_fallback, missing_required_lib 46from ansible.module_utils.six.moves.urllib.parse import unquote 47 48 49class TaskError(Exception): 50 def __init__(self, *args, **kwargs): 51 super(TaskError, self).__init__(*args, **kwargs) 52 53 54class ApiAccessError(Exception): 55 def __init__(self, *args, **kwargs): 56 super(ApiAccessError, self).__init__(*args, **kwargs) 57 58 59def check_answer_question_status(vm): 60 """Check whether locked a virtual machine. 61 62 Args: 63 vm: Virtual machine management object 64 65 Returns: bool 66 """ 67 if hasattr(vm, "runtime") and vm.runtime.question: 68 return True 69 70 return False 71 72 73def make_answer_response(vm, answers): 74 """Make the response contents to answer against locked a virtual machine. 75 76 Args: 77 vm: Virtual machine management object 78 answers: Answer contents 79 80 Returns: Dict with answer id and number 81 Raises: TaskError on failure 82 """ 83 response_list = {} 84 for message in vm.runtime.question.message: 85 response_list[message.id] = {} 86 for choice in vm.runtime.question.choice.choiceInfo: 87 response_list[message.id].update({ 88 choice.label: choice.key 89 }) 90 91 responses = [] 92 try: 93 for answer in answers: 94 responses.append({ 95 "id": vm.runtime.question.id, 96 "response_num": response_list[answer["question"]][answer["response"]] 97 }) 98 except Exception: 99 raise TaskError("not found %s or %s or both in the response list" % (answer["question"], answer["response"])) 100 101 return responses 102 103 104def answer_question(vm, responses): 105 """Answer against the question for unlocking a virtual machine. 106 107 Args: 108 vm: Virtual machine management object 109 responses: Answer contents to unlock a virtual machine 110 """ 111 for response in responses: 112 try: 113 vm.AnswerVM(response["id"], response["response_num"]) 114 except Exception as e: 115 raise TaskError("answer failed: %s" % to_text(e)) 116 117 118def wait_for_task(task, max_backoff=64, timeout=3600, vm=None, answers=None): 119 """Wait for given task using exponential back-off algorithm. 120 121 Args: 122 task: VMware task object 123 max_backoff: Maximum amount of sleep time in seconds 124 timeout: Timeout for the given task in seconds 125 126 Returns: Tuple with True and result for successful task 127 Raises: TaskError on failure 128 """ 129 failure_counter = 0 130 start_time = time.time() 131 132 while True: 133 if check_answer_question_status(vm): 134 if answers: 135 responses = make_answer_response(vm, answers) 136 answer_question(vm, responses) 137 else: 138 raise TaskError("%s" % to_text(vm.runtime.question.text)) 139 if time.time() - start_time >= timeout: 140 raise TaskError("Timeout") 141 if task.info.state == vim.TaskInfo.State.success: 142 return True, task.info.result 143 if task.info.state == vim.TaskInfo.State.error: 144 error_msg = task.info.error 145 host_thumbprint = None 146 try: 147 error_msg = error_msg.msg 148 if hasattr(task.info.error, 'thumbprint'): 149 host_thumbprint = task.info.error.thumbprint 150 except AttributeError: 151 pass 152 finally: 153 raise_from(TaskError(error_msg, host_thumbprint), task.info.error) 154 if task.info.state in [vim.TaskInfo.State.running, vim.TaskInfo.State.queued]: 155 sleep_time = min(2 ** failure_counter + randint(1, 1000) / 1000, max_backoff) 156 time.sleep(sleep_time) 157 failure_counter += 1 158 159 160def wait_for_vm_ip(content, vm, timeout=300): 161 facts = dict() 162 interval = 15 163 while timeout > 0: 164 _facts = gather_vm_facts(content, vm) 165 if _facts['ipv4'] or _facts['ipv6']: 166 facts = _facts 167 break 168 time.sleep(interval) 169 timeout -= interval 170 171 return facts 172 173 174def find_obj(content, vimtype, name, first=True, folder=None): 175 container = content.viewManager.CreateContainerView(folder or content.rootFolder, recursive=True, type=vimtype) 176 # Get all objects matching type (and name if given) 177 obj_list = [obj for obj in container.view if not name or to_text(unquote(obj.name)) == to_text(unquote(name))] 178 container.Destroy() 179 180 # Return first match or None 181 if first: 182 if obj_list: 183 return obj_list[0] 184 return None 185 186 # Return all matching objects or empty list 187 return obj_list 188 189 190def find_dvspg_by_name(dv_switch, portgroup_name): 191 portgroup_name = quote_obj_name(portgroup_name) 192 portgroups = dv_switch.portgroup 193 194 for pg in portgroups: 195 if pg.name == portgroup_name: 196 return pg 197 198 return None 199 200 201def find_object_by_name(content, name, obj_type, folder=None, recurse=True): 202 if not isinstance(obj_type, list): 203 obj_type = [obj_type] 204 205 name = name.strip() 206 207 objects = get_all_objs(content, obj_type, folder=folder, recurse=recurse) 208 for obj in objects: 209 try: 210 if unquote(obj.name) == name: 211 return obj 212 except vmodl.fault.ManagedObjectNotFound: 213 pass 214 215 return None 216 217 218def find_cluster_by_name(content, cluster_name, datacenter=None): 219 if datacenter and hasattr(datacenter, 'hostFolder'): 220 folder = datacenter.hostFolder 221 else: 222 folder = content.rootFolder 223 224 return find_object_by_name(content, cluster_name, [vim.ClusterComputeResource], folder=folder) 225 226 227def find_datacenter_by_name(content, datacenter_name): 228 return find_object_by_name(content, datacenter_name, [vim.Datacenter]) 229 230 231def get_parent_datacenter(obj): 232 """ Walk the parent tree to find the objects datacenter """ 233 if isinstance(obj, vim.Datacenter): 234 return obj 235 datacenter = None 236 while True: 237 if not hasattr(obj, 'parent'): 238 break 239 obj = obj.parent or obj.parentVApp 240 if isinstance(obj, vim.Datacenter): 241 datacenter = obj 242 break 243 return datacenter 244 245 246def find_datastore_by_name(content, datastore_name, datacenter_name=None): 247 return find_object_by_name(content, datastore_name, [vim.Datastore], datacenter_name) 248 249 250def find_folder_by_name(content, folder_name): 251 return find_object_by_name(content, folder_name, [vim.Folder]) 252 253 254def find_folder_by_fqpn(content, folder_name, datacenter_name=None, folder_type=None): 255 """ 256 Find the folder by its given fully qualified path name. 257 The Fully Qualified Path Name is I(datacenter)/I(folder type)/folder name/ 258 for example - Lab/vm/someparent/myfolder is a vm folder in the Lab datacenter. 259 """ 260 # Remove leading/trailing slashes and create list of subfolders 261 folder = folder_name.strip('/') 262 folder_parts = folder.strip('/').split('/') 263 264 # Process datacenter 265 if len(folder_parts) > 0: 266 if not datacenter_name: 267 datacenter_name = folder_parts[0] 268 if datacenter_name == folder_parts[0]: 269 folder_parts.pop(0) 270 datacenter = find_datacenter_by_name(content, datacenter_name) 271 if not datacenter: 272 return None 273 274 # Process folder type 275 if len(folder_parts) > 0: 276 if not folder_type: 277 folder_type = folder_parts[0] 278 if folder_type == folder_parts[0]: 279 folder_parts.pop(0) 280 if folder_type in ['vm', 'host', 'datastore', 'network']: 281 parent_obj = getattr(datacenter, "%sFolder" % folder_type.lower()) 282 else: 283 return None 284 285 # Process remaining subfolders 286 if len(folder_parts) > 0: 287 for part in folder_parts: 288 folder_obj = None 289 for part_obj in parent_obj.childEntity: 290 if part_obj.name == part and 'Folder' in part_obj.childType: 291 folder_obj = part_obj 292 parent_obj = part_obj 293 break 294 if not folder_obj: 295 return None 296 else: 297 folder_obj = parent_obj 298 return folder_obj 299 300 301def find_dvs_by_name(content, switch_name, folder=None): 302 return find_object_by_name(content, switch_name, [vim.DistributedVirtualSwitch], folder=folder) 303 304 305def find_hostsystem_by_name(content, hostname, datacenter=None): 306 if datacenter and hasattr(datacenter, 'hostFolder'): 307 folder = datacenter.hostFolder 308 else: 309 folder = content.rootFolder 310 return find_object_by_name(content, hostname, [vim.HostSystem], folder=folder) 311 312 313def find_resource_pool_by_name(content, resource_pool_name): 314 return find_object_by_name(content, resource_pool_name, [vim.ResourcePool]) 315 316 317def find_resource_pool_by_cluster(content, resource_pool_name='Resources', cluster=None): 318 return find_object_by_name(content, resource_pool_name, [vim.ResourcePool], folder=cluster) 319 320 321def find_network_by_name(content, network_name, datacenter_name=None): 322 return find_object_by_name(content, network_name, [vim.Network], datacenter_name) 323 324 325def find_vm_by_id(content, vm_id, vm_id_type="vm_name", datacenter=None, 326 cluster=None, folder=None, match_first=False): 327 """ UUID is unique to a VM, every other id returns the first match. """ 328 si = content.searchIndex 329 vm = None 330 331 if vm_id_type == 'dns_name': 332 vm = si.FindByDnsName(datacenter=datacenter, dnsName=vm_id, vmSearch=True) 333 elif vm_id_type == 'uuid': 334 # Search By BIOS UUID rather than instance UUID 335 vm = si.FindByUuid(datacenter=datacenter, instanceUuid=False, uuid=vm_id, vmSearch=True) 336 elif vm_id_type == 'instance_uuid': 337 vm = si.FindByUuid(datacenter=datacenter, instanceUuid=True, uuid=vm_id, vmSearch=True) 338 elif vm_id_type == 'ip': 339 vm = si.FindByIp(datacenter=datacenter, ip=vm_id, vmSearch=True) 340 elif vm_id_type == 'vm_name': 341 folder = None 342 if cluster: 343 folder = cluster 344 elif datacenter: 345 folder = datacenter.hostFolder 346 vm = find_vm_by_name(content, vm_id, folder) 347 elif vm_id_type == 'inventory_path': 348 searchpath = folder 349 # get all objects for this path 350 f_obj = si.FindByInventoryPath(searchpath) 351 if f_obj: 352 if isinstance(f_obj, vim.Datacenter): 353 f_obj = f_obj.vmFolder 354 for c_obj in f_obj.childEntity: 355 if not isinstance(c_obj, vim.VirtualMachine): 356 continue 357 if c_obj.name == vm_id: 358 vm = c_obj 359 if match_first: 360 break 361 return vm 362 363 364def find_vm_by_name(content, vm_name, folder=None, recurse=True): 365 return find_object_by_name(content, vm_name, [vim.VirtualMachine], folder=folder, recurse=recurse) 366 367 368def find_host_portgroup_by_name(host, portgroup_name): 369 370 for portgroup in host.config.network.portgroup: 371 if portgroup.spec.name == portgroup_name: 372 return portgroup 373 return None 374 375 376def compile_folder_path_for_object(vobj): 377 """ make a /vm/foo/bar/baz like folder path for an object """ 378 379 paths = [] 380 if isinstance(vobj, vim.Folder): 381 paths.append(vobj.name) 382 383 thisobj = vobj 384 while hasattr(thisobj, 'parent'): 385 thisobj = thisobj.parent 386 try: 387 moid = thisobj._moId 388 except AttributeError: 389 moid = None 390 if moid in ['group-d1', 'ha-folder-root']: 391 break 392 if isinstance(thisobj, vim.Folder): 393 paths.append(thisobj.name) 394 paths.reverse() 395 return '/' + '/'.join(paths) 396 397 398def _get_vm_prop(vm, attributes): 399 """Safely get a property or return None""" 400 result = vm 401 for attribute in attributes: 402 try: 403 result = getattr(result, attribute) 404 except (AttributeError, IndexError): 405 return None 406 return result 407 408 409def gather_vm_facts(content, vm): 410 """ Gather facts from vim.VirtualMachine object. """ 411 facts = { 412 'module_hw': True, 413 'hw_name': vm.config.name, 414 'hw_power_status': vm.summary.runtime.powerState, 415 'hw_guest_full_name': vm.summary.guest.guestFullName, 416 'hw_guest_id': vm.summary.guest.guestId, 417 'hw_product_uuid': vm.config.uuid, 418 'hw_processor_count': vm.config.hardware.numCPU, 419 'hw_cores_per_socket': vm.config.hardware.numCoresPerSocket, 420 'hw_memtotal_mb': vm.config.hardware.memoryMB, 421 'hw_interfaces': [], 422 'hw_datastores': [], 423 'hw_files': [], 424 'hw_esxi_host': None, 425 'hw_guest_ha_state': None, 426 'hw_is_template': vm.config.template, 427 'hw_folder': None, 428 'hw_version': vm.config.version, 429 'instance_uuid': vm.config.instanceUuid, 430 'guest_tools_status': _get_vm_prop(vm, ('guest', 'toolsRunningStatus')), 431 'guest_tools_version': _get_vm_prop(vm, ('guest', 'toolsVersion')), 432 'guest_question': json.loads(json.dumps(vm.summary.runtime.question, cls=VmomiSupport.VmomiJSONEncoder, 433 sort_keys=True, strip_dynamic=True)), 434 'guest_consolidation_needed': vm.summary.runtime.consolidationNeeded, 435 'ipv4': None, 436 'ipv6': None, 437 'annotation': vm.config.annotation, 438 'customvalues': {}, 439 'snapshots': [], 440 'current_snapshot': None, 441 'vnc': {}, 442 'moid': vm._moId, 443 'vimref': "vim.VirtualMachine:%s" % vm._moId, 444 'advanced_settings': {}, 445 } 446 447 # facts that may or may not exist 448 if vm.summary.runtime.host: 449 try: 450 host = vm.summary.runtime.host 451 facts['hw_esxi_host'] = host.summary.config.name 452 facts['hw_cluster'] = host.parent.name if host.parent and isinstance(host.parent, vim.ClusterComputeResource) else None 453 454 except vim.fault.NoPermission: 455 # User does not have read permission for the host system, 456 # proceed without this value. This value does not contribute or hamper 457 # provisioning or power management operations. 458 pass 459 if vm.summary.runtime.dasVmProtection: 460 facts['hw_guest_ha_state'] = vm.summary.runtime.dasVmProtection.dasProtected 461 462 datastores = vm.datastore 463 for ds in datastores: 464 facts['hw_datastores'].append(ds.info.name) 465 466 try: 467 files = vm.config.files 468 layout = vm.layout 469 if files: 470 facts['hw_files'] = [files.vmPathName] 471 for item in layout.snapshot: 472 for snap in item.snapshotFile: 473 if 'vmsn' in snap: 474 facts['hw_files'].append(snap) 475 for item in layout.configFile: 476 facts['hw_files'].append(os.path.join(os.path.dirname(files.vmPathName), item)) 477 for item in vm.layout.logFile: 478 facts['hw_files'].append(os.path.join(files.logDirectory, item)) 479 for item in vm.layout.disk: 480 for disk in item.diskFile: 481 facts['hw_files'].append(disk) 482 except Exception: 483 pass 484 485 facts['hw_folder'] = PyVmomi.get_vm_path(content, vm) 486 487 cfm = content.customFieldsManager 488 # Resolve custom values 489 for value_obj in vm.summary.customValue: 490 kn = value_obj.key 491 if cfm is not None and cfm.field: 492 for f in cfm.field: 493 if f.key == value_obj.key: 494 kn = f.name 495 # Exit the loop immediately, we found it 496 break 497 498 facts['customvalues'][kn] = value_obj.value 499 500 # Resolve advanced settings 501 for advanced_setting in vm.config.extraConfig: 502 facts['advanced_settings'][advanced_setting.key] = advanced_setting.value 503 504 net_dict = {} 505 vmnet = _get_vm_prop(vm, ('guest', 'net')) 506 if vmnet: 507 for device in vmnet: 508 if device.deviceConfigId > 0: 509 net_dict[device.macAddress] = list(device.ipAddress) 510 511 if vm.guest.ipAddress: 512 if ':' in vm.guest.ipAddress: 513 facts['ipv6'] = vm.guest.ipAddress 514 else: 515 facts['ipv4'] = vm.guest.ipAddress 516 517 ethernet_idx = 0 518 for entry in vm.config.hardware.device: 519 if not hasattr(entry, 'macAddress'): 520 continue 521 522 if entry.macAddress: 523 mac_addr = entry.macAddress 524 mac_addr_dash = mac_addr.replace(':', '-') 525 else: 526 mac_addr = mac_addr_dash = None 527 528 if ( 529 hasattr(entry, "backing") 530 and hasattr(entry.backing, "port") 531 and hasattr(entry.backing.port, "portKey") 532 and hasattr(entry.backing.port, "portgroupKey") 533 ): 534 port_group_key = entry.backing.port.portgroupKey 535 port_key = entry.backing.port.portKey 536 else: 537 port_group_key = None 538 port_key = None 539 540 factname = 'hw_eth' + str(ethernet_idx) 541 facts[factname] = { 542 'addresstype': entry.addressType, 543 'label': entry.deviceInfo.label, 544 'macaddress': mac_addr, 545 'ipaddresses': net_dict.get(entry.macAddress, None), 546 'macaddress_dash': mac_addr_dash, 547 'summary': entry.deviceInfo.summary, 548 'portgroup_portkey': port_key, 549 'portgroup_key': port_group_key, 550 } 551 facts['hw_interfaces'].append('eth' + str(ethernet_idx)) 552 ethernet_idx += 1 553 554 snapshot_facts = list_snapshots(vm) 555 if 'snapshots' in snapshot_facts: 556 facts['snapshots'] = snapshot_facts['snapshots'] 557 facts['current_snapshot'] = snapshot_facts['current_snapshot'] 558 559 facts['vnc'] = get_vnc_extraconfig(vm) 560 return facts 561 562 563def ansible_date_time_facts(timestamp): 564 # timestamp is a datetime.datetime object 565 date_time_facts = {} 566 if timestamp is None: 567 return date_time_facts 568 569 utctimestamp = timestamp.astimezone(datetime.timezone.utc) 570 571 date_time_facts['year'] = timestamp.strftime('%Y') 572 date_time_facts['month'] = timestamp.strftime('%m') 573 date_time_facts['weekday'] = timestamp.strftime('%A') 574 date_time_facts['weekday_number'] = timestamp.strftime('%w') 575 date_time_facts['weeknumber'] = timestamp.strftime('%W') 576 date_time_facts['day'] = timestamp.strftime('%d') 577 date_time_facts['hour'] = timestamp.strftime('%H') 578 date_time_facts['minute'] = timestamp.strftime('%M') 579 date_time_facts['second'] = timestamp.strftime('%S') 580 date_time_facts['epoch'] = timestamp.strftime('%s') 581 date_time_facts['date'] = timestamp.strftime('%Y-%m-%d') 582 date_time_facts['time'] = timestamp.strftime('%H:%M:%S') 583 date_time_facts['iso8601_micro'] = utctimestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") 584 date_time_facts['iso8601'] = utctimestamp.strftime("%Y-%m-%dT%H:%M:%SZ") 585 date_time_facts['iso8601_basic'] = timestamp.strftime("%Y%m%dT%H%M%S%f") 586 date_time_facts['iso8601_basic_short'] = timestamp.strftime("%Y%m%dT%H%M%S") 587 date_time_facts['tz'] = timestamp.strftime("%Z") 588 date_time_facts['tz_offset'] = timestamp.strftime("%z") 589 590 return date_time_facts 591 592 593def deserialize_snapshot_obj(obj): 594 return {'id': obj.id, 595 'name': obj.name, 596 'description': obj.description, 597 'creation_time': obj.createTime, 598 'state': obj.state, 599 'quiesced': obj.quiesced} 600 601 602def list_snapshots_recursively(snapshots): 603 snapshot_data = [] 604 for snapshot in snapshots: 605 snapshot_data.append(deserialize_snapshot_obj(snapshot)) 606 snapshot_data = snapshot_data + list_snapshots_recursively(snapshot.childSnapshotList) 607 return snapshot_data 608 609 610def get_current_snap_obj(snapshots, snapob): 611 snap_obj = [] 612 for snapshot in snapshots: 613 if snapshot.snapshot == snapob: 614 snap_obj.append(snapshot) 615 snap_obj = snap_obj + get_current_snap_obj(snapshot.childSnapshotList, snapob) 616 return snap_obj 617 618 619def list_snapshots(vm): 620 result = {} 621 snapshot = _get_vm_prop(vm, ('snapshot',)) 622 if not snapshot: 623 return result 624 if vm.snapshot is None: 625 return result 626 627 result['snapshots'] = list_snapshots_recursively(vm.snapshot.rootSnapshotList) 628 current_snapref = vm.snapshot.currentSnapshot 629 current_snap_obj = get_current_snap_obj(vm.snapshot.rootSnapshotList, current_snapref) 630 if current_snap_obj: 631 result['current_snapshot'] = deserialize_snapshot_obj(current_snap_obj[0]) 632 else: 633 result['current_snapshot'] = dict() 634 return result 635 636 637def get_vnc_extraconfig(vm): 638 result = {} 639 for opts in vm.config.extraConfig: 640 for optkeyname in ['enabled', 'ip', 'port', 'password']: 641 if opts.key.lower() == "remotedisplay.vnc." + optkeyname: 642 result[optkeyname] = opts.value 643 return result 644 645 646def vmware_argument_spec(): 647 return dict( 648 hostname=dict(type='str', 649 required=False, 650 fallback=(env_fallback, ['VMWARE_HOST']), 651 ), 652 username=dict(type='str', 653 aliases=['user', 'admin'], 654 required=False, 655 fallback=(env_fallback, ['VMWARE_USER'])), 656 password=dict(type='str', 657 aliases=['pass', 'pwd'], 658 required=False, 659 no_log=True, 660 fallback=(env_fallback, ['VMWARE_PASSWORD'])), 661 port=dict(type='int', 662 default=443, 663 fallback=(env_fallback, ['VMWARE_PORT'])), 664 validate_certs=dict(type='bool', 665 required=False, 666 default=True, 667 fallback=(env_fallback, ['VMWARE_VALIDATE_CERTS']) 668 ), 669 proxy_host=dict(type='str', 670 required=False, 671 default=None, 672 fallback=(env_fallback, ['VMWARE_PROXY_HOST'])), 673 proxy_port=dict(type='int', 674 required=False, 675 default=None, 676 fallback=(env_fallback, ['VMWARE_PROXY_PORT'])), 677 ) 678 679 680def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=None, username=None, password=None, port=None, validate_certs=None, 681 httpProxyHost=None, httpProxyPort=None): 682 if module: 683 if not hostname: 684 hostname = module.params['hostname'] 685 if not username: 686 username = module.params['username'] 687 if not password: 688 password = module.params['password'] 689 if not httpProxyHost: 690 httpProxyHost = module.params.get('proxy_host') 691 if not httpProxyPort: 692 httpProxyPort = module.params.get('proxy_port') 693 if not port: 694 port = module.params.get('port', 443) 695 if not validate_certs: 696 validate_certs = module.params['validate_certs'] 697 698 def _raise_or_fail(msg): 699 if module is not None: 700 module.fail_json(msg=msg) 701 raise ApiAccessError(msg) 702 703 if not hostname: 704 _raise_or_fail(msg="Hostname parameter is missing." 705 " Please specify this parameter in task or" 706 " export environment variable like 'export VMWARE_HOST=ESXI_HOSTNAME'") 707 708 if not username: 709 _raise_or_fail(msg="Username parameter is missing." 710 " Please specify this parameter in task or" 711 " export environment variable like 'export VMWARE_USER=ESXI_USERNAME'") 712 713 if not password: 714 _raise_or_fail(msg="Password parameter is missing." 715 " Please specify this parameter in task or" 716 " export environment variable like 'export VMWARE_PASSWORD=ESXI_PASSWORD'") 717 718 if validate_certs and not hasattr(ssl, 'SSLContext'): 719 _raise_or_fail(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update ' 720 'python or use validate_certs=false.') 721 elif validate_certs: 722 ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 723 ssl_context.verify_mode = ssl.CERT_REQUIRED 724 ssl_context.check_hostname = True 725 ssl_context.load_default_certs() 726 elif hasattr(ssl, 'SSLContext'): 727 ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 728 ssl_context.verify_mode = ssl.CERT_NONE 729 ssl_context.check_hostname = False 730 else: # Python < 2.7.9 or RHEL/Centos < 7.4 731 ssl_context = None 732 733 service_instance = None 734 735 connect_args = dict( 736 host=hostname, 737 port=port, 738 ) 739 if ssl_context: 740 connect_args.update(sslContext=ssl_context) 741 742 msg_suffix = '' 743 try: 744 if httpProxyHost: 745 msg_suffix = " [proxy: %s:%d]" % (httpProxyHost, httpProxyPort) 746 connect_args.update(httpProxyHost=httpProxyHost, httpProxyPort=httpProxyPort) 747 smart_stub = connect.SmartStubAdapter(**connect_args) 748 session_stub = connect.VimSessionOrientedStub(smart_stub, connect.VimSessionOrientedStub.makeUserLoginMethod(username, password)) 749 service_instance = vim.ServiceInstance('ServiceInstance', session_stub) 750 else: 751 connect_args.update(user=username, pwd=password) 752 service_instance = connect.SmartConnect(**connect_args) 753 except vim.fault.InvalidLogin as invalid_login: 754 msg = "Unable to log on to vCenter or ESXi API at %s:%s " % (hostname, port) 755 _raise_or_fail(msg="%s as %s: %s" % (msg, username, invalid_login.msg) + msg_suffix) 756 except vim.fault.NoPermission as no_permission: 757 _raise_or_fail(msg="User %s does not have required permission" 758 " to log on to vCenter or ESXi API at %s:%s : %s" % (username, hostname, port, no_permission.msg)) 759 except (requests.ConnectionError, ssl.SSLError) as generic_req_exc: 760 _raise_or_fail(msg="Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (hostname, port, generic_req_exc)) 761 except vmodl.fault.InvalidRequest as invalid_request: 762 # Request is malformed 763 msg = "Failed to get a response from server %s:%s " % (hostname, port) 764 _raise_or_fail(msg="%s as request is malformed: %s" % (msg, invalid_request.msg) + msg_suffix) 765 except Exception as generic_exc: 766 msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port) + msg_suffix 767 _raise_or_fail(msg="%s : %s" % (msg, generic_exc)) 768 769 if service_instance is None: 770 msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port) 771 _raise_or_fail(msg=msg + msg_suffix) 772 773 # Disabling atexit should be used in special cases only. 774 # Such as IP change of the ESXi host which removes the connection anyway. 775 # Also removal significantly speeds up the return of the module 776 if disconnect_atexit: 777 atexit.register(connect.Disconnect, service_instance) 778 if return_si: 779 return service_instance, service_instance.RetrieveContent() 780 return service_instance.RetrieveContent() 781 782 783def get_all_objs(content, vimtype, folder=None, recurse=True): 784 if not folder: 785 folder = content.rootFolder 786 787 obj = {} 788 container = content.viewManager.CreateContainerView(folder, vimtype, recurse) 789 for managed_object_ref in container.view: 790 try: 791 obj.update({managed_object_ref: managed_object_ref.name}) 792 except vmodl.fault.ManagedObjectNotFound: 793 pass 794 return obj 795 796 797def run_command_in_guest(content, vm, username, password, program_path, program_args, program_cwd, program_env): 798 799 result = {'failed': False} 800 801 tools_status = vm.guest.toolsStatus 802 if (tools_status == 'toolsNotInstalled' or tools_status == 'toolsNotRunning'): 803 result['failed'] = True 804 result['msg'] = "VMwareTools is not installed or is not running in the guest" 805 return result 806 807 # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst 808 creds = vim.vm.guest.NamePasswordAuthentication( 809 username=username, password=password 810 ) 811 812 try: 813 # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/ProcessManager.rst 814 pm = content.guestOperationsManager.processManager 815 # https://www.vmware.com/support/developer/converter-sdk/conv51_apireference/vim.vm.guest.ProcessManager.ProgramSpec.html 816 ps = vim.vm.guest.ProcessManager.ProgramSpec( 817 # programPath=program, 818 # arguments=args 819 programPath=program_path, 820 arguments=program_args, 821 workingDirectory=program_cwd, 822 ) 823 824 res = pm.StartProgramInGuest(vm, creds, ps) 825 result['pid'] = res 826 pdata = pm.ListProcessesInGuest(vm, creds, [res]) 827 828 # wait for pid to finish 829 while not pdata[0].endTime: 830 time.sleep(1) 831 pdata = pm.ListProcessesInGuest(vm, creds, [res]) 832 833 result['owner'] = pdata[0].owner 834 result['startTime'] = pdata[0].startTime.isoformat() 835 result['endTime'] = pdata[0].endTime.isoformat() 836 result['exitCode'] = pdata[0].exitCode 837 if result['exitCode'] != 0: 838 result['failed'] = True 839 result['msg'] = "program exited non-zero" 840 else: 841 result['msg'] = "program completed successfully" 842 843 except Exception as e: 844 result['msg'] = str(e) 845 result['failed'] = True 846 847 return result 848 849 850def serialize_spec(clonespec): 851 """Serialize a clonespec or a relocation spec""" 852 data = {} 853 attrs = dir(clonespec) 854 attrs = [x for x in attrs if not x.startswith('_')] 855 for x in attrs: 856 xo = getattr(clonespec, x) 857 if callable(xo): 858 continue 859 xt = type(xo) 860 if xo is None: 861 data[x] = None 862 elif isinstance(xo, vim.vm.ConfigSpec): 863 data[x] = serialize_spec(xo) 864 elif isinstance(xo, vim.vm.RelocateSpec): 865 data[x] = serialize_spec(xo) 866 elif isinstance(xo, vim.vm.device.VirtualDisk): 867 data[x] = serialize_spec(xo) 868 elif isinstance(xo, vim.vm.device.VirtualDeviceSpec.FileOperation): 869 data[x] = to_text(xo) 870 elif isinstance(xo, vim.Description): 871 data[x] = { 872 'dynamicProperty': serialize_spec(xo.dynamicProperty), 873 'dynamicType': serialize_spec(xo.dynamicType), 874 'label': serialize_spec(xo.label), 875 'summary': serialize_spec(xo.summary), 876 } 877 elif hasattr(xo, 'name'): 878 data[x] = to_text(xo) + ':' + to_text(xo.name) 879 elif isinstance(xo, vim.vm.ProfileSpec): 880 pass 881 elif issubclass(xt, list): 882 data[x] = [] 883 for xe in xo: 884 data[x].append(serialize_spec(xe)) 885 elif issubclass(xt, string_types + integer_types + (float, bool)): 886 if issubclass(xt, integer_types): 887 data[x] = int(xo) 888 else: 889 data[x] = to_text(xo) 890 elif issubclass(xt, bool): 891 data[x] = xo 892 elif issubclass(xt, dict): 893 data[to_text(x)] = {} 894 for k, v in xo.items(): 895 k = to_text(k) 896 data[x][k] = serialize_spec(v) 897 else: 898 data[x] = str(xt) 899 900 return data 901 902 903def find_host_by_cluster_datacenter(module, content, datacenter_name, cluster_name, host_name): 904 dc = find_datacenter_by_name(content, datacenter_name) 905 if dc is None: 906 module.fail_json(msg="Unable to find datacenter with name %s" % datacenter_name) 907 cluster = find_cluster_by_name(content, cluster_name, datacenter=dc) 908 if cluster is None: 909 module.fail_json(msg="Unable to find cluster with name %s" % cluster_name) 910 911 for host in cluster.host: 912 if host.name == host_name: 913 return host, cluster 914 915 return None, cluster 916 917 918def set_vm_power_state(content, vm, state, force, timeout=0, answers=None): 919 """ 920 Set the power status for a VM determined by the current and 921 requested states. force is forceful 922 """ 923 facts = gather_vm_facts(content, vm) 924 if state == 'present': 925 state = 'poweredon' 926 expected_state = state.replace('_', '').replace('-', '').lower() 927 current_state = facts['hw_power_status'].lower() 928 result = dict( 929 changed=False, 930 failed=False, 931 ) 932 933 # Need Force 934 if not force and current_state not in ['poweredon', 'poweredoff']: 935 result['failed'] = True 936 result['msg'] = "Virtual Machine is in %s power state. Force is required!" % current_state 937 result['instance'] = gather_vm_facts(content, vm) 938 return result 939 940 # State is not already true 941 if current_state != expected_state: 942 task = None 943 try: 944 if expected_state == 'poweredoff': 945 task = vm.PowerOff() 946 947 elif expected_state == 'poweredon': 948 task = vm.PowerOn() 949 950 elif expected_state == 'restarted': 951 if current_state in ('poweredon', 'poweringon', 'resetting', 'poweredoff'): 952 task = vm.Reset() 953 else: 954 result['failed'] = True 955 result['msg'] = "Cannot restart virtual machine in the current state %s" % current_state 956 957 elif expected_state == 'suspended': 958 if current_state in ('poweredon', 'poweringon'): 959 task = vm.Suspend() 960 else: 961 result['failed'] = True 962 result['msg'] = 'Cannot suspend virtual machine in the current state %s' % current_state 963 964 elif expected_state in ['shutdownguest', 'rebootguest']: 965 if current_state == 'poweredon': 966 if vm.guest.toolsRunningStatus == 'guestToolsRunning': 967 if expected_state == 'shutdownguest': 968 task = vm.ShutdownGuest() 969 if timeout > 0: 970 result.update(wait_for_poweroff(vm, timeout)) 971 else: 972 task = vm.RebootGuest() 973 # Set result['changed'] immediately because 974 # shutdown and reboot return None. 975 result['changed'] = True 976 else: 977 result['failed'] = True 978 result['msg'] = "VMware tools should be installed for guest shutdown/reboot" 979 else: 980 result['failed'] = True 981 result['msg'] = "Virtual machine %s must be in poweredon state for guest shutdown/reboot" % vm.name 982 983 else: 984 result['failed'] = True 985 result['msg'] = "Unsupported expected state provided: %s" % expected_state 986 987 except Exception as e: 988 result['failed'] = True 989 result['msg'] = to_text(e) 990 991 if task: 992 try: 993 wait_for_task(task, vm=vm, answers=answers) 994 except TaskError as e: 995 result['failed'] = True 996 result['msg'] = to_text(e) 997 finally: 998 if task.info.state == 'error': 999 result['failed'] = True 1000 result['msg'] = task.info.error.msg 1001 else: 1002 result['changed'] = True 1003 1004 # need to get new metadata if changed 1005 result['instance'] = gather_vm_facts(content, vm) 1006 1007 return result 1008 1009 1010def wait_for_poweroff(vm, timeout=300): 1011 result = dict() 1012 interval = 15 1013 while timeout > 0: 1014 if vm.runtime.powerState.lower() == 'poweredoff': 1015 break 1016 time.sleep(interval) 1017 timeout -= interval 1018 else: 1019 result['failed'] = True 1020 result['msg'] = 'Timeout while waiting for VM power off.' 1021 return result 1022 1023 1024def is_integer(value, type_of='int'): 1025 try: 1026 VmomiSupport.vmodlTypes[type_of](value) 1027 return True 1028 except (TypeError, ValueError): 1029 return False 1030 1031 1032def is_boolean(value): 1033 if str(value).lower() in ['true', 'on', 'yes', 'false', 'off', 'no']: 1034 return True 1035 return False 1036 1037 1038def is_truthy(value): 1039 if str(value).lower() in ['true', 'on', 'yes']: 1040 return True 1041 return False 1042 1043 1044# options is the dict as defined in the module parameters, current_options is 1045# the list of the currently set options as returned by the vSphere API. 1046# When truthy_strings_as_bool is True, strings like 'true', 'off' or 'yes' 1047# are converted to booleans. 1048def option_diff(options, current_options, truthy_strings_as_bool=True): 1049 current_options_dict = {} 1050 for option in current_options: 1051 current_options_dict[option.key] = option.value 1052 1053 change_option_list = [] 1054 for option_key, option_value in options.items(): 1055 if truthy_strings_as_bool and is_boolean(option_value): 1056 option_value = VmomiSupport.vmodlTypes['bool'](is_truthy(option_value)) 1057 elif type(option_value) is int: 1058 option_value = VmomiSupport.vmodlTypes['int'](option_value) 1059 elif type(option_value) is float: 1060 option_value = VmomiSupport.vmodlTypes['float'](option_value) 1061 elif type(option_value) is str: 1062 option_value = VmomiSupport.vmodlTypes['string'](option_value) 1063 1064 if option_key not in current_options_dict or current_options_dict[option_key] != option_value: 1065 change_option_list.append(vim.option.OptionValue(key=option_key, value=option_value)) 1066 1067 return change_option_list 1068 1069 1070def quote_obj_name(object_name=None): 1071 """ 1072 Replace special characters in object name 1073 with urllib quote equivalent 1074 1075 """ 1076 if not object_name: 1077 return None 1078 1079 SPECIAL_CHARS = OrderedDict({ 1080 '%': '%25', 1081 '/': '%2f', 1082 '\\': '%5c' 1083 }) 1084 for key in SPECIAL_CHARS.keys(): 1085 if key in object_name: 1086 object_name = object_name.replace(key, SPECIAL_CHARS[key]) 1087 1088 return object_name 1089 1090 1091def dvs_supports_mac_learning(dvs): 1092 """ 1093 Test if the switch supports MAC learning 1094 """ 1095 return hasattr(dvs.capability.featuresSupported, 'macLearningSupported') and dvs.capability.featuresSupported.macLearningSupported 1096 1097 1098class PyVmomi(object): 1099 def __init__(self, module): 1100 """ 1101 Constructor 1102 """ 1103 if not HAS_REQUESTS: 1104 module.fail_json(msg=missing_required_lib('requests'), 1105 exception=REQUESTS_IMP_ERR) 1106 1107 if not HAS_PYVMOMI: 1108 module.fail_json(msg=missing_required_lib('PyVmomi'), 1109 exception=PYVMOMI_IMP_ERR) 1110 1111 self.module = module 1112 self.params = module.params 1113 self.current_vm_obj = None 1114 self.si, self.content = connect_to_api(self.module, return_si=True) 1115 self.custom_field_mgr = [] 1116 if self.content.customFieldsManager: # not an ESXi 1117 self.custom_field_mgr = self.content.customFieldsManager.field 1118 1119 def is_vcenter(self): 1120 """ 1121 Check if given hostname is vCenter or ESXi host 1122 Returns: True if given connection is with vCenter server 1123 False if given connection is with ESXi server 1124 1125 """ 1126 api_type = None 1127 try: 1128 api_type = self.content.about.apiType 1129 except (vmodl.RuntimeFault, vim.fault.VimFault) as exc: 1130 self.module.fail_json(msg="Failed to get status of vCenter server : %s" % exc.msg) 1131 1132 if api_type == 'VirtualCenter': 1133 return True 1134 elif api_type == 'HostAgent': 1135 return False 1136 1137 def get_managed_objects_properties(self, vim_type, properties=None): 1138 """ 1139 Look up a Managed Object Reference in vCenter / ESXi Environment 1140 :param vim_type: Type of vim object e.g, for datacenter - vim.Datacenter 1141 :param properties: List of properties related to vim object e.g. Name 1142 :return: local content object 1143 """ 1144 # Get Root Folder 1145 root_folder = self.content.rootFolder 1146 1147 if properties is None: 1148 properties = ['name'] 1149 1150 # Create Container View with default root folder 1151 mor = self.content.viewManager.CreateContainerView(root_folder, [vim_type], True) 1152 1153 # Create Traversal spec 1154 traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( 1155 name="traversal_spec", 1156 path='view', 1157 skip=False, 1158 type=vim.view.ContainerView 1159 ) 1160 1161 # Create Property Spec 1162 property_spec = vmodl.query.PropertyCollector.PropertySpec( 1163 type=vim_type, # Type of object to retrieved 1164 all=False, 1165 pathSet=properties 1166 ) 1167 1168 # Create Object Spec 1169 object_spec = vmodl.query.PropertyCollector.ObjectSpec( 1170 obj=mor, 1171 skip=True, 1172 selectSet=[traversal_spec] 1173 ) 1174 1175 # Create Filter Spec 1176 filter_spec = vmodl.query.PropertyCollector.FilterSpec( 1177 objectSet=[object_spec], 1178 propSet=[property_spec], 1179 reportMissingObjectsInResults=False 1180 ) 1181 1182 return self.content.propertyCollector.RetrieveContents([filter_spec]) 1183 1184 # Virtual Machine related functions 1185 def get_vm(self): 1186 """ 1187 Find unique virtual machine either by UUID, MoID or Name. 1188 Returns: virtual machine object if found, else None. 1189 1190 """ 1191 vm_obj = None 1192 user_desired_path = None 1193 use_instance_uuid = self.params.get('use_instance_uuid') or False 1194 if 'uuid' in self.params and self.params['uuid']: 1195 if not use_instance_uuid: 1196 vm_obj = find_vm_by_id(self.content, vm_id=self.params['uuid'], vm_id_type="uuid") 1197 elif use_instance_uuid: 1198 vm_obj = find_vm_by_id(self.content, 1199 vm_id=self.params['uuid'], 1200 vm_id_type="instance_uuid") 1201 elif 'name' in self.params and self.params['name']: 1202 objects = self.get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) 1203 vms = [] 1204 1205 for temp_vm_object in objects: 1206 if ( 1207 len(temp_vm_object.propSet) == 1 1208 and unquote(temp_vm_object.propSet[0].val) == self.params["name"] 1209 ): 1210 vms.append(temp_vm_object.obj) 1211 1212 # get_managed_objects_properties may return multiple virtual machine, 1213 # following code tries to find user desired one depending upon the folder specified. 1214 if len(vms) > 1: 1215 # We have found multiple virtual machines, decide depending upon folder value 1216 if self.params['folder'] is None: 1217 self.module.fail_json(msg="Multiple virtual machines with same name [%s] found, " 1218 "Folder value is a required parameter to find uniqueness " 1219 "of the virtual machine" % self.params['name'], 1220 details="Please see documentation of the vmware_guest module " 1221 "for folder parameter.") 1222 1223 # Get folder path where virtual machine is located 1224 # User provided folder where user thinks virtual machine is present 1225 user_folder = self.params['folder'] 1226 # User defined datacenter 1227 user_defined_dc = self.params['datacenter'] 1228 # User defined datacenter's object 1229 datacenter_obj = find_datacenter_by_name(self.content, self.params['datacenter']) 1230 # Get Path for Datacenter 1231 dcpath = compile_folder_path_for_object(vobj=datacenter_obj) 1232 1233 # Nested folder does not return trailing / 1234 if not dcpath.endswith('/'): 1235 dcpath += '/' 1236 1237 if user_folder in [None, '', '/']: 1238 # User provided blank value or 1239 # User provided only root value, we fail 1240 self.module.fail_json(msg="vmware_guest found multiple virtual machines with same " 1241 "name [%s], please specify folder path other than blank " 1242 "or '/'" % self.params['name']) 1243 elif user_folder.startswith('/vm/'): 1244 # User provided nested folder under VMware default vm folder i.e. folder = /vm/india/finance 1245 user_desired_path = "%s%s%s" % (dcpath, user_defined_dc, user_folder) 1246 else: 1247 # User defined datacenter is not nested i.e. dcpath = '/' , or 1248 # User defined datacenter is nested i.e. dcpath = '/F0/DC0' or 1249 # User provided folder starts with / and datacenter i.e. folder = /ha-datacenter/ or 1250 # User defined folder starts with datacenter without '/' i.e. 1251 # folder = DC0/vm/india/finance or 1252 # folder = DC0/vm 1253 user_desired_path = user_folder 1254 1255 for vm in vms: 1256 # Check if user has provided same path as virtual machine 1257 actual_vm_folder_path = self.get_vm_path(content=self.content, vm_name=vm) 1258 if not actual_vm_folder_path.startswith("%s%s" % (dcpath, user_defined_dc)): 1259 continue 1260 if user_desired_path in actual_vm_folder_path: 1261 vm_obj = vm 1262 break 1263 elif vms: 1264 # Unique virtual machine found. 1265 actual_vm_folder_path = self.get_vm_path(content=self.content, vm_name=vms[0]) 1266 if self.params.get('folder') is None: 1267 vm_obj = vms[0] 1268 elif self.params['folder'] in actual_vm_folder_path: 1269 vm_obj = vms[0] 1270 elif 'moid' in self.params and self.params['moid']: 1271 vm_obj = VmomiSupport.templateOf('VirtualMachine')(self.params['moid'], self.si._stub) 1272 try: 1273 getattr(vm_obj, 'name') 1274 except vmodl.fault.ManagedObjectNotFound: 1275 vm_obj = None 1276 1277 if vm_obj: 1278 self.current_vm_obj = vm_obj 1279 1280 return vm_obj 1281 1282 def gather_facts(self, vm): 1283 """ 1284 Gather facts of virtual machine. 1285 Args: 1286 vm: Name of virtual machine. 1287 1288 Returns: Facts dictionary of the given virtual machine. 1289 1290 """ 1291 return gather_vm_facts(self.content, vm) 1292 1293 @staticmethod 1294 def get_vm_path(content, vm_name): 1295 """ 1296 Find the path of virtual machine. 1297 Args: 1298 content: VMware content object 1299 vm_name: virtual machine managed object 1300 1301 Returns: Folder of virtual machine if exists, else None 1302 1303 """ 1304 folder_name = None 1305 folder = vm_name.parent 1306 if folder: 1307 folder_name = folder.name 1308 fp = folder.parent 1309 # climb back up the tree to find our path, stop before the root folder 1310 while fp is not None and fp.name is not None and fp != content.rootFolder: 1311 folder_name = fp.name + '/' + folder_name 1312 try: 1313 fp = fp.parent 1314 except Exception: 1315 break 1316 folder_name = '/' + folder_name 1317 return folder_name 1318 1319 def get_vm_or_template(self, template_name=None): 1320 """ 1321 Find the virtual machine or virtual machine template using name 1322 used for cloning purpose. 1323 Args: 1324 template_name: Name of virtual machine or virtual machine template 1325 1326 Returns: virtual machine or virtual machine template object 1327 1328 """ 1329 template_obj = None 1330 if not template_name: 1331 return template_obj 1332 1333 if "/" in template_name: 1334 vm_obj_path = os.path.dirname(template_name) 1335 vm_obj_name = os.path.basename(template_name) 1336 template_obj = find_vm_by_id(self.content, vm_obj_name, vm_id_type="inventory_path", folder=vm_obj_path) 1337 if template_obj: 1338 return template_obj 1339 else: 1340 template_obj = find_vm_by_id(self.content, vm_id=template_name, vm_id_type="uuid") 1341 if template_obj: 1342 return template_obj 1343 1344 objects = self.get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) 1345 templates = [] 1346 1347 for temp_vm_object in objects: 1348 if len(temp_vm_object.propSet) != 1: 1349 continue 1350 for temp_vm_object_property in temp_vm_object.propSet: 1351 if temp_vm_object_property.val == template_name: 1352 templates.append(temp_vm_object.obj) 1353 break 1354 1355 if len(templates) > 1: 1356 # We have found multiple virtual machine templates 1357 self.module.fail_json(msg="Multiple virtual machines or templates with same name [%s] found." % template_name) 1358 elif templates: 1359 template_obj = templates[0] 1360 1361 return template_obj 1362 1363 # Cluster related functions 1364 def find_cluster_by_name(self, cluster_name, datacenter_name=None): 1365 """ 1366 Find Cluster by name in given datacenter 1367 Args: 1368 cluster_name: Name of cluster name to find 1369 datacenter_name: (optional) Name of datacenter 1370 1371 Returns: True if found 1372 1373 """ 1374 return find_cluster_by_name(self.content, cluster_name, datacenter=datacenter_name) 1375 1376 def get_all_hosts_by_cluster(self, cluster_name): 1377 """ 1378 Get all hosts from cluster by cluster name 1379 Args: 1380 cluster_name: Name of cluster 1381 1382 Returns: List of hosts 1383 1384 """ 1385 cluster_obj = self.find_cluster_by_name(cluster_name=cluster_name) 1386 if cluster_obj: 1387 return list(cluster_obj.host) 1388 else: 1389 return [] 1390 1391 # Hosts related functions 1392 def find_hostsystem_by_name(self, host_name, datacenter=None): 1393 """ 1394 Find Host by name 1395 Args: 1396 host_name: Name of ESXi host 1397 datacenter: (optional) Datacenter of ESXi resides 1398 1399 Returns: True if found 1400 1401 """ 1402 return find_hostsystem_by_name(self.content, hostname=host_name, datacenter=datacenter) 1403 1404 def get_all_host_objs(self, cluster_name=None, esxi_host_name=None): 1405 """ 1406 Get all host system managed object 1407 1408 Args: 1409 cluster_name: Name of Cluster 1410 esxi_host_name: Name of ESXi server 1411 1412 Returns: A list of all host system managed objects, else empty list 1413 1414 """ 1415 host_obj_list = [] 1416 if not self.is_vcenter(): 1417 hosts = get_all_objs(self.content, [vim.HostSystem]).keys() 1418 if hosts: 1419 host_obj_list.append(list(hosts)[0]) 1420 else: 1421 if cluster_name: 1422 cluster_obj = self.find_cluster_by_name(cluster_name=cluster_name) 1423 if cluster_obj: 1424 host_obj_list = list(cluster_obj.host) 1425 else: 1426 self.module.fail_json(changed=False, msg="Cluster '%s' not found" % cluster_name) 1427 elif esxi_host_name: 1428 if isinstance(esxi_host_name, str): 1429 esxi_host_name = [esxi_host_name] 1430 1431 for host in esxi_host_name: 1432 esxi_host_obj = self.find_hostsystem_by_name(host_name=host) 1433 if esxi_host_obj: 1434 host_obj_list.append(esxi_host_obj) 1435 else: 1436 self.module.fail_json(changed=False, msg="ESXi '%s' not found" % host) 1437 1438 return host_obj_list 1439 1440 def host_version_at_least(self, version=None, vm_obj=None, host_name=None): 1441 """ 1442 Check that the ESXi Host is at least a specific version number 1443 Args: 1444 vm_obj: virtual machine object, required one of vm_obj, host_name 1445 host_name (string): ESXi host name 1446 version (tuple): a version tuple, for example (6, 7, 0) 1447 Returns: bool 1448 """ 1449 if vm_obj: 1450 host_system = vm_obj.summary.runtime.host 1451 elif host_name: 1452 host_system = self.find_hostsystem_by_name(host_name=host_name) 1453 else: 1454 self.module.fail_json(msg='VM object or ESXi host name must be set one.') 1455 if host_system and version: 1456 host_version = host_system.summary.config.product.version 1457 return StrictVersion(host_version) >= StrictVersion('.'.join(map(str, version))) 1458 else: 1459 self.module.fail_json(msg='Unable to get the ESXi host from vm: %s, or hostname %s,' 1460 'or the passed ESXi version: %s is None.' % (vm_obj, host_name, version)) 1461 1462 # Network related functions 1463 @staticmethod 1464 def find_host_portgroup_by_name(host, portgroup_name): 1465 """ 1466 Find Portgroup on given host 1467 Args: 1468 host: Host config object 1469 portgroup_name: Name of portgroup 1470 1471 Returns: True if found else False 1472 1473 """ 1474 for portgroup in host.config.network.portgroup: 1475 if portgroup.spec.name == portgroup_name: 1476 return portgroup 1477 return False 1478 1479 def get_all_port_groups_by_host(self, host_system): 1480 """ 1481 Get all Port Group by host 1482 Args: 1483 host_system: Name of Host System 1484 1485 Returns: List of Port Group Spec 1486 """ 1487 pgs_list = [] 1488 for pg in host_system.config.network.portgroup: 1489 pgs_list.append(pg) 1490 return pgs_list 1491 1492 def find_network_by_name(self, network_name=None): 1493 """ 1494 Get network specified by name 1495 Args: 1496 network_name: Name of network 1497 1498 Returns: List of network managed objects 1499 """ 1500 networks = [] 1501 1502 if not network_name: 1503 return networks 1504 1505 objects = self.get_managed_objects_properties(vim_type=vim.Network, properties=['name']) 1506 1507 for temp_vm_object in objects: 1508 if len(temp_vm_object.propSet) != 1: 1509 continue 1510 for temp_vm_object_property in temp_vm_object.propSet: 1511 if temp_vm_object_property.val == network_name: 1512 networks.append(temp_vm_object.obj) 1513 break 1514 return networks 1515 1516 def network_exists_by_name(self, network_name=None): 1517 """ 1518 Check if network with a specified name exists or not 1519 Args: 1520 network_name: Name of network 1521 1522 Returns: True if network exists else False 1523 """ 1524 ret = False 1525 if not network_name: 1526 return ret 1527 ret = True if self.find_network_by_name(network_name=network_name) else False 1528 return ret 1529 1530 # Datacenter 1531 def find_datacenter_by_name(self, datacenter_name): 1532 """ 1533 Get datacenter managed object by name 1534 1535 Args: 1536 datacenter_name: Name of datacenter 1537 1538 Returns: datacenter managed object if found else None 1539 1540 """ 1541 return find_datacenter_by_name(self.content, datacenter_name=datacenter_name) 1542 1543 def is_datastore_valid(self, datastore_obj=None): 1544 """ 1545 Check if datastore selected is valid or not 1546 Args: 1547 datastore_obj: datastore managed object 1548 1549 Returns: True if datastore is valid, False if not 1550 """ 1551 if not datastore_obj \ 1552 or datastore_obj.summary.maintenanceMode != 'normal' \ 1553 or not datastore_obj.summary.accessible: 1554 return False 1555 return True 1556 1557 def find_datastore_by_name(self, datastore_name, datacenter_name=None): 1558 """ 1559 Get datastore managed object by name 1560 Args: 1561 datastore_name: Name of datastore 1562 datacenter_name: Name of datacenter where the datastore resides. This is needed because Datastores can be 1563 shared across Datacenters, so we need to specify the datacenter to assure we get the correct Managed Object Reference 1564 1565 Returns: datastore managed object if found else None 1566 1567 """ 1568 return find_datastore_by_name(self.content, datastore_name=datastore_name, datacenter_name=datacenter_name) 1569 1570 def find_folder_by_name(self, folder_name): 1571 """ 1572 Get vm folder managed object by name 1573 Args: 1574 folder_name: Name of the vm folder 1575 1576 Returns: vm folder managed object if found else None 1577 1578 """ 1579 return find_folder_by_name(self.content, folder_name=folder_name) 1580 1581 def find_folder_by_fqpn(self, folder_name, datacenter_name=None, folder_type=None): 1582 """ 1583 Get a unique folder managed object by specifying its Fully Qualified Path Name 1584 as datacenter/folder_type/sub1/sub2 1585 Args: 1586 folder_name: Fully Qualified Path Name folder name 1587 datacenter_name: Name of the datacenter, taken from Fully Qualified Path Name if not defined 1588 folder_type: Type of folder, vm, host, datastore or network, 1589 taken from Fully Qualified Path Name if not defined 1590 1591 Returns: folder managed object if found, else None 1592 1593 """ 1594 return find_folder_by_fqpn(self.content, folder_name=folder_name, datacenter_name=datacenter_name, folder_type=folder_type) 1595 1596 # Datastore cluster 1597 def find_datastore_cluster_by_name(self, datastore_cluster_name, datacenter=None, folder=None): 1598 """ 1599 Get datastore cluster managed object by name 1600 Args: 1601 datastore_cluster_name: Name of datastore cluster 1602 datacenter: Managed object of the datacenter 1603 folder: Managed object of the folder which holds datastore 1604 1605 Returns: Datastore cluster managed object if found else None 1606 1607 """ 1608 if datacenter and hasattr(datacenter, 'datastoreFolder'): 1609 folder = datacenter.datastoreFolder 1610 if not folder: 1611 folder = self.content.rootFolder 1612 1613 data_store_clusters = get_all_objs(self.content, [vim.StoragePod], folder=folder) 1614 for dsc in data_store_clusters: 1615 if dsc.name == datastore_cluster_name: 1616 return dsc 1617 return None 1618 1619 def get_recommended_datastore(self, datastore_cluster_obj=None): 1620 """ 1621 Return Storage DRS recommended datastore from datastore cluster 1622 Args: 1623 datastore_cluster_obj: datastore cluster managed object 1624 1625 Returns: Name of recommended datastore from the given datastore cluster 1626 1627 """ 1628 if datastore_cluster_obj is None: 1629 return None 1630 # Check if Datastore Cluster provided by user is SDRS ready 1631 sdrs_status = datastore_cluster_obj.podStorageDrsEntry.storageDrsConfig.podConfig.enabled 1632 if sdrs_status: 1633 # We can get storage recommendation only if SDRS is enabled on given datastorage cluster 1634 pod_sel_spec = vim.storageDrs.PodSelectionSpec() 1635 pod_sel_spec.storagePod = datastore_cluster_obj 1636 storage_spec = vim.storageDrs.StoragePlacementSpec() 1637 storage_spec.podSelectionSpec = pod_sel_spec 1638 storage_spec.type = 'create' 1639 1640 try: 1641 rec = self.content.storageResourceManager.RecommendDatastores(storageSpec=storage_spec) 1642 rec_action = rec.recommendations[0].action[0] 1643 return rec_action.destination.name 1644 except Exception: 1645 # There is some error so we fall back to general workflow 1646 pass 1647 datastore = None 1648 datastore_freespace = 0 1649 for ds in datastore_cluster_obj.childEntity: 1650 if isinstance(ds, vim.Datastore) and ds.summary.freeSpace > datastore_freespace: 1651 # If datastore field is provided, filter destination datastores 1652 if not self.is_datastore_valid(datastore_obj=ds): 1653 continue 1654 1655 datastore = ds 1656 datastore_freespace = ds.summary.freeSpace 1657 if datastore: 1658 return datastore.name 1659 return None 1660 1661 # Resource pool 1662 def find_resource_pool_by_name(self, resource_pool_name='Resources', folder=None): 1663 """ 1664 Get resource pool managed object by name 1665 Args: 1666 resource_pool_name: Name of resource pool 1667 1668 Returns: Resource pool managed object if found else None 1669 1670 """ 1671 if not folder: 1672 folder = self.content.rootFolder 1673 1674 resource_pools = get_all_objs(self.content, [vim.ResourcePool], folder=folder) 1675 for rp in resource_pools: 1676 if rp.name == resource_pool_name: 1677 return rp 1678 return None 1679 1680 def find_resource_pool_by_cluster(self, resource_pool_name='Resources', cluster=None): 1681 """ 1682 Get resource pool managed object by cluster object 1683 Args: 1684 resource_pool_name: Name of resource pool 1685 cluster: Managed object of cluster 1686 1687 Returns: Resource pool managed object if found else None 1688 1689 """ 1690 desired_rp = None 1691 if not cluster: 1692 return desired_rp 1693 1694 if resource_pool_name != 'Resources': 1695 # Resource pool name is different than default 'Resources' 1696 resource_pools = cluster.resourcePool.resourcePool 1697 if resource_pools: 1698 for rp in resource_pools: 1699 if rp.name == resource_pool_name: 1700 desired_rp = rp 1701 break 1702 else: 1703 desired_rp = cluster.resourcePool 1704 1705 return desired_rp 1706 1707 # VMDK stuff 1708 def vmdk_disk_path_split(self, vmdk_path): 1709 """ 1710 Takes a string in the format 1711 1712 [datastore_name] path/to/vm_name.vmdk 1713 1714 Returns a tuple with multiple strings: 1715 1716 1. datastore_name: The name of the datastore (without brackets) 1717 2. vmdk_fullpath: The "path/to/vm_name.vmdk" portion 1718 3. vmdk_filename: The "vm_name.vmdk" portion of the string (os.path.basename equivalent) 1719 4. vmdk_folder: The "path/to/" portion of the string (os.path.dirname equivalent) 1720 """ 1721 try: 1722 datastore_name = re.match(r'^\[(.*?)\]', vmdk_path, re.DOTALL).groups()[0] 1723 vmdk_fullpath = re.match(r'\[.*?\] (.*)$', vmdk_path).groups()[0] 1724 vmdk_filename = os.path.basename(vmdk_fullpath) 1725 vmdk_folder = os.path.dirname(vmdk_fullpath) 1726 return datastore_name, vmdk_fullpath, vmdk_filename, vmdk_folder 1727 except (IndexError, AttributeError) as e: 1728 self.module.fail_json(msg="Bad path '%s' for filename disk vmdk image: %s" % (vmdk_path, to_native(e))) 1729 1730 def find_vmdk_file(self, datastore_obj, vmdk_fullpath, vmdk_filename, vmdk_folder): 1731 """ 1732 Return vSphere file object or fail_json 1733 Args: 1734 datastore_obj: Managed object of datastore 1735 vmdk_fullpath: Path of VMDK file e.g., path/to/vm/vmdk_filename.vmdk 1736 vmdk_filename: Name of vmdk e.g., VM0001_1.vmdk 1737 vmdk_folder: Base dir of VMDK e.g, path/to/vm 1738 1739 """ 1740 1741 browser = datastore_obj.browser 1742 datastore_name = datastore_obj.name 1743 datastore_name_sq = "[" + datastore_name + "]" 1744 if browser is None: 1745 self.module.fail_json(msg="Unable to access browser for datastore %s" % datastore_name) 1746 1747 detail_query = vim.host.DatastoreBrowser.FileInfo.Details( 1748 fileOwner=True, 1749 fileSize=True, 1750 fileType=True, 1751 modification=True 1752 ) 1753 search_spec = vim.host.DatastoreBrowser.SearchSpec( 1754 details=detail_query, 1755 matchPattern=[vmdk_filename], 1756 searchCaseInsensitive=True, 1757 ) 1758 search_res = browser.SearchSubFolders( 1759 datastorePath=datastore_name_sq, 1760 searchSpec=search_spec 1761 ) 1762 1763 changed = False 1764 vmdk_path = datastore_name_sq + " " + vmdk_fullpath 1765 try: 1766 changed, result = wait_for_task(search_res) 1767 except TaskError as task_e: 1768 self.module.fail_json(msg=to_native(task_e)) 1769 1770 if not changed: 1771 self.module.fail_json(msg="No valid disk vmdk image found for path %s" % vmdk_path) 1772 1773 target_folder_paths = [ 1774 datastore_name_sq + " " + vmdk_folder + '/', 1775 datastore_name_sq + " " + vmdk_folder, 1776 ] 1777 1778 for file_result in search_res.info.result: 1779 for f in getattr(file_result, 'file'): 1780 if f.path == vmdk_filename and file_result.folderPath in target_folder_paths: 1781 return f 1782 1783 self.module.fail_json(msg="No vmdk file found for path specified [%s]" % vmdk_path) 1784 1785 def find_first_class_disk_by_name(self, disk_name, datastore_obj): 1786 """ 1787 Get first-class disk managed object by name 1788 Args: 1789 disk_name: Name of the first-class disk 1790 datastore_obj: Managed object of datastore 1791 1792 Returns: First-class disk managed object if found else None 1793 1794 """ 1795 1796 if self.is_vcenter(): 1797 for id in self.content.vStorageObjectManager.ListVStorageObject(datastore_obj): 1798 disk = self.content.vStorageObjectManager.RetrieveVStorageObject(id, datastore_obj) 1799 if disk.config.name == disk_name: 1800 return disk 1801 else: 1802 for id in self.content.vStorageObjectManager.HostListVStorageObject(datastore_obj): 1803 disk = self.content.vStorageObjectManager.HostRetrieveVStorageObject(id, datastore_obj) 1804 if disk.config.name == disk_name: 1805 return disk 1806 1807 return None 1808 1809 # 1810 # Conversion to JSON 1811 # 1812 1813 def _deepmerge(self, d, u): 1814 """ 1815 Deep merges u into d. 1816 1817 Credit: 1818 https://bit.ly/2EDOs1B (stackoverflow question 3232943) 1819 License: 1820 cc-by-sa 3.0 (https://creativecommons.org/licenses/by-sa/3.0/) 1821 Changes: 1822 using collections_compat for compatibility 1823 1824 Args: 1825 - d (dict): dict to merge into 1826 - u (dict): dict to merge into d 1827 1828 Returns: 1829 dict, with u merged into d 1830 """ 1831 for k, v in iteritems(u): 1832 if isinstance(v, collections_compat.Mapping): 1833 d[k] = self._deepmerge(d.get(k, {}), v) 1834 else: 1835 d[k] = v 1836 return d 1837 1838 def _extract(self, data, remainder): 1839 """ 1840 This is used to break down dotted properties for extraction. 1841 1842 Args: 1843 - data (dict): result of _jsonify on a property 1844 - remainder: the remainder of the dotted property to select 1845 1846 Return: 1847 dict 1848 """ 1849 result = dict() 1850 if '.' not in remainder: 1851 result[remainder] = data[remainder] 1852 return result 1853 key, remainder = remainder.split('.', 1) 1854 if isinstance(data, list): 1855 temp_ds = [] 1856 for i in range(len(data)): 1857 temp_ds.append(self._extract(data[i][key], remainder)) 1858 result[key] = temp_ds 1859 else: 1860 result[key] = self._extract(data[key], remainder) 1861 return result 1862 1863 def _jsonify(self, obj): 1864 """ 1865 Convert an object from pyVmomi into JSON. 1866 1867 Args: 1868 - obj (object): vim object 1869 1870 Return: 1871 dict 1872 """ 1873 return json.loads(json.dumps(obj, cls=VmomiSupport.VmomiJSONEncoder, 1874 sort_keys=True, strip_dynamic=True)) 1875 1876 def to_json(self, obj, properties=None): 1877 """ 1878 Convert a vSphere (pyVmomi) Object into JSON. This is a deep 1879 transformation. The list of properties is optional - if not 1880 provided then all properties are deeply converted. The resulting 1881 JSON is sorted to improve human readability. 1882 1883 Requires upstream support from pyVmomi > 6.7.1 1884 (https://github.com/vmware/pyvmomi/pull/732) 1885 1886 Args: 1887 - obj (object): vim object 1888 - properties (list, optional): list of properties following 1889 the property collector specification, for example: 1890 ["config.hardware.memoryMB", "name", "overallStatus"] 1891 default is a complete object dump, which can be large 1892 1893 Return: 1894 dict 1895 """ 1896 if not HAS_PYVMOMIJSON: 1897 self.module.fail_json(msg='The installed version of pyvmomi lacks JSON output support; need pyvmomi>6.7.1') 1898 1899 result = dict() 1900 if properties: 1901 for prop in properties: 1902 try: 1903 if '.' in prop: 1904 key, remainder = prop.split('.', 1) 1905 tmp = dict() 1906 tmp[key] = self._extract(self._jsonify(getattr(obj, key)), remainder) 1907 self._deepmerge(result, tmp) 1908 else: 1909 result[prop] = self._jsonify(getattr(obj, prop)) 1910 # To match gather_vm_facts output 1911 prop_name = prop 1912 if prop.lower() == '_moid': 1913 prop_name = 'moid' 1914 elif prop.lower() == '_vimref': 1915 prop_name = 'vimref' 1916 result[prop_name] = result[prop] 1917 except (AttributeError, KeyError): 1918 self.module.fail_json(msg="Property '{0}' not found.".format(prop)) 1919 else: 1920 result = self._jsonify(obj) 1921 return result 1922 1923 def get_folder_path(self, cur): 1924 full_path = '/' + cur.name 1925 while hasattr(cur, 'parent') and cur.parent: 1926 if cur.parent == self.content.rootFolder: 1927 break 1928 cur = cur.parent 1929 full_path = '/' + cur.name + full_path 1930 return full_path 1931 1932 def find_obj_by_moid(self, object_type, moid): 1933 """ 1934 Get Managed Object based on an object type and moid. 1935 If you'd like to search for a virtual machine, recommended you use get_vm method. 1936 1937 Args: 1938 - object_type: Managed Object type 1939 It is possible to specify types the following. 1940 ["Datacenter", "ClusterComputeResource", "ResourcePool", "Folder", "HostSystem", 1941 "VirtualMachine", "DistributedVirtualSwitch", "DistributedVirtualPortgroup", "Datastore"] 1942 - moid: moid of Managed Object 1943 :return: Managed Object if it exists else None 1944 """ 1945 1946 obj = VmomiSupport.templateOf(object_type)(moid, self.si._stub) 1947 try: 1948 getattr(obj, 'name') 1949 except vmodl.fault.ManagedObjectNotFound: 1950 obj = None 1951 1952 return obj 1953