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