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