#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2018, Ansible Project # Copyright: (c) 2018, Abhijeet Kasurde # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = ''' --- module: vmware_guest_disk short_description: Manage disks related to virtual machine in given vCenter infrastructure description: - This module can be used to add, remove and update disks belonging to given virtual machine. - All parameters and VMware object names are case sensitive. - This module is destructive in nature, please read documentation carefully before proceeding. - Be careful while removing disk specified as this may lead to data loss. version_added: 2.8 author: - Abhijeet Kasurde (@akasurde) notes: - Tested on vSphere 6.0 and 6.5 requirements: - "python >= 2.6" - PyVmomi options: name: description: - Name of the virtual machine. - This is a required parameter, if parameter C(uuid) is not supplied. uuid: description: - UUID of the instance to gather facts if known, this is VMware's unique identifier. - This is a required parameter, if parameter C(name) is not supplied. folder: description: - Destination folder, absolute or relative path to find an existing guest. - This is a required parameter, only if multiple VMs are found with same name. - The folder should include the datacenter. ESX's datacenter is ha-datacenter - 'Examples:' - ' folder: /ha-datacenter/vm' - ' folder: ha-datacenter/vm' - ' folder: /datacenter1/vm' - ' folder: datacenter1/vm' - ' folder: /datacenter1/vm/folder1' - ' folder: datacenter1/vm/folder1' - ' folder: /folder1/datacenter1/vm' - ' folder: folder1/datacenter1/vm' - ' folder: /folder1/datacenter1/vm/folder2' datacenter: description: - The datacenter name to which virtual machine belongs to. required: True disk: description: - A list of disks to add. - The virtual disk related information is provided using this list. - All values and parameters are case sensitive. - 'Valid attributes are:' - ' - C(size[_tb,_gb,_mb,_kb]) (integer): Disk storage size in specified unit.' - ' If C(size) specified then unit must be specified. There is no space allowed in between size number and unit.' - ' Only first occurance in disk element will be considered, even if there are multiple size* parameters available.' - ' - C(type) (string): Valid values are:' - ' - C(thin) thin disk' - ' - C(eagerzeroedthick) eagerzeroedthick disk' - ' - C(thick) thick disk' - ' Default: C(thick) thick disk, no eagerzero.' - ' - C(datastore) (string): Name of datastore or datastore cluster to be used for the disk.' - ' - C(autoselect_datastore) (bool): Select the less used datastore. Specify only if C(datastore) is not specified.' - ' - C(scsi_controller) (integer): SCSI controller number. Valid value range from 0 to 3.' - ' Only 4 SCSI controllers are allowed per VM.' - ' Care should be taken while specifying C(scsi_controller) is 0 and C(unit_number) as 0 as this disk may contain OS.' - ' - C(unit_number) (integer): Disk Unit Number. Valid value range from 0 to 15. Only 15 disks are allowed per SCSI Controller.' - ' - C(scsi_type) (string): Type of SCSI controller. This value is required only for the first occurance of SCSI Controller.' - ' This value is ignored, if SCSI Controller is already present or C(state) is C(absent).' - ' Valid values are C(buslogic), C(lsilogic), C(lsilogicsas) and C(paravirtual).' - ' C(paravirtual) is default value for this parameter.' - ' - C(state) (string): State of disk. This is either "absent" or "present".' - ' If C(state) is set to C(absent), disk will be removed permanently from virtual machine configuration and from VMware storage.' - ' If C(state) is set to C(present), disk will be added if not present at given SCSI Controller and Unit Number.' - ' If C(state) is set to C(present) and disk exists with different size, disk size is increased.' - ' Reducing disk size is not allowed.' default: [] extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' - name: Add disks to virtual machine using UUID vmware_guest_disk: hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_username }}" password: "{{ vcenter_password }}" datacenter: "{{ datacenter_name }}" validate_certs: no uuid: 421e4592-c069-924d-ce20-7e7533fab926 disk: - size_mb: 10 type: thin datastore: datacluster0 state: present scsi_controller: 1 unit_number: 1 scsi_type: 'paravirtual' - size_gb: 10 type: eagerzeroedthick state: present autoselect_datastore: True scsi_controller: 2 scsi_type: 'buslogic' unit_number: 12 - size: 10Gb type: eagerzeroedthick state: present autoselect_datastore: True scsi_controller: 2 scsi_type: 'buslogic' unit_number: 1 delegate_to: localhost register: disk_facts - name: Remove disks from virtual machine using name vmware_guest_disk: hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_username }}" password: "{{ vcenter_password }}" datacenter: "{{ datacenter_name }}" validate_certs: no name: VM_225 disk: - state: absent scsi_controller: 1 unit_number: 1 delegate_to: localhost register: disk_facts ''' RETURN = """ disk_status: description: metadata about the virtual machine's disks after managing them returned: always type: dict sample: { "0": { "backing_datastore": "datastore2", "backing_disk_mode": "persistent", "backing_eagerlyscrub": false, "backing_filename": "[datastore2] VM_225/VM_225.vmdk", "backing_thinprovisioned": false, "backing_writethrough": false, "capacity_in_bytes": 10485760, "capacity_in_kb": 10240, "controller_key": 1000, "key": 2000, "label": "Hard disk 1", "summary": "10,240 KB", "unit_number": 0 }, } """ import re try: from pyVmomi import vim except ImportError: pass from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native from ansible.module_utils.vmware import PyVmomi, vmware_argument_spec, wait_for_task, find_obj, get_all_objs class PyVmomiHelper(PyVmomi): def __init__(self, module): super(PyVmomiHelper, self).__init__(module) self.desired_disks = self.params['disk'] # Match with vmware_guest parameter self.vm = None self.scsi_device_type = dict(lsilogic=vim.vm.device.VirtualLsiLogicController, paravirtual=vim.vm.device.ParaVirtualSCSIController, buslogic=vim.vm.device.VirtualBusLogicController, lsilogicsas=vim.vm.device.VirtualLsiLogicSASController) self.config_spec = vim.vm.ConfigSpec() self.config_spec.deviceChange = [] def create_scsi_controller(self, scsi_type, scsi_bus_number): """ Create SCSI Controller with given SCSI Type and SCSI Bus Number Args: scsi_type: Type of SCSI scsi_bus_number: SCSI Bus number to be assigned Returns: Virtual device spec for SCSI Controller """ scsi_ctl = vim.vm.device.VirtualDeviceSpec() scsi_ctl.operation = vim.vm.device.VirtualDeviceSpec.Operation.add scsi_ctl.device = self.scsi_device_type[scsi_type]() scsi_ctl.device.unitNumber = 3 scsi_ctl.device.busNumber = scsi_bus_number scsi_ctl.device.hotAddRemove = True scsi_ctl.device.sharedBus = 'noSharing' scsi_ctl.device.scsiCtlrUnitNumber = 7 return scsi_ctl @staticmethod def create_scsi_disk(scsi_ctl_key, disk_index): """ Create Virtual Device Spec for virtual disk Args: scsi_ctl_key: Unique SCSI Controller Key disk_index: Disk unit number at which disk needs to be attached Returns: Virtual Device Spec for virtual disk """ disk_spec = vim.vm.device.VirtualDeviceSpec() disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add disk_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create disk_spec.device = vim.vm.device.VirtualDisk() disk_spec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo() disk_spec.device.backing.diskMode = 'persistent' disk_spec.device.controllerKey = scsi_ctl_key disk_spec.device.unitNumber = disk_index return disk_spec def reconfigure_vm(self, config_spec, device_type): """ Reconfigure virtual machine after modifying device spec Args: config_spec: Config Spec device_type: Type of device being modified Returns: Boolean status 'changed' and actual task result """ changed, results = (False, '') try: # Perform actual VM reconfiguration task = self.vm.ReconfigVM_Task(spec=config_spec) changed, results = wait_for_task(task) except vim.fault.InvalidDeviceSpec as invalid_device_spec: self.module.fail_json(msg="Failed to manage %s on given virtual machine due to invalid" " device spec : %s" % (device_type, to_native(invalid_device_spec.msg)), details="Please check ESXi server logs for more details.") except vim.fault.RestrictedVersion as e: self.module.fail_json(msg="Failed to reconfigure virtual machine due to" " product versioning restrictions: %s" % to_native(e.msg)) return changed, results def ensure_disks(self, vm_obj=None): """ Manage internal state of virtual machine disks Args: vm_obj: Managed object of virtual machine """ # Set vm object self.vm = vm_obj # Sanitize user input disk_data = self.sanitize_disk_inputs() # Create stateful information about SCSI devices current_scsi_info = dict() results = dict(changed=False, disk_data=None, disk_changes=dict()) # Deal with SCSI Controller for device in vm_obj.config.hardware.device: if isinstance(device, tuple(self.scsi_device_type.values())): # Found SCSI device if device.busNumber not in current_scsi_info: device_bus_number = 1000 + device.busNumber current_scsi_info[device_bus_number] = dict(disks=dict()) scsi_changed = False for disk in disk_data: scsi_controller = disk['scsi_controller'] + 1000 if scsi_controller not in current_scsi_info and disk['state'] == 'present': scsi_ctl = self.create_scsi_controller(disk['scsi_type'], disk['scsi_controller']) current_scsi_info[scsi_controller] = dict(disks=dict()) self.config_spec.deviceChange.append(scsi_ctl) scsi_changed = True if scsi_changed: self.reconfigure_vm(self.config_spec, 'SCSI Controller') self.config_spec = vim.vm.ConfigSpec() self.config_spec.deviceChange = [] # Deal with Disks for device in vm_obj.config.hardware.device: if isinstance(device, vim.vm.device.VirtualDisk): # Found Virtual Disk device if device.controllerKey not in current_scsi_info: current_scsi_info[device.controllerKey] = dict(disks=dict()) current_scsi_info[device.controllerKey]['disks'][device.unitNumber] = device vm_name = self.vm.name disk_change_list = [] for disk in disk_data: disk_change = False scsi_controller = disk['scsi_controller'] + 1000 # VMware auto assign 1000 + SCSI Controller if disk['disk_unit_number'] not in current_scsi_info[scsi_controller]['disks'] and disk['state'] == 'present': # Add new disk disk_spec = self.create_scsi_disk(scsi_controller, disk['disk_unit_number']) disk_spec.device.capacityInKB = disk['size'] if disk['disk_type'] == 'thin': disk_spec.device.backing.thinProvisioned = True elif disk['disk_type'] == 'eagerzeroedthick': disk_spec.device.backing.eagerlyScrub = True disk_spec.device.backing.fileName = "[%s] %s/%s_%s_%s.vmdk" % (disk['datastore'].name, vm_name, vm_name, str(scsi_controller), str(disk['disk_unit_number'])) disk_spec.device.backing.datastore = disk['datastore'] self.config_spec.deviceChange.append(disk_spec) disk_change = True current_scsi_info[scsi_controller]['disks'][disk['disk_unit_number']] = disk_spec.device results['disk_changes'][disk['disk_index']] = "Disk created." elif disk['disk_unit_number'] in current_scsi_info[scsi_controller]['disks']: if disk['state'] == 'present': disk_spec = vim.vm.device.VirtualDeviceSpec() # set the operation to edit so that it knows to keep other settings disk_spec.device = current_scsi_info[scsi_controller]['disks'][disk['disk_unit_number']] # Edit and no resizing allowed if disk['size'] < disk_spec.device.capacityInKB: self.module.fail_json(msg="Given disk size at disk index [%s] is smaller than found (%d < %d)." " Reducing disks is not allowed." % (disk['disk_index'], disk['size'], disk_spec.device.capacityInKB)) if disk['size'] != disk_spec.device.capacityInKB: disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit disk_spec.device.capacityInKB = disk['size'] self.config_spec.deviceChange.append(disk_spec) disk_change = True results['disk_changes'][disk['disk_index']] = "Disk size increased." else: results['disk_changes'][disk['disk_index']] = "Disk already exists." elif disk['state'] == 'absent': # Disk already exists, deleting disk_spec = vim.vm.device.VirtualDeviceSpec() disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove disk_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.destroy disk_spec.device = current_scsi_info[scsi_controller]['disks'][disk['disk_unit_number']] self.config_spec.deviceChange.append(disk_spec) disk_change = True results['disk_changes'][disk['disk_index']] = "Disk deleted." if disk_change: # Adding multiple disks in a single attempt raises weird errors # So adding single disk at a time. self.reconfigure_vm(self.config_spec, 'disks') self.config_spec = vim.vm.ConfigSpec() self.config_spec.deviceChange = [] disk_change_list.append(disk_change) if any(disk_change_list): results['changed'] = True results['disk_data'] = self.gather_disk_facts(vm_obj=self.vm) self.module.exit_json(**results) def sanitize_disk_inputs(self): """ Check correctness of disk input provided by user Returns: A list of dictionary containing disk information """ disks_data = list() if not self.desired_disks: self.module.exit_json(changed=False, msg="No disks provided for virtual" " machine '%s' for management." % self.vm.name) for disk_index, disk in enumerate(self.desired_disks): # Initialize default value for disk current_disk = dict(disk_index=disk_index, state='present', datastore=None, autoselect_datastore=True, disk_unit_number=0, scsi_controller=0) # Check state if 'state' in disk: if disk['state'] not in ['absent', 'present']: self.module.fail_json(msg="Invalid state provided '%s' for disk index [%s]." " State can be either - 'absent', 'present'" % (disk['state'], disk_index)) else: current_disk['state'] = disk['state'] if current_disk['state'] == 'present': # Select datastore or datastore cluster if 'datastore' in disk: if 'autoselect_datastore' in disk: self.module.fail_json(msg="Please specify either 'datastore' " "or 'autoselect_datastore' for disk index [%s]" % disk_index) # Check if given value is datastore or datastore cluster datastore_name = disk['datastore'] datastore_cluster = find_obj(self.content, [vim.StoragePod], datastore_name) if datastore_cluster: # If user specified datastore cluster so get recommended datastore datastore_name = self.get_recommended_datastore(datastore_cluster_obj=datastore_cluster) # Check if get_recommended_datastore or user specified datastore exists or not datastore = find_obj(self.content, [vim.Datastore], datastore_name) if datastore is None: self.module.fail_json(msg="Failed to find datastore named '%s' " "in given configuration." % disk['datastore']) current_disk['datastore'] = datastore current_disk['autoselect_datastore'] = False elif 'autoselect_datastore' in disk: # Find datastore which fits requirement datastores = get_all_objs(self.content, [vim.Datastore]) if not datastores: self.module.fail_json(msg="Failed to gather information about" " available datastores in given datacenter.") datastore = None datastore_freespace = 0 for ds in datastores: if ds.summary.freeSpace > datastore_freespace: # If datastore field is provided, filter destination datastores datastore = ds datastore_freespace = ds.summary.freeSpace current_disk['datastore'] = datastore if 'datastore' not in disk and 'autoselect_datastore' not in disk: self.module.fail_json(msg="Either 'datastore' or 'autoselect_datastore' is" " required parameter while creating disk for " "disk index [%s]." % disk_index) if [x for x in disk.keys() if x.startswith('size_') or x == 'size']: # size, size_tb, size_gb, size_mb, size_kb disk_size_parse_failed = False if 'size' in disk: size_regex = re.compile(r'(\d+(?:\.\d+)?)([tgmkTGMK][bB])') disk_size_m = size_regex.match(disk['size']) if disk_size_m: expected = disk_size_m.group(1) unit = disk_size_m.group(2) else: disk_size_parse_failed = True try: if re.match(r'\d+\.\d+', expected): # We found float value in string, let's typecast it expected = float(expected) else: # We found int value in string, let's typecast it expected = int(expected) except (TypeError, ValueError, NameError): disk_size_parse_failed = True else: # Even multiple size_ parameter provided by user, # consider first value only param = [x for x in disk.keys() if x.startswith('size_')][0] unit = param.split('_')[-1] disk_size = disk[param] if isinstance(disk_size, (float, int)): disk_size = str(disk_size) try: if re.match(r'\d+\.\d+', disk_size): # We found float value in string, let's typecast it expected = float(disk_size) else: # We found int value in string, let's typecast it expected = int(disk_size) except (TypeError, ValueError, NameError): disk_size_parse_failed = True if disk_size_parse_failed: # Common failure self.module.fail_json(msg="Failed to parse disk size for disk index [%s]," " please review value provided" " using documentation." % disk_index) disk_units = dict(tb=3, gb=2, mb=1, kb=0) unit = unit.lower() if unit in disk_units: current_disk['size'] = expected * (1024 ** disk_units[unit]) else: self.module.fail_json(msg="%s is not a supported unit for disk size for disk index [%s]." " Supported units are ['%s']." % (unit, disk_index, "', '".join(disk_units.keys()))) else: # No size found but disk, fail self.module.fail_json(msg="No size, size_kb, size_mb, size_gb or size_tb" " attribute found into disk index [%s] configuration." % disk_index) # Check SCSI controller key if 'scsi_controller' in disk: try: temp_disk_controller = int(disk['scsi_controller']) except ValueError: self.module.fail_json(msg="Invalid SCSI controller ID '%s' specified" " at index [%s]" % (disk['scsi_controller'], disk_index)) if temp_disk_controller not in range(0, 4): # Only 4 SCSI controllers are allowed per VM self.module.fail_json(msg="Invalid SCSI controller ID specified [%s]," " please specify value between 0 to 3 only." % temp_disk_controller) current_disk['scsi_controller'] = temp_disk_controller else: self.module.fail_json(msg="Please specify 'scsi_controller' under disk parameter" " at index [%s], which is required while creating disk." % disk_index) # Check for disk unit number if 'unit_number' in disk: try: temp_disk_unit_number = int(disk['unit_number']) except ValueError: self.module.fail_json(msg="Invalid Disk unit number ID '%s'" " specified at index [%s]" % (disk['unit_number'], disk_index)) if temp_disk_unit_number not in range(0, 16): self.module.fail_json(msg="Invalid Disk unit number ID specified for disk [%s] at index [%s]," " please specify value between 0 to 15" " only (excluding 7)." % (temp_disk_unit_number, disk_index)) if temp_disk_unit_number == 7: self.module.fail_json(msg="Invalid Disk unit number ID specified for disk at index [%s]," " please specify value other than 7 as it is reserved" "for SCSI Controller" % disk_index) current_disk['disk_unit_number'] = temp_disk_unit_number else: self.module.fail_json(msg="Please specify 'unit_number' under disk parameter" " at index [%s], which is required while creating disk." % disk_index) # Type of Disk disk_type = disk.get('type', 'thick').lower() if disk_type not in ['thin', 'thick', 'eagerzeroedthick']: self.module.fail_json(msg="Invalid 'disk_type' specified for disk index [%s]. Please specify" " 'disk_type' value from ['thin', 'thick', 'eagerzeroedthick']." % disk_index) current_disk['disk_type'] = disk_type # SCSI Controller Type scsi_contrl_type = disk.get('scsi_type', 'paravirtual').lower() if scsi_contrl_type not in self.scsi_device_type.keys(): self.module.fail_json(msg="Invalid 'scsi_type' specified for disk index [%s]. Please specify" " 'scsi_type' value from ['%s']" % (disk_index, "', '".join(self.scsi_device_type.keys()))) current_disk['scsi_type'] = scsi_contrl_type disks_data.append(current_disk) return disks_data def get_recommended_datastore(self, datastore_cluster_obj): """ Return Storage DRS recommended datastore from datastore cluster Args: datastore_cluster_obj: datastore cluster managed object Returns: Name of recommended datastore from the given datastore cluster, Returns None if no datastore recommendation found. """ # Check if Datastore Cluster provided by user is SDRS ready sdrs_status = datastore_cluster_obj.podStorageDrsEntry.storageDrsConfig.podConfig.enabled if sdrs_status: # We can get storage recommendation only if SDRS is enabled on given datastorage cluster pod_sel_spec = vim.storageDrs.PodSelectionSpec() pod_sel_spec.storagePod = datastore_cluster_obj storage_spec = vim.storageDrs.StoragePlacementSpec() storage_spec.podSelectionSpec = pod_sel_spec storage_spec.type = 'create' try: rec = self.content.storageResourceManager.RecommendDatastores(storageSpec=storage_spec) rec_action = rec.recommendations[0].action[0] return rec_action.destination.name except Exception as e: # There is some error so we fall back to general workflow pass datastore = None datastore_freespace = 0 for ds in datastore_cluster_obj.childEntity: if ds.summary.freeSpace > datastore_freespace: # If datastore field is provided, filter destination datastores datastore = ds datastore_freespace = ds.summary.freeSpace if datastore: return datastore.name return None @staticmethod def gather_disk_facts(vm_obj): """ Gather facts about VM's disks Args: vm_obj: Managed object of virtual machine Returns: A list of dict containing disks information """ disks_facts = dict() if vm_obj is None: return disks_facts disk_index = 0 for disk in vm_obj.config.hardware.device: if isinstance(disk, vim.vm.device.VirtualDisk): disks_facts[disk_index] = dict( key=disk.key, label=disk.deviceInfo.label, summary=disk.deviceInfo.summary, backing_filename=disk.backing.fileName, backing_datastore=disk.backing.datastore.name, backing_disk_mode=disk.backing.diskMode, backing_writethrough=disk.backing.writeThrough, backing_thinprovisioned=disk.backing.thinProvisioned, backing_eagerlyscrub=bool(disk.backing.eagerlyScrub), controller_key=disk.controllerKey, unit_number=disk.unitNumber, capacity_in_kb=disk.capacityInKB, capacity_in_bytes=disk.capacityInBytes, ) disk_index += 1 return disks_facts def main(): argument_spec = vmware_argument_spec() argument_spec.update( name=dict(type='str'), uuid=dict(type='str'), folder=dict(type='str'), datacenter=dict(type='str', required=True), disk=dict(type=list, default=[]), ) module = AnsibleModule(argument_spec=argument_spec, required_one_of=[['name', 'uuid']]) if module.params['folder']: # FindByInventoryPath() does not require an absolute path # so we should leave the input folder path unmodified module.params['folder'] = module.params['folder'].rstrip('/') pyv = PyVmomiHelper(module) # Check if the VM exists before continuing vm = pyv.get_vm() if not vm: # We unable to find the virtual machine user specified # Bail out module.fail_json(msg="Unable to manage disks for non-existing" " virtual machine '%s'." % (module.params.get('uuid') or module.params.get('name'))) # VM exists try: pyv.ensure_disks(vm_obj=vm) except Exception as exc: module.fail_json(msg="Failed to manage disks for virtual machine" " '%s' with exception : %s" % (vm.name, to_native(exc))) if __name__ == '__main__': main()