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