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