1"""
2Connection library for VMware vSAN endpoint
3
4This library used the vSAN extension of the VMware SDK
5used to manage vSAN related objects
6
7:codeauthor: Alexandru Bleotu <alexandru.bleotu@morganstaley.com>
8
9Dependencies
10~~~~~~~~~~~~
11
12- pyVmomi Python Module
13
14pyVmomi
15-------
16
17PyVmomi can be installed via pip:
18
19.. code-block:: bash
20
21    pip install pyVmomi
22
23.. note::
24
25    versions of Python. If using version 6.0 of pyVmomi, Python 2.6,
26    Python 2.7.9, or newer must be present. This is due to an upstream dependency
27    in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the
28    version of Python is not in the supported range, you will need to install an
29    earlier version of pyVmomi. See `Issue #29537`_ for more information.
30
31.. _Issue #29537: https://github.com/saltstack/salt/issues/29537
32
33Based on the note above, to install an earlier version of pyVmomi than the
34version currently listed in PyPi, run the following:
35
36.. code-block:: bash
37
38    pip install pyVmomi==5.5.0.2014.1.1
39
40The 5.5.0.2014.1.1 is a known stable version that this original VMware utils file
41was developed against.
42"""
43
44
45import logging
46import ssl
47import sys
48
49import salt.utils.vmware
50from salt.exceptions import (
51    VMwareApiError,
52    VMwareObjectRetrievalError,
53    VMwareRuntimeError,
54)
55
56try:
57    from pyVmomi import vim, vmodl  # pylint: disable=no-name-in-module
58
59    HAS_PYVMOMI = True
60except ImportError:
61    HAS_PYVMOMI = False
62
63
64try:
65    from salt.ext.vsan import vsanapiutils
66
67    HAS_PYVSAN = True
68except ImportError:
69    HAS_PYVSAN = False
70
71# Get Logging Started
72log = logging.getLogger(__name__)
73
74
75def __virtual__():
76    """
77    Only load if PyVmomi is installed.
78    """
79    if HAS_PYVSAN and HAS_PYVMOMI:
80        return True
81    else:
82        return (
83            False,
84            "Missing dependency: The salt.utils.vsan module "
85            "requires pyvmomi and the pyvsan extension library",
86        )
87
88
89def vsan_supported(service_instance):
90    """
91    Returns whether vsan is supported on the vCenter:
92        api version needs to be 6 or higher
93
94    service_instance
95        Service instance to the host or vCenter
96    """
97    try:
98        api_version = service_instance.content.about.apiVersion
99    except vim.fault.NoPermission as exc:
100        log.exception(exc)
101        raise VMwareApiError(
102            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
103        )
104    except vim.fault.VimFault as exc:
105        log.exception(exc)
106        raise VMwareApiError(exc.msg)
107    except vmodl.RuntimeFault as exc:
108        log.exception(exc)
109        raise VMwareRuntimeError(exc.msg)
110    if int(api_version.split(".")[0]) < 6:
111        return False
112    return True
113
114
115def get_vsan_cluster_config_system(service_instance):
116    """
117    Returns a vim.cluster.VsanVcClusterConfigSystem object
118
119    service_instance
120        Service instance to the host or vCenter
121    """
122
123    # TODO Replace when better connection mechanism is available
124
125    # For python 2.7.9 and later, the default SSL conext has more strict
126    # connection handshaking rule. We may need turn of the hostname checking
127    # and client side cert verification
128    context = None
129    if sys.version_info[:3] > (2, 7, 8):
130        context = ssl.create_default_context()
131        context.check_hostname = False
132        context.verify_mode = ssl.CERT_NONE
133
134    stub = service_instance._stub
135    vc_mos = vsanapiutils.GetVsanVcMos(stub, context=context)
136    return vc_mos["vsan-cluster-config-system"]
137
138
139def get_vsan_disk_management_system(service_instance):
140    """
141    Returns a vim.VimClusterVsanVcDiskManagementSystem object
142
143    service_instance
144        Service instance to the host or vCenter
145    """
146
147    # TODO Replace when better connection mechanism is available
148
149    # For python 2.7.9 and later, the default SSL conext has more strict
150    # connection handshaking rule. We may need turn of the hostname checking
151    # and client side cert verification
152    context = None
153    if sys.version_info[:3] > (2, 7, 8):
154        context = ssl.create_default_context()
155        context.check_hostname = False
156        context.verify_mode = ssl.CERT_NONE
157
158    stub = service_instance._stub
159    vc_mos = vsanapiutils.GetVsanVcMos(stub, context=context)
160    return vc_mos["vsan-disk-management-system"]
161
162
163def get_host_vsan_system(service_instance, host_ref, hostname=None):
164    """
165    Returns a host's vsan system
166
167    service_instance
168        Service instance to the host or vCenter
169
170    host_ref
171        Refernce to ESXi host
172
173    hostname
174        Name of ESXi host. Default value is None.
175    """
176    if not hostname:
177        hostname = salt.utils.vmware.get_managed_object_name(host_ref)
178    traversal_spec = vmodl.query.PropertyCollector.TraversalSpec(
179        path="configManager.vsanSystem", type=vim.HostSystem, skip=False
180    )
181    objs = salt.utils.vmware.get_mors_with_properties(
182        service_instance,
183        vim.HostVsanSystem,
184        property_list=["config.enabled"],
185        container_ref=host_ref,
186        traversal_spec=traversal_spec,
187    )
188    if not objs:
189        raise VMwareObjectRetrievalError(
190            "Host's '{}' VSAN system was not retrieved".format(hostname)
191        )
192    log.trace("[%s] Retrieved VSAN system", hostname)
193    return objs[0]["object"]
194
195
196def create_diskgroup(
197    service_instance, vsan_disk_mgmt_system, host_ref, cache_disk, capacity_disks
198):
199    """
200    Creates a disk group
201
202    service_instance
203        Service instance to the host or vCenter
204
205    vsan_disk_mgmt_system
206        vim.VimClusterVsanVcDiskManagemenetSystem representing the vSan disk
207        management system retrieved from the vsan endpoint.
208
209    host_ref
210        vim.HostSystem object representing the target host the disk group will
211        be created on
212
213    cache_disk
214        The vim.HostScsidisk to be used as a cache disk. It must be an ssd disk.
215
216    capacity_disks
217        List of vim.HostScsiDisk objects representing of disks to be used as
218        capacity disks. Can be either ssd or non-ssd. There must be a minimum
219        of 1 capacity disk in the list.
220    """
221    hostname = salt.utils.vmware.get_managed_object_name(host_ref)
222    cache_disk_id = cache_disk.canonicalName
223    log.debug(
224        "Creating a new disk group with cache disk '%s' on host '%s'",
225        cache_disk_id,
226        hostname,
227    )
228    log.trace("capacity_disk_ids = %s", [c.canonicalName for c in capacity_disks])
229    spec = vim.VimVsanHostDiskMappingCreationSpec()
230    spec.cacheDisks = [cache_disk]
231    spec.capacityDisks = capacity_disks
232    # All capacity disks must be either ssd or non-ssd (mixed disks are not
233    # supported)
234    spec.creationType = "allFlash" if getattr(capacity_disks[0], "ssd") else "hybrid"
235    spec.host = host_ref
236    try:
237        task = vsan_disk_mgmt_system.InitializeDiskMappings(spec)
238    except vim.fault.NoPermission as exc:
239        log.exception(exc)
240        raise VMwareApiError(
241            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
242        )
243    except vim.fault.VimFault as exc:
244        log.exception(exc)
245        raise VMwareApiError(exc.msg)
246    except vmodl.fault.MethodNotFound as exc:
247        log.exception(exc)
248        raise VMwareRuntimeError("Method '{}' not found".format(exc.method))
249    except vmodl.RuntimeFault as exc:
250        log.exception(exc)
251        raise VMwareRuntimeError(exc.msg)
252    _wait_for_tasks([task], service_instance)
253    return True
254
255
256def add_capacity_to_diskgroup(
257    service_instance, vsan_disk_mgmt_system, host_ref, diskgroup, new_capacity_disks
258):
259    """
260    Adds capacity disk(s) to a disk group.
261
262    service_instance
263        Service instance to the host or vCenter
264
265    vsan_disk_mgmt_system
266        vim.VimClusterVsanVcDiskManagemenetSystem representing the vSan disk
267        management system retrieved from the vsan endpoint.
268
269    host_ref
270        vim.HostSystem object representing the target host the disk group will
271        be created on
272
273    diskgroup
274        The vsan.HostDiskMapping object representing the host's diskgroup where
275        the additional capacity needs to be added
276
277    new_capacity_disks
278        List of vim.HostScsiDisk objects representing the disks to be added as
279        capacity disks. Can be either ssd or non-ssd. There must be a minimum
280        of 1 new capacity disk in the list.
281    """
282    hostname = salt.utils.vmware.get_managed_object_name(host_ref)
283    cache_disk = diskgroup.ssd
284    cache_disk_id = cache_disk.canonicalName
285    log.debug(
286        "Adding capacity to disk group with cache disk '%s' on host '%s'",
287        cache_disk_id,
288        hostname,
289    )
290    log.trace(
291        "new_capacity_disk_ids = %s", [c.canonicalName for c in new_capacity_disks]
292    )
293    spec = vim.VimVsanHostDiskMappingCreationSpec()
294    spec.cacheDisks = [cache_disk]
295    spec.capacityDisks = new_capacity_disks
296    # All new capacity disks must be either ssd or non-ssd (mixed disks are not
297    # supported); also they need to match the type of the existing capacity
298    # disks; we assume disks are already validated
299    spec.creationType = (
300        "allFlash" if getattr(new_capacity_disks[0], "ssd") else "hybrid"
301    )
302    spec.host = host_ref
303    try:
304        task = vsan_disk_mgmt_system.InitializeDiskMappings(spec)
305    except vim.fault.NoPermission as exc:
306        log.exception(exc)
307        raise VMwareApiError(
308            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
309        )
310    except vim.fault.VimFault as exc:
311        log.exception(exc)
312        raise VMwareApiError(exc.msg)
313    except vmodl.fault.MethodNotFound as exc:
314        log.exception(exc)
315        raise VMwareRuntimeError("Method '{}' not found".format(exc.method))
316    except vmodl.RuntimeFault as exc:
317        raise VMwareRuntimeError(exc.msg)
318    _wait_for_tasks([task], service_instance)
319    return True
320
321
322def remove_capacity_from_diskgroup(
323    service_instance,
324    host_ref,
325    diskgroup,
326    capacity_disks,
327    data_evacuation=True,
328    hostname=None,
329    host_vsan_system=None,
330):
331    """
332    Removes capacity disk(s) from a disk group.
333
334    service_instance
335        Service instance to the host or vCenter
336
337    host_vsan_system
338        ESXi host's VSAN system
339
340    host_ref
341        Reference to the ESXi host
342
343    diskgroup
344        The vsan.HostDiskMapping object representing the host's diskgroup from
345        where the capacity needs to be removed
346
347    capacity_disks
348        List of vim.HostScsiDisk objects representing the capacity disks to be
349        removed. Can be either ssd or non-ssd. There must be a minimum
350        of 1 capacity disk in the list.
351
352    data_evacuation
353        Specifies whether to gracefully evacuate the data on the capacity disks
354        before removing them from the disk group. Default value is True.
355
356    hostname
357        Name of ESXi host. Default value is None.
358
359    host_vsan_system
360        ESXi host's VSAN system. Default value is None.
361    """
362    if not hostname:
363        hostname = salt.utils.vmware.get_managed_object_name(host_ref)
364    cache_disk = diskgroup.ssd
365    cache_disk_id = cache_disk.canonicalName
366    log.debug(
367        "Removing capacity from disk group with cache disk '%s' on host '%s'",
368        cache_disk_id,
369        hostname,
370    )
371    log.trace("capacity_disk_ids = %s", [c.canonicalName for c in capacity_disks])
372    if not host_vsan_system:
373        host_vsan_system = get_host_vsan_system(service_instance, host_ref, hostname)
374    # Set to evacuate all data before removing the disks
375    maint_spec = vim.HostMaintenanceSpec()
376    maint_spec.vsanMode = vim.VsanHostDecommissionMode()
377    if data_evacuation:
378        maint_spec.vsanMode.objectAction = (
379            vim.VsanHostDecommissionModeObjectAction.evacuateAllData
380        )
381    else:
382        maint_spec.vsanMode.objectAction = (
383            vim.VsanHostDecommissionModeObjectAction.noAction
384        )
385    try:
386        task = host_vsan_system.RemoveDisk_Task(
387            disk=capacity_disks, maintenanceSpec=maint_spec
388        )
389    except vim.fault.NoPermission as exc:
390        log.exception(exc)
391        raise VMwareApiError(
392            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
393        )
394    except vim.fault.VimFault as exc:
395        log.exception(exc)
396        raise VMwareApiError(exc.msg)
397    except vmodl.RuntimeFault as exc:
398        log.exception(exc)
399        raise VMwareRuntimeError(exc.msg)
400    salt.utils.vmware.wait_for_task(task, hostname, "remove_capacity")
401    return True
402
403
404def remove_diskgroup(
405    service_instance,
406    host_ref,
407    diskgroup,
408    hostname=None,
409    host_vsan_system=None,
410    erase_disk_partitions=False,
411    data_accessibility=True,
412):
413    """
414    Removes a disk group.
415
416    service_instance
417        Service instance to the host or vCenter
418
419    host_ref
420        Reference to the ESXi host
421
422    diskgroup
423        The vsan.HostDiskMapping object representing the host's diskgroup from
424        where the capacity needs to be removed
425
426    hostname
427        Name of ESXi host. Default value is None.
428
429    host_vsan_system
430        ESXi host's VSAN system. Default value is None.
431
432    data_accessibility
433        Specifies whether to ensure data accessibility. Default value is True.
434    """
435    if not hostname:
436        hostname = salt.utils.vmware.get_managed_object_name(host_ref)
437    cache_disk_id = diskgroup.ssd.canonicalName
438    log.debug(
439        "Removing disk group with cache disk '%s' on host '%s'",
440        cache_disk_id,
441        hostname,
442    )
443    if not host_vsan_system:
444        host_vsan_system = get_host_vsan_system(service_instance, host_ref, hostname)
445    # Set to evacuate all data before removing the disks
446    maint_spec = vim.HostMaintenanceSpec()
447    maint_spec.vsanMode = vim.VsanHostDecommissionMode()
448    object_action = vim.VsanHostDecommissionModeObjectAction
449    if data_accessibility:
450        maint_spec.vsanMode.objectAction = object_action.ensureObjectAccessibility
451    else:
452        maint_spec.vsanMode.objectAction = object_action.noAction
453    try:
454        task = host_vsan_system.RemoveDiskMapping_Task(
455            mapping=[diskgroup], maintenanceSpec=maint_spec
456        )
457    except vim.fault.NoPermission as exc:
458        log.exception(exc)
459        raise VMwareApiError(
460            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
461        )
462    except vim.fault.VimFault as exc:
463        log.exception(exc)
464        raise VMwareApiError(exc.msg)
465    except vmodl.RuntimeFault as exc:
466        log.exception(exc)
467        raise VMwareRuntimeError(exc.msg)
468    salt.utils.vmware.wait_for_task(task, hostname, "remove_diskgroup")
469    log.debug(
470        "Removed disk group with cache disk '%s' on host '%s'", cache_disk_id, hostname
471    )
472    return True
473
474
475def get_cluster_vsan_info(cluster_ref):
476    """
477    Returns the extended cluster vsan configuration object
478    (vim.VsanConfigInfoEx).
479
480    cluster_ref
481        Reference to the cluster
482    """
483
484    cluster_name = salt.utils.vmware.get_managed_object_name(cluster_ref)
485    log.trace("Retrieving cluster vsan info of cluster '%s'", cluster_name)
486    si = salt.utils.vmware.get_service_instance_from_managed_object(cluster_ref)
487    vsan_cl_conf_sys = get_vsan_cluster_config_system(si)
488    try:
489        return vsan_cl_conf_sys.VsanClusterGetConfig(cluster_ref)
490    except vim.fault.NoPermission as exc:
491        log.exception(exc)
492        raise VMwareApiError(
493            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
494        )
495    except vim.fault.VimFault as exc:
496        log.exception(exc)
497        raise VMwareApiError(exc.msg)
498    except vmodl.RuntimeFault as exc:
499        log.exception(exc)
500        raise VMwareRuntimeError(exc.msg)
501
502
503def reconfigure_cluster_vsan(cluster_ref, cluster_vsan_spec):
504    """
505    Reconfigures the VSAN system of a cluster.
506
507    cluster_ref
508        Reference to the cluster
509
510    cluster_vsan_spec
511        Cluster VSAN reconfigure spec (vim.vsan.ReconfigSpec).
512    """
513    cluster_name = salt.utils.vmware.get_managed_object_name(cluster_ref)
514    log.trace("Reconfiguring vsan on cluster '%s': %s", cluster_name, cluster_vsan_spec)
515    si = salt.utils.vmware.get_service_instance_from_managed_object(cluster_ref)
516    vsan_cl_conf_sys = salt.utils.vsan.get_vsan_cluster_config_system(si)
517    try:
518        task = vsan_cl_conf_sys.VsanClusterReconfig(cluster_ref, cluster_vsan_spec)
519    except vim.fault.NoPermission as exc:
520        log.exception(exc)
521        raise VMwareApiError(
522            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
523        )
524    except vim.fault.VimFault as exc:
525        log.exception(exc)
526        raise VMwareApiError(exc.msg)
527    except vmodl.RuntimeFault as exc:
528        log.exception(exc)
529        raise VMwareRuntimeError(exc.msg)
530    _wait_for_tasks([task], si)
531
532
533def _wait_for_tasks(tasks, service_instance):
534    """
535    Wait for tasks created via the VSAN API
536    """
537    log.trace("Waiting for vsan tasks: {0}", ", ".join([str(t) for t in tasks]))
538    try:
539        vsanapiutils.WaitForTasks(tasks, service_instance)
540    except vim.fault.NoPermission as exc:
541        log.exception(exc)
542        raise VMwareApiError(
543            "Not enough permissions. Required privilege: {}".format(exc.privilegeId)
544        )
545    except vim.fault.VimFault as exc:
546        log.exception(exc)
547        raise VMwareApiError(exc.msg)
548    except vmodl.RuntimeFault as exc:
549        log.exception(exc)
550        raise VMwareRuntimeError(exc.msg)
551    log.trace("Tasks %s finished successfully", ", ".join([str(t) for t in tasks]))
552