diff --git a/src/saltext/vmware/modules/datacenter.py b/src/saltext/vmware/modules/datacenter.py index 71ae9778..09a873e5 100644 --- a/src/saltext/vmware/modules/datacenter.py +++ b/src/saltext/vmware/modules/datacenter.py @@ -67,8 +67,8 @@ def create(name, service_instance=None, profile=None): try: utils_datacenter.create_datacenter(service_instance, name) except salt.exceptions.VMwareApiError as exc: - return {name: False, "reason": str(exc)} - return {name: True} + return {name: False, "reason": str(exc), "success": False} + return {name: True, "success": True} def get(name, service_instance=None, profile=None): @@ -103,7 +103,19 @@ def get(name, service_instance=None, profile=None): ret = dc[0] except (salt.exceptions.VMwareApiError, salt.exceptions.VMwareObjectRetrievalError) as exc: return {name: False, "reason": str(exc)} - return ret + + vm_folders = utils_common.get_all_folders(service_instance, datacenter_name=None, datacenter=dc_ref) + ds_folders = utils_common.get_all_folders(service_instance, datacenter_name=None, datacenter=dc_ref, folder_key="datastoreFolder") + hs_folders = utils_common.get_all_folders(service_instance, datacenter_name=None, datacenter=dc_ref, folder_key="networkFolder") + nt_folders = utils_common.get_all_folders(service_instance, datacenter_name=None, datacenter=dc_ref, folder_key="hostFolder") + return { + "name": ret["name"], + "status": ret["overallStatus"], + "vm_folders": [f.name for f in vm_folders], + "ds_folders": [f.name for f in ds_folders], + "hst_folders": [f.name for f in hs_folders], + "ntwk_folders": [f.name for f in nt_folders], + } def delete(name, service_instance=None, profile=None): @@ -131,5 +143,5 @@ def delete(name, service_instance=None, profile=None): try: utils_datacenter.delete_datacenter(service_instance, name) except (salt.exceptions.VMwareApiError, salt.exceptions.VMwareObjectRetrievalError) as exc: - return {name: False, "reason": str(exc)} - return {name: True} + return {name: False, "reason": str(exc), "success": False} + return {name: True, "success": True} diff --git a/src/saltext/vmware/modules/datastore.py b/src/saltext/vmware/modules/datastore.py index eb8b7619..5c6f1fd0 100644 --- a/src/saltext/vmware/modules/datastore.py +++ b/src/saltext/vmware/modules/datastore.py @@ -144,6 +144,7 @@ def get( "type": summary.type, "url": summary.url, "uncommitted": summary.uncommitted if summary.uncommitted else 0, + "folder": utils_common.get_path(datastore, service_instance), } ret.append(info) @@ -230,7 +231,6 @@ def list_datastores( datastores = utils_datastore.get_datastores_from_ref( service_instance, host, datastore_names, backing_disk_ids, get_all_datastores ) - # Search for disk backed datastores if target is host # to be able to add the backing_disk_ids mount_infos = [] @@ -247,17 +247,18 @@ def list_datastores( "type": datastore.summary.type, "free_space": datastore.summary.freeSpace, "capacity": datastore.summary.capacity, + "uuid": datastore.info.vmfs.uuid, } - backing_disk_ids = [] + backing_disk_id_list = [] for vol in [ i.volume for i in mount_infos if i.volume.name == datastore.name and isinstance(i.volume, vim.HostVmfsVolume) ]: - backing_disk_ids.extend([e.diskName for e in vol.extent]) - if backing_disk_ids: - datastore_dict["backing_disk_ids"] = backing_disk_ids + backing_disk_id_list.extend([e.diskName for e in vol.extent]) + if backing_disk_id_list: + datastore_dict["backing_disk_ids"] = backing_disk_id_list ret[host.name].append(datastore_dict) return ret @@ -408,3 +409,211 @@ def list_datastore_clusters( get_all_hosts=host_name is None, ) return utils_datastore.list_datastore_clusters(service_instance) + +def unmount_datastore( + datastore_name, + datacenter_name=None, + cluster_name=None, + host_name=None, + service_instance=None, + profile=None, +): + """ + unmounts a datastore and detaches the backing disk + + datastore_name + Name of the datastore to unmount + + datacenter_name + Filter by this datacenter name (required when cluster is not specified) + + cluster_name + Filter by this cluster name (required when datacenter is not specified) + + host_name + Filter by this ESXi hostname (optional). + + service_instance + Use this vCenter service connection instance instead of creating a new one. (optional). + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_datastore.unmount_datastore datastore_name=ds1 + salt '*' vmware_datastore.unmount_datastore datastore_name=ds1 cluster_name=cl1 + """ + log.debug("Running vmware_datastore.unmount_datastore") + ret = {"success": True} + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + for host in hosts: + ret[host.name] = [] + datastores = utils_datastore.get_datastores_from_ref( + service_instance, host, [datastore_name], None, False + ) + if isinstance(host, vim.HostSystem): + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + props = utils_common.get_properties_of_managed_object( + storage_system, ["fileSystemVolumeInfo.mountInfo"] + ) + mount_infos = props.get("fileSystemVolumeInfo.mountInfo", []) + + for datastore in datastores: + for host_mount in datastore.host: + if host_mount.key.name == host.name: + if host_mount.mountInfo.mounted: + try: + ret[host.name].append(f"unmounting vmfs {datastore.info.vmfs.uuid} from {host.name}") + storage_system.UnmountVmfsVolume(datastore.info.vmfs.uuid) + except Exception as err: + ret["success"] = False + ret[host.name].append((False, f"Error unmounting vmfs: {err}")) + return ret + else: + ret[host.name].append(f"vmfs {datastore.info.vmfs.uuid} not mounted on {host.name}") + try: + for vol in [ + i.volume + for i in mount_infos + if i.volume.name == datastore.name and isinstance(i.volume, vim.HostVmfsVolume) + ]: + for dsk in vol.extent: + ret[host.name].append(f"detaching disk {dsk.diskName} from {host.name}") + storage_system.DetachScsiLun(dsk.diskName) + except Exception as err: + ret["success"] = False + ret[host.name].append((False, f"Error detaching lun: {err}")) + return ret + + storage_system.RefreshStorageSystem() + ret[host.name].append(f"rescanning storage on {host.name}") + return ret + +def mount_datastore( + datastore_name, + vmfs_uuid, + disk_id, + datacenter_name=None, + cluster_name=None, + host_name=None, + folder_name=None, + service_instance=None, + profile=None, +): + """ + attaches a lun and mounts the datastore in the lun + + datastore_name + The name of the datastore that will be attached + + vmfs_uuid + The uuid of the VMFS datastore to mount + + disk_id + The id of the scsi lun to attach + + datacenter_name + Filter by this datacenter name (required when cluster is not specified) + + cluster_name + Filter by this cluster name (required when datacenter is not specified) + + host_name + Filter by this ESXi hostname (optional). + + folder_name + The name of the folder to put the datastore in (optional) + + service_instance + Use this vCenter service connection instance instead of creating a new one. (optional). + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_datastore.mount_datastore datastore_name=ds1 vmfs_uuid=xxxx=-xxx-xxxxx disk_id=naa.xxxxxxxx + salt '*' vmware_datastore.mount_datastore datastore_name=ds1 vmfs_uuid=xxxx=-xxx-xxxxx disk_id=naa.xxxxxxxx cluster_name=cl1 + """ + log.debug("Running vmware_datastore.mount_datastore") + ret = {"success": True} + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + for host in hosts: + ret[host.name] = [] + datastores = utils_datastore.get_datastores_from_ref( + service_instance, host, [datastore_name], None, False + ) + ds = utils_vmware.get_host_datastore_system(host) + if not datastores: + if isinstance(host, vim.HostSystem): + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + props = utils_common.get_properties_of_managed_object( + storage_system, ["fileSystemVolumeInfo.mountInfo"] + ) + try: + ret[host.name].append(f"attaching disk {disk_id} to {host.name}") + storage_system.AttachScsiLun(disk_id) + except vim.fault.InvalidState: + ret[host.name].append(f"disk {disk_id} already attached to {host.name}") + + storage_system.RefreshStorageSystem() + ret[host.name].append(f"rescanning storage on {host.name}") + try: + ret[host.name].append(f"mounting vmfs {vmfs_uuid} to {host.name}") + storage_system.MountVmfsVolume(vmfs_uuid) + except vim.fault.NotFound as err: + ret[host.name].append(f"vmfs {vmfs_uuid} not found") + unresolved_vols = storage_system.QueryUnresolvedVmfsVolume() + if unresolved_vols: + ret[host.name].append(f"unresolved volumes found") + for un_vol in unresolved_vols: + if un_vol.vmfsUuid == vmfs_uuid: + if un_vol.resolveStatus.resolvable: + ret[host.name].append(f"{vmfs_uuid} has a resolvable conflict") + spec = vim.host.UnresolvedVmfsResolutionSpec() + paths = [] + for extent in un_vol.extent: + paths.append(extent.devicePath) + spec.extentDevicePath = paths + spec.uuidResolution = "forceMounted" + try: + ret[host.name].append(f"resolving {vmfs_uuid}") + storage_system.ResolveMultipleUnresolvedVmfsVolumes([spec]) + except Exception as err: + ret["success"] = False + ret[host.name].append(f"Error: {err}") + else: + ret["success"] = False + ret[host.name].append((False, f"{vmfs_uuid} has an unresolvable conflict")) + except vim.fault.InvalidState: + ret[host.name].append(f"vmfs {vmfs_uuid} already mounted") + + storage_system.RefreshStorageSystem() + ret[host.name].append(f"rescanning storage on {host.name}") + else: + ret[host.name].append("datastore already mounted") + + return ret diff --git a/src/saltext/vmware/modules/dvportgroup.py b/src/saltext/vmware/modules/dvportgroup.py index 8583fe7c..3d1d8bf5 100644 --- a/src/saltext/vmware/modules/dvportgroup.py +++ b/src/saltext/vmware/modules/dvportgroup.py @@ -25,7 +25,7 @@ def __virtual__(): return __virtualname__ -def get(switch_name, portgroup_key, host_name=None, service_instance=None, profile=None): +def get(switch_name, portgroup_id=None, host_name=None, service_instance=None, profile=None): """ Get distributed portgroup from a distributed switch optionally from a host. @@ -35,9 +35,9 @@ def get(switch_name, portgroup_key, host_name=None, service_instance=None, profi switch_name Name of the distributed switch. - portgroup_key - Portgroup key. - + portgroup_id + Portgroup key or name. (Optional). + host_name Name of the ESXi host. (optional). @@ -47,25 +47,32 @@ def get(switch_name, portgroup_key, host_name=None, service_instance=None, profi profile Profile to use (optional) """ - ret = {} + ret = [] service_instance = service_instance or connect.get_service_instance( config=__opts__, profile=profile ) switch_ref = utils_vmware._get_dvs(service_instance=service_instance, dvs_name=switch_name) - if switch_ref: for portgroup in switch_ref.portgroup: - if portgroup.key == portgroup_key: - ret["name"] = portgroup.config.name - ret["vlan"] = portgroup.config.defaultPortConfig.vlan.vlanId - ret["pnic"] = [] - if host_name: - for host in portgroup.config.distributedVirtualSwitch.config.host: - if host.config.host.name == host_name: - for pnic in host.config.backing.pnicSpec: - ret["pnic"].append(pnic.pnicDevice) + pg = {} + pg["name"] = portgroup.config.name + pg["key"] = portgroup.config.key + pg["vlan"] = portgroup.config.defaultPortConfig.vlan.vlanId + pg["ports"] = portgroup.config.numPorts + pg["pnic"] = [] + if host_name: + for host in portgroup.config.distributedVirtualSwitch.config.host: + if host.config.host.name == host_name: + for pnic in host.config.backing.pnicSpec: + pg["pnic"].append(pnic.pnicDevice) + + if portgroup_id and portgroup_id in [portgroup.config.name, portgroup.config.key]: + ret.append(pg) + elif not portgroup_id: + ret.append(pg) + ret = json.loads(json.dumps(ret, cls=VmomiSupport.VmomiJSONEncoder)) return ret diff --git a/src/saltext/vmware/modules/esxi.py b/src/saltext/vmware/modules/esxi.py index aa518b4a..4fbeb013 100644 --- a/src/saltext/vmware/modules/esxi.py +++ b/src/saltext/vmware/modules/esxi.py @@ -67,6 +67,146 @@ def get_lun_ids(service_instance=None, profile=None): return list(ids) +def list_scsi_luns( + datacenter_name=None, + cluster_name=None, + host_name=None, + lun_name=None, + state=None, + profile=None, + service_instance=None, +): + """ + List SCSI Luns on the ESXi datacenter/cluster/host. + + datacenter_name + Filter by this datacenter name (required when cluster is specified) + + cluster_name + Filter by this cluster name (optional) + + host_name + Filter by this ESXi hostname whose power state needs to be managed (optional). + + state + Disk state, [attached, detached] (optional) + + profile + Profile to use (optional) + + .. code-block:: bash + + salt '*' vmware_esxi.list_scsi_luns datacenter_name=dc1 cluster_name=cl1 host_name=host1 + """ + + state_map = { + "attached": "ok", + "detached": "off", + "ok": "attached", + "off": "detached" + } + + ret = {} + service_instance = service_instance or utils_connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + + for host in hosts: + try: + ret[host.name] = [] + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + for scsi_lun in storage_system.storageDeviceInfo.scsiLun: + if lun_name and lun_name in scsi_lun.devicePath: + ret[host.name].append({ + "state": state_map[scsi_lun.operationalState[0]], + "name": scsi_lun.displayName, + "path": scsi_lun.devicePath, + "device": scsi_lun.deviceName, + "uuid": scsi_lun.uuid, + "descriptor": [(d.id, d.quality) for d in scsi_lun.descriptor if d.quality == 'highQuality'], + "local": scsi_lun.localDisk, + "location": scsi_lun.physicalLocation, + }) + elif state and state_map[state] == scsi_lun.operationalState[0]: + ret[host.name].append({ + "state": state_map[scsi_lun.operationalState[0]], + "name": scsi_lun.displayName, + "path": scsi_lun.devicePath, + "device": scsi_lun.deviceName, + "uuid": scsi_lun.uuid, + "descriptor": [(d.id, d.quality) for d in scsi_lun.descriptor if d.quality == 'highQuality'], + "local": scsi_lun.localDisk, + "location": scsi_lun.physicalLocation, + }) + elif lun_name == None and state == None: + ret[host.name].append({ + "state": state_map[scsi_lun.operationalState[0]], + "name": scsi_lun.displayName, + "path": scsi_lun.devicePath, + "device": scsi_lun.deviceName, + "uuid": scsi_lun.uuid, + "descriptor": [(d.id, d.quality) for d in scsi_lun.descriptor if d.quality == 'highQuality'], + "local": scsi_lun.localDisk, + "location": scsi_lun.physicalLocation, + }) + + except Exception as err: + ret[host.name] = f"Error: {err}" + + return ret + + +def get_host_disks( + service_instance=None, + datacenter_name=None, + cluster_name=None, + host_name=None, + disk_name=None, + profile=None): + """ + Return a list of hosts and the LUNs that are assigned to them. + + service_instance + Use this vCenter service connection instance instead of creating a new one. (optional). + + profile + Profile to use (optional) + """ + + service_instance = service_instance or utils_connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + ids = {} + for host in hosts: + if host.name not in ids: + ids[host.name] = [] + for datastore in host.datastore: + for extent in datastore.info.vmfs.extent: + if (disk_name and disk_name == datastore.info.vmfs.name) or \ + not disk_name: + ids[host.name].append({ + "scsi_address": extent.diskName, + "name": datastore.info.vmfs.name, + "uuid": datastore.info.vmfs.uuid, + "local": datastore.info.vmfs.local + }) + return ids + + def _get_capability_attribs(host): ret = {} for attrib in dir(host.capability): @@ -169,6 +309,106 @@ def power_state( return ret +def rescan_storage( + datacenter_name=None, + cluster_name=None, + host_name=None, + service_instance=None, + profile=None, +): + log.debug("Running vmware_esxi.rescan_storage") + ret = {} + service_instance = service_instance or utils_connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + + for host in hosts: + try: + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + res = storage_system.RefreshStorageSystem() + if not res: + ret[host.name] = True + else: + ret[host.name] = res + except Exception as err: + ret[host.name] = f"Error: {err}" + + return ret + + +def mount_storage(lun_canonical_name, + datacenter_name=None, + cluster_name=None, + host_name=None, + service_instance=None, + profile=None, +): + log.debug("Running vmware_esxi.mount_storage") + ret = {} + service_instance = service_instance or utils_connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + + for host in hosts: + try: + log.debug(f"mounting {lun_canonical_name} on {host.name}") + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + storage_system.AttachScsiLun(lun_canonical_name) + ret[host.name] = True + #ret[host.name] = f"mounting {lun_canonical_name} on {host.name}" + except Exception as err: + ret[host.name] = f"Error: {err}" + + + return ret + + +def unmount_storage(lun_canonical_name, + datacenter_name=None, + cluster_name=None, + host_name=None, + service_instance=None, + profile=None, +): + log.debug("Running vmware_esxi.unmount_storage") + ret = {} + service_instance = service_instance or utils_connect.get_service_instance( + config=__opts__, profile=profile + ) + hosts = utils_esxi.get_hosts( + service_instance=service_instance, + host_names=[host_name] if host_name else None, + cluster_name=cluster_name, + datacenter_name=datacenter_name, + get_all_hosts=host_name is None, + ) + + for host in hosts: + try: + log.debug(f"unmounting {lun_canonical_name} on {host.name}") + storage_system = utils_common.get_storage_system(service_instance, host, host.name) + #storage_system.DetachScsiLun(lun_canonical_name) + ret[host.name] = str(dir(storage_system)) + except Exception as err: + ret[host.name] = f"Error: {err}" + + return ret + + def service_start( service_name, datacenter_name=None, diff --git a/src/saltext/vmware/modules/vm.py b/src/saltext/vmware/modules/vm.py index 6071a456..adaded77 100644 --- a/src/saltext/vmware/modules/vm.py +++ b/src/saltext/vmware/modules/vm.py @@ -6,8 +6,10 @@ import salt.utils.platform import saltext.vmware.utils.common as utils_common import saltext.vmware.utils.connect as connect +import saltext.vmware.utils.datacenter as utils_datacenter import saltext.vmware.utils.datastore as utils_datastore import saltext.vmware.utils.vm as utils_vm +import saltext.vmware.utils.vsphere as utils_vmware log = logging.getLogger(__name__) @@ -315,6 +317,11 @@ def info(vm_name=None, service_instance=None, profile=None): vms.append(i) for vm in vms: + if not vm: + info[vm_name] = f"{vm_name} not found" + info["success"] = False + continue + datacenter_ref = utils_common.get_parent_type(vm, vim.Datacenter) mac_address = utils_vm.get_mac_address(vm) network = utils_vm.get_network(vm) @@ -324,6 +331,7 @@ def info(vm_name=None, service_instance=None, profile=None): folder_path = utils_common.get_path(vm, service_instance) info[vm.summary.config.name] = { "guest_name": vm.summary.config.name, + "path": vm.summary.config.vmPathName, "guest_fullname": vm.summary.guest.guestFullName, "power_state": vm.summary.runtime.powerState, "ip_address": vm.summary.guest.ipAddress, @@ -335,7 +343,7 @@ def info(vm_name=None, service_instance=None, profile=None): "cluster": vm.summary.runtime.host.parent.name, "tags": tags, "folder": folder_path, - "moid": vm._moId, + "moid": vm._moId } return info @@ -377,6 +385,9 @@ def power_state(vm_name, state, datacenter_name=None, service_instance=None, pro ) else: vm_ref = utils_common.get_mor_by_property(service_instance, vim.VirtualMachine, vm_name) + + if vm_ref == None: + return (False, "vm doesn't exist or not found") if state == "powered-on" and vm_ref.summary.runtime.powerState == "poweredOn": result = { "comment": "Virtual machine is already powered on", @@ -398,7 +409,7 @@ def power_state(vm_name, state, datacenter_name=None, service_instance=None, pro result_ref_vm = utils_vm.power_cycle_vm(vm_ref, state) result = { "comment": f"Virtual machine {state} action succeeded", - "changes": {"state": result_ref_vm.summary.runtime.powerState}, + "changes": {"state": f"{vm_name} -> {result_ref_vm.summary.runtime.powerState}"}, } return result @@ -530,9 +541,9 @@ def create_snapshot( snapshot = utils_vm.create_snapshot(vm_ref, snapshot_name, description, include_memory, quiesce) if isinstance(snapshot, vim.vm.Snapshot): - return {"snapshot": "created"} + return {"snapshot": "created", "success": True} else: - return {"snapshot": "failed to create"} + return {"snapshot": "failed to create", "success": False} def destroy_snapshot( @@ -676,8 +687,8 @@ def relocate( vm_ref, resources["destination_host"], datastore_ref, resources["resource_pool"] ) if ret == "success": - return {"virtual_machine": "moved"} - return {"virtual_machine": "failed to move"} + return {"virtual_machine": "moved", "success": True} + return {"virtual_machine": "failed to move", "success": False} def get_mks_ticket(vm_name, ticket_type, service_instance=None, profile=None): @@ -714,3 +725,368 @@ def get_mks_ticket(vm_name, ticket_type, service_instance=None, profile=None): ticket = vm_ref.AcquireTicket(ticket_type) return json.loads(json.dumps(ticket, cls=VmomiSupport.VmomiJSONEncoder)) return {} + +def unregister(vm_name, shutdown=False, service_instance=None, profile=None): + """ + Unregisters a VM + + vm_name + The name of the virtual machine to unregister. + + service_instance + (optional) The Service Instance from which to obtain managed object references. + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_vm.unregister_vm vm_name=vm01 + """ + ret = "No changes made" + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + + log.debug("running vmware_vm.unregister") + vm = utils_common.get_mor_by_property( + service_instance, + vim.VirtualMachine, + vm_name, + ) + + if vm.summary.runtime.powerState == "poweredOn": + if shutdown: + try: + utils_vm.shutdown(vm) + except Exception as err: + return (False, f"error powering off vm before unregistration: {err}") + else: + return (False, "VM must be powered off") + + try: + log.debug(f"unregistering {vm.name}") + utils_vm.unregister_vm(vm) + ret = { + "success": True, + } + except Exception as err: + ret = { + "success": False, + "comment": "Unregsitering VM failed", + "error": f"error unregistering vm: {err}" + } + + return ret + +def register(datacenter_name, pool_name, vm_name, vmx_path, folder_name=None, service_instance=None, profile=None): + """ + registers a VM + + datacenter_name + The name of the datacenter to place the VM in + + pool_name + The name of the resource pool to place the VM in + + vm_name + The name of the virtual machine to register. + + vmx_path + The path to the vmx file containing the machine info + + folder_name + The name of the folder to place the VM in + + service_instance + (optional) The Service Instance from which to obtain managed object references. + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_vm.info vm_name=vm01 + """ + ret = "No changes made" + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + + log.debug("running vmware_vm.register") + datacenter = utils_datacenter.get_datacenter(service_instance, datacenter_name) + pool = utils_common.get_resource_pools(service_instance, [pool_name], datacenter_name) + + if len(pool) > 1: + return { + "success": False, + "comment": "register VM failed", + "error": "too many pools" + } + elif len(pool) == 0: + return { + "success": False, + "comment": "register VM failed", + "error": f"pool {pool_name} not found" + } + else: + pool = pool[0] + + try: + log.debug(f"registering {vm_name}") + ret = utils_vm.register_vm(datacenter, vm_name, vmx_path, pool, folder_name, service_instance) + if not isinstance(ret, dict): + ret = True + except Exception as err: + ret = { + "success": False, + "comment": "register VM failed", + "error": f"error registering vm: {err}" + } + + return ret + + +def set_ip_info(ip, subnet, gw, dns, domain, vm_name, os=None, service_instance=None, profile=None): + """ + sets IP info for a VM (VM must be powered off first) + + ip + The IP address to set + + subnet + The subnet mask to set + + gw + The gateway to set + + dns + (list) "[dns1, dns2]" + + domain + dns domain for the NIC + + vm_name + The name of the VM to modify + + os + The VM of the guest, if the guest is newly imported this field must be used (Optional) + + service_instance + (optional) The Service Instance from which to obtain managed object references. + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_vm.set_ip_info ip=192.168.2.2 subnet=255.255.255.0 gw=192.168.2.1 dns=(192.168.2.10,192.168.2.11) vm_name=vm01 + """ + + + ret = "No changes made" + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + + log.debug("running vmware_vm.set_ip_info") + + vm = utils_common.get_mor_by_property( + service_instance, + vim.VirtualMachine, + vm_name, + ) + if vm.summary.runtime.powerState == "poweredOn": + return { + "success": False, + "comment": "set_ip_info failed", + "error": "VM must be powered off" + } + + if vm.summary.guest.guestFullName: + vm_os = vm.summary.guest.guestFullName.lower() + elif os: + vm_os = os + else: + return { + "success": False, + "comment": "set_ip_info failed", + "error": "guestFullName is empty AND os was not passed" + } + + if 'linux' in vm_os: + identity = vim.vm.customization.LinuxPrep() + identity.hostName = vim.vm.customization.FixedName() + identity.hostName.name = vm_name + identity.domain = domain + elif 'windows' in vm_os: + identity = vim.vm.customization.Sysprep() + # there are likely some other settings needed here + else: + return { + "success": False, + "comment": "set_ip_info failed", + "error": "Unsupported OS for IP customization" + } + + adapter_map = {} + adapter_count = 0 + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + adapter_count += 1 + adapter_map[device.deviceInfo.label] = device + + if adapter_count > 1: + return { + "success": False, + "comment": "set_ip_info failed", + "error": "Only a single NIC is supported for IP customization" + } + + ip_settings = vim.vm.customization.IPSettings() + ip_settings.ip = vim.vm.customization.FixedIp() + ip_settings.ip.ipAddress = ip + ip_settings.subnetMask = subnet + ip_settings.gateway = [gw] + ip_settings.dnsServerList = list(dns) + ip_settings.dnsDomain = domain + + globalip = vim.vm.customization.GlobalIPSettings() + globalip.dnsServerList = list(dns) + globalip.dnsSuffixList = [domain] + + adapter = vim.vm.customization.AdapterMapping() + adapter.adapter = ip_settings + + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + adapter_map[device.deviceInfo.label] = adapter + + spec = vim.vm.customization.Specification() + spec.identity = identity + spec.globalIPSettings = globalip + spec.nicSettingMap = list(adapter_map.values()) + + try: + utils_vm.customize_vm(vm, spec) + ret = True + except Exception as err: + ret = { + "success": False, + "comment": "set_ip_info failed", + "error": f"error updating ip: {err}" + } + + return ret + + +def set_dvport(vm_name, dvswitch_name, dport_group_name, service_instance=None, profile=None): + """ + Sets the Distributed Virtual Port Group of a VM (only single NIC supported) + + vm_name + The name of the VM to change + + dvswitch_name + The name of the Distributed Virtual Switch that contains the desired Port Group + + dport_group_name + The name of the Distributed Port Group for the VM + + service_instance + (optional) The Service Instance from which to obtain managed object references. + + profile + Profile to use (optional) + + CLI Example: + + .. code-block:: bash + + salt '*' vmware_vm.set_dvportgroup vm1 dvs2 dport1 profile=vcsa_config1 + """ + log.debug("Running vmware_vm.set_dvport") + service_instance = service_instance or connect.get_service_instance( + config=__opts__, profile=profile + ) + + dvs = utils_vmware._get_dvs(service_instance, dvswitch_name) + if not dvs: + return { + "success": False, + "comment": "setting distributed virtual port failed", + "error": "Specified Distributed Switch not found" + } + + port_group = utils_vmware._get_dvs_portgroup(dvs=dvs, portgroup_name=dport_group_name) + if not port_group: + return { + "success": False, + "comment": "setting distributed virtual port failed", + "error": "Specifed Distributed Port Group not found" + + } + + vm = utils_common.get_mor_by_property( + service_instance, + vim.VirtualMachine, + vm_name, + ) + if not vm: + return { + "success": False, + "comment": "setting distributed virtual port failed", + "error": "Specified VM not found" + } + if vm.summary.runtime.powerState == "poweredOn": + return { + "success": False, + "comment": "setting distributed virtual port failed", + "error": "VM must be powered off" + } + + vm_reconfig_spec = vim.vm.ConfigSpec() + nic_change_spec = vim.vm.device.VirtualDeviceSpec() + nic_change_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit + nic = None + + nic_count = 0 + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + nic_count += 1 + if nic_count > 1: + return { + "success": False, + "comment": "setting distributed virtual port failed", + "error": "Only VMs with a single NIC supported" + } + if isinstance(device, vim.vm.device.VirtualVmxnet3): + nic = device + + port_config_spec = vim.dvs.PortConnection() + port_config_spec.portgroupKey = port_group.key + port_config_spec.switchUuid = port_group.config.distributedVirtualSwitch.uuid + nic.backing = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() + nic.backing.port = port_config_spec + + nic_change_spec.device = nic + vm_reconfig_spec.deviceChange.append(nic_change_spec) + + try: + utils_vm.update_vm(vm, vm_reconfig_spec) + ret = True + except Exception as err: + ret = { + "success": False, + "comment": "setting distributed virtual port failed", + "error": f"error updating Distributed Virtual Port Group: {err}" + } + + return ret + diff --git a/src/saltext/vmware/modules/vsphere.py b/src/saltext/vmware/modules/vsphere.py index 736e76e1..b10a6bc3 100644 --- a/src/saltext/vmware/modules/vsphere.py +++ b/src/saltext/vmware/modules/vsphere.py @@ -98,7 +98,8 @@ def list_resourcepools( service_instance = service_instance or utils_connect.get_service_instance( config=__opts__, profile=profile ) - return utils_vsphere.list_resourcepools(service_instance) + pools = utils_vsphere.list_resourcepools(service_instance) + return [(p['owner'].name,p['summary'].config.entity._moId) for p in pools] def list_networks( diff --git a/src/saltext/vmware/utils/common.py b/src/saltext/vmware/utils/common.py index 5828000f..e92733da 100644 --- a/src/saltext/vmware/utils/common.py +++ b/src/saltext/vmware/utils/common.py @@ -331,6 +331,31 @@ def list_objects(service_instance, vim_object, properties=None): return items +def list_objects2(service_instance, vim_object, properties=None): + """ + Returns a simple list of objects from a given service instance. + + service_instance + The Service Instance for which to obtain a list of objects. + + object_type + The type of content for which to obtain information. + + properties + An optional list of object properties used to return reference results. + If not provided, defaults to ``name``. + """ + if properties is None: + properties = ["name"] + + items = [] + item_list = get_mors_with_properties(service_instance, vim_object, properties) + for item in item_list: + items.append(item) + return items + + + def get_service_instance_from_managed_object(mo_ref, name=""): """ Retrieves the service instance from a managed object. @@ -405,6 +430,49 @@ def get_managed_object_name(mo_ref): return props.get("name") +def get_folder(name, folder_type, service_instance=None, datacenter_name=None): + folder = None + for f in get_all_folders(service_instance, datacenter_name, folder_key=folder_type): + if f.name == name: + folder = f + + return folder + + +def get_all_folders(service_instance=None, datacenter_name=None, datacenter=None, folder_key="vmFolder"): + dcenter = None + if datacenter_name: + try: + import saltext.vmware.utils.datacenter as utils_datacenter + dc_ref = utils_datacenter.get_datacenter(service_instance, name) + dc = get_mors_with_properties( + service_instance, vim.Datacenter, container_ref=dc_ref, local_properties=True + ) + except (salt.exceptions.VMwareApiError, salt.exceptions.VMwareObjectRetrievalError) as exc: + return {name: False, "reason": str(exc), "success": False} + elif datacenter: + try: + dc = get_mors_with_properties( + service_instance, vim.Datacenter, container_ref=datacenter, local_properties=True + ) + except (salt.exceptions.VMwareApiError, salt.exceptions.VMwareObjectRetrievalError) as exc: + return {name: False, "reason": str(exc), "success": False} + + if dc: + dcenter = dc[0] + + folders = _get_folders(dcenter[folder_key]) + + return folders + +def _get_folders(dcenter, folders=[]): + for child in dcenter.childEntity: + if hasattr(child,"childEntity"): + folders.append(child) + _get_folders(child, folders) + return folders + + def get_resource_pools( service_instance, resource_pool_names, @@ -430,7 +498,7 @@ def get_resource_pools( Resourcepool managed object reference """ - properties = ["name"] + properties = ["name", "summary", "owner", "parent"] if not resource_pool_names: resource_pool_names = [] if datacenter_name: @@ -449,12 +517,15 @@ def get_resource_pools( selected_pools = [] for pool in resource_pools: - if get_all_resource_pools or (pool["name"] in resource_pool_names): + if get_all_resource_pools or \ + pool["name"] in resource_pool_names or \ + pool["owner"].name in resource_pool_names or \ + pool["summary"].config.entity._moId in resource_pool_names: selected_pools.append(pool["object"]) if not selected_pools: raise salt.exceptions.VMwareObjectRetrievalError( "The resource pools with properties " - "names={} get_all={} could not be found".format(selected_pools, get_all_resource_pools) + "names={} get_all={} could not be found".format(resource_pool_names, get_all_resource_pools) ) return selected_pools diff --git a/src/saltext/vmware/utils/vm.py b/src/saltext/vmware/utils/vm.py index 6da7da28..d7935228 100644 --- a/src/saltext/vmware/utils/vm.py +++ b/src/saltext/vmware/utils/vm.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 import logging import tarfile +import time import salt.exceptions import saltext.vmware.utils.cluster as utils_cluster @@ -19,6 +20,33 @@ log = logging.getLogger(__name__) +def shutdown(virtual_machine): + """ + Gracefully shuts down a VM + + virtual_machine + vim.VirtualMachine object to power on/off virtual machine + + """ + + if virtual_machine.summary.runtime.powerState == "poweredOn": + try: + virtual_machine.ShutdownGuest() + except vim.fault.ToolsUnavailable: + raise salt.exceptions.VMwareApiError( + "VMware tools not running" + ) + timeout_counter = 20 + while virtual_machine.summary.runtime.powerState != "poweredOff": + time.sleep(5) + timeout_counter -= 1 + if timeout_counter == 0: + return False + + return True + + return "VM already powered off" + def power_cycle_vm(virtual_machine, action="on"): """ Powers on/off a virtual machine specified by its name. @@ -186,7 +214,7 @@ def clone_vm(vm_name, folder_object, template, clone_config_spec): return vm_object -def register_vm(datacenter, name, vmx_path, resourcepool_object, host_object=None): +def register_vm(datacenter, name, vmx_path, resourcepool_object, folder_name=None, service_instance=None, host_object=None): """ Registers a virtual machine to the inventory with the given vmx file, on success it returns the vim.VirtualMachine managed object reference @@ -203,11 +231,47 @@ def register_vm(datacenter, name, vmx_path, resourcepool_object, host_object=Non resourcepool Placement resource pool of the virtual machine, vim.ResourcePool object - host + folder_name + Folder to register the VM in + + service_instance + The Service Instance Object from which to obtain Folders (required with folder_name). + + host_object Placement host of the virtual machine, vim.HostSystem object """ try: - if host_object: + if folder_name: + folder = None + folders = [f for f in utils_common.get_all_folders(service_instance=service_instance, datacenter=datacenter) if f.name == folder_name] + if not folders: + return { + "success": False, + "comment": "register_vm failed", + "error": "failed to find folder" + } + elif len(folders) > 1: + return { + "success": False, + "comment": "register_vm failed", + "error": "More than one matching folder" + } + else: + folder = folders[0] + + if host_object and folder_name: + task = folder.RegisterVM_Task( + path=vmx_path, + name=name, + asTemplate=False, + host=host_object, + pool=resourcepool_object, + ) + elif folder_name: + task = folder.RegisterVM_Task( + path=vmx_path, name=name, asTemplate=False, pool=resourcepool_object + ) + elif host_object and not folder_name: task = datacenter.vmFolder.RegisterVM_Task( path=vmx_path, name=name, @@ -215,10 +279,16 @@ def register_vm(datacenter, name, vmx_path, resourcepool_object, host_object=Non host=host_object, pool=resourcepool_object, ) - else: + elif not host_object and not folder_name: task = datacenter.vmFolder.RegisterVM_Task( path=vmx_path, name=name, asTemplate=False, pool=resourcepool_object ) + else: + return { + "success": False, + "comment": "register_vm failed", + "error": "Should not be here" + } except vim.fault.NoPermission as exc: log.exception(exc) raise salt.exceptions.VMwareApiError( @@ -242,7 +312,7 @@ def register_vm(datacenter, name, vmx_path, resourcepool_object, host_object=Non def update_vm(vm_ref, vm_config_spec): """ - Updates the virtual machine configuration with the given object + Updates the virtual machine hardware configuration with the given object vm_ref Virtual machine managed object reference @@ -269,6 +339,36 @@ def update_vm(vm_ref, vm_config_spec): return vm_ref +def customize_vm(vm_ref, vm_config_spec): + """ + Customizes the virtual machine OS configuration with the given object + + vm_ref + Virtual machine managed object reference + + vm_config_spec + Virtual machine config spec object to update + """ + vm_name = utils_common.get_managed_object_name(vm_ref) + log.trace("Customizing vm '%s'", vm_name) + try: + task = vm_ref.CustomizeVM_Task(spec=vm_config_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError( + "Not enough permissions. Required privilege: " "{}".format(exc.privilegeId) + ) + except vim.fault.VimFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise salt.exceptions.VMwareRuntimeError(exc.msg) + vm_ref = utils_common.wait_for_task(task, vm_name, "CustomizeVM Task") + return vm_ref + + + def delete_vm(vm_ref): """ Destroys the virtual machine diff --git a/src/saltext/vmware/utils/vsphere.py b/src/saltext/vmware/utils/vsphere.py index bd5e020c..ac2dd1f5 100644 --- a/src/saltext/vmware/utils/vsphere.py +++ b/src/saltext/vmware/utils/vsphere.py @@ -2325,7 +2325,7 @@ def list_resourcepools(service_instance): service_instance The Service Instance Object from which to obtain resource pools. """ - return utils_common.list_objects(service_instance, vim.ResourcePool) + return utils_common.list_objects2(service_instance, vim.ResourcePool, ['summary', 'owner', 'parent']) def list_networks(service_instance):