CIVMDisks.psm1
# CIVMDisks.psm1 # PowerShell Module to aid VM internal disk manipulation in VMware # Cloud Director (VCD) # Author: Jon Waite, (c)2020-2023 All Rights Reserved # License: MIT - See LICENSE file # Version: 1.0 (initial release) # Date: February 2nd 2023 # Homepage: https://github.com/jondwaite/CIVMDisks # Controller types available in VCD enum BusTypes { ide = 1 parallel = 3 sas = 4 paravirtual = 5 sata = 6 } # Internal function to get the highest supported/non-deprecated API version from a Uri: Function Get-APIVersion { Param( [Parameter(Mandatory=$true)][Uri]$Uri, [Switch]$SkipCertificateCheck ) $APICheckParams = @{ Uri = "https://$($Uri.Host)/api/versions" Headers = @{'Accept'='application/*+xml'} Method = 'Get' } If ($SkipCertificateCheck) {$APICheckParams += @{'SkipCertificateCheck'=$true}} Try { [xml]$Versions = Invoke-RestMethod @APICheckParams $APIVersion = (($Versions.SupportedVersions.VersionInfo | ` Where-Object { $_.deprecated -eq $false }) | ` Measure-Object -Property Version -Maximum).Maximum.ToString() # Detect and deal with the differing returns for integer API versions vs. versions ending in a decimal (e.g. 36 vs 36.1): If ($APIVersion.Substring($APIVersion.Length-2,1) -ne ".") { $APIVersion += ".0" } return $APIVersion } Catch { Write-Error ("Error occurred determining maximum supported API version.") return $_.Exception } } # Internal function to get the SessionId token for the current PowerCLI session: Function Get-SessionId { Param( [Parameter(Mandatory=$true)]$VM ) $SessionId = ($global:DefaultCIServers | Where-Object { $_.ServiceUri.Host -eq ([Uri]$VM.Href).Host }).SessionId If (!$SessionId) { Write-Error ("Could not match supplied VM to a connected VCD instance, exiting.");Break } return $SessionId } # Internal function to retrieve the XML representation of a VM and strip the non VmSpecSection nodes from it: Function Get-VMDiskXML { Param( [Parameter(Mandatory=$true)]$VM, [Switch]$SkipCertificateCheck ) $APIVersParams = @{'Uri'=$VM.Href} if ($SkipCertificateCheck) { $APIVersParams += @{'SkipCertificateCheck'=$true}} $APIVersion = Get-APIVersion @APIVersParams $SessionId = Get-SessionId -VM $VM $Headers = @{ 'Accept'="application/*+xml;version=$($APIVersion)" 'x-vcloud-authorization'=$SessionId } # Get VM details from API $VMDetailsParams = @{'Method'='Get';'Headers'=$Headers;'Uri'=$VM.Href} if ($SkipCertificateCheck) { $VMDetailsParams += @{'SkipCertificateCheck'=$true}} Try { [xml]$xmlvm = Invoke-RestMethod @VMDetailsParams } Catch { Write-Error "Error retrieving VM properties from VCD API, exiting.";Break } # Remove all Child nodes in XML EXCEPT VmSpecSection: $RemoveNodes = @() $xmlvm.Vm.ChildNodes | ForEach-Object { If ($_.Name -ne 'VmSpecSection') { $RemoveNodes += $_ } } $RemoveNodes | ForEach-Object { [void]$xmlvm.Vm.RemoveChild($_) } return $xmlvm } # Internal function to update the VM with modified XML: Function Set-VMDiskXML { Param( [Parameter(Mandatory=$true)]$VM, [Parameter(Mandatory=$true)]$BodyXML, [int]$TaskTimeout, [Switch]$SkipCertificateCheck ) $APIVersParams = @{'Uri'=$VM.Href} if ($SkipCertificateCheck) { $APIVersParams += @{'SkipCertificateCheck'=$true}} $APIVersion = Get-APIVersion @APIVersParams $SessionId = Get-SessionId -VM $VM $Headers = @{ 'Accept'="application/*+xml;version=$($APIVersion)" 'x-vcloud-authorization'=$SessionId 'Content-Type'='application/vnd.vmware.vcloud.vm+xml' } $UpdateVmParams = @{ Uri = "$($VM.Href)/action/reconfigureVm" Body = $BodyXML Method = 'Post' Headers = $Headers } if ($SkipCertificateCheck) { $UpdateVmParams += @{'SkipCertificateCheck'=$true}} try { $VCDTask = Invoke-RestMethod @UpdateVmParams } catch { Write-Error ("Exception occurred attempting to add disk to VM, exiting."); Break } if ($VCDtask.Task.href) { # Task submitted ok and we've asked to wait for it to complete $WaitParams = @{TaskHref=$VCDtask.Task.Href; APIVersion=$APIVersion; SessionId=$SessionId; TaskTimeout=$TaskTimeout} if ($SkipCertificateCheck) { $WaitParams += @{'SkipCertificateCheck'=$true}} $response = WaitForTask @WaitParams return $response } else { Write-Host -ForegroundColor Red ("Error, request was submitted but no VCD task was returned.") return $false } } # Internal function to wait for a VCD task to complete: Function WaitForTask { Param( [Parameter(Mandatory=$true)]$TaskHref, [Parameter(Mandatory=$true)]$APIVersion, [Parameter(Mandatory=$true)]$SessionId, [int]$TaskTimeout = 30, [Switch]$SkipCertificateCheck ) $TaskParams = @{ Uri = $TaskHref Method = 'Get' Headers = @{'Accept'="application/*+xml;version=$($APIVersion)";'x-vcloud-authorization'=$SessionId} } if ($SkipCertificateCheck) { $TaskParams += @{'SkipCertificateCheck'=$true}} Write-Host -ForegroundColor Cyan ("Task submitted successfully.") # Give the task a chance to initialize before checking status: Start-Sleep -Seconds 5 :taskcheck While ($TaskTimeout -gt 0) { # Check task status until timeout is exceeeded Try { $taskStatus = Invoke-RestMethod @TaskParams } Catch { Write-Error ("Error getting task status from VCD API: $($_.Exception.Message), exiting."); Break } switch ($taskStatus.Task.Status) { "success" { break taskcheck } "running" { Write-Host -ForegroundColor Green "Task Status: Running" } "error" { Write-Error ("Task ended with error, exiting."); Break } "canceled" { Write-Error ("Task was cancelled, exiting."); Break } "aborted" { Write-Error ("Task was aborted, exiting."); Break } "queued" { Write-Host -ForegroundColor Yellow "Task is queued." } "preRunning" { Write-Host -ForegroundColor Yellow "Task is pre-running." } } # Sleep for 3 seconds before checking status again: $TaskTimeout -= 3 Start-Sleep -Seconds 3 } # While TaskTimeout > 0 If ($taskStatus.Task.Status -eq "success") { Write-Host -ForegroundColor Green "Operation completed successfully." return $true } else { Write-Host -ForegroundColor Yellow "Task timeout reached (task may still be in progress)" return $false } } # Internal function to process storage size suffix and turn into a disk size in MB: Function GetDiskSizeMB { Param( [Parameter(Mandatory=$true)][string]$DiskSize ) [int64]$diskSizeMB = 0 $unit = $DiskSize.Substring($DiskSize.Length - 1,1) $size = $DiskSize.Substring(0,$DiskSize.Length - 1) switch ($unit) { "M" { $diskSizeMB = [int64]$size } "G" { $diskSizeMB = ([float]$size * 1024) } "T" { $diskSizeMB = ([float]$size * 1024 * 1024) } default { $diskSizeMB = [int64]$diskSize } } return $diskSizeMB } # Get details of the disks attached to a VM Function Get-CIVMDisk { Param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$VM, [switch]$SkipCertificateCheck, [int]$TaskTimeout = 30 ) # Get the VM XML representation from the VCD API: $VMDiskXMLParams = @{VM = $VM} if ($SkipCertificateCheck) { $VMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $xmlvm = Get-VMDiskXML @VMDiskXMLParams $DiskDetails = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings $DiskDetails | ForEach-Object { switch ($_.AdapterType) { "1" { $AdapterName = "ide"} "3" { $AdapterName = "parallel"} "4" { $AdapterName = "sas"} "5" { $AdapterName = "paravirtual"} "6" { $AdapterName = "sata"} default { $AdapterName = "unknown"} } $_.AdapterType = $AdapterName $_ | Add-Member -NotePropertyName StorageProfileName -NotePropertyValue $_.StorageProfile.Name } return $DiskDetails } Function Add-CIVMDisk { Param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$VM, [Parameter(Mandatory=$true)][string]$DiskSize, [string]$StorageProfile, [BusTypes]$BusType = 'paravirtual', [int]$BusId = 0, [AllowNull()][Nullable[System.int32]]$UnitId, [int64]$iops, [switch]$SkipCertificateCheck, [int]$TaskTimeout = 30 ) # Get the VM XML representation from the VCD API: $VMDiskXMLParams = @{VM = $VM} if ($SkipCertificateCheck) { $VMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $xmlvm = Get-VMDiskXML @VMDiskXMLParams # Convert specified disk size into MB: [int64]$SizeMB = GetDiskSizeMB -DiskSize $DiskSize # Get Vdc details for this VM and retrieve Storage Profiles available to this Vdc/VM: $VMSP = $VM.ExtensionData.StorageProfile $OrgVdc = Get-OrgVdc -Org ($VM.Org) $VDCSPs = @() $OrgVdc | ForEach-Object { $_.ExtensionData.vdcStorageProfiles.vdcStorageProfile | ForEach-Object { $VDCSPs += $_ } } $DefaultSP = $true # Check any user specified Storage Profile and match to available Storage Profiles: if ($StorageProfile -eq '*') { $DiskSP = $VMSP } else { if ($StorageProfile) { $DiskSP = $VDCSPs | Where-Object { $_.Name -eq $StorageProfile } $DefaultSP = $false if (!$DiskSP) { Write-Error ("Could not match Storage Profile $($StorageProfile) to any accessible Storage Profile available.") Write-Host -ForegroundColor Cyan ("Available Storage Profiles are:") $VDCSPs | ForEach-Object { Write-Host -ForegroundColor Cyan ($_.Name) } Break } } else { $DiskSP = $VMSP } } $VmDisks = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings | Where-Object { (($_.AdapterType -eq $BusType.value__) -and ($_.BusNumber -eq $BusId)) } # Build valid disk slot numbers based on BusType: $DiskSlots = @() Switch ($BusType) { "sata" { $DiskSlots = 0..29 } "ide" { $DiskSlots = 0..1 } default { $DiskSlots = 0..6 + 8..15 } } # If we've specified a UnitId that isn't valid for this bus type then give a meaningful error: if (($null -ne $UnitId) -and ($UnitId -notin $DiskSlots)) { Write-Error ("Cannot use UnitId $($UnitId) on Bus type '$($BusType)', exiting.") Write-Host -ForegroundColor Cyan ("Valid Unit Ids for $($BusType) are:") $DiskSlots | ForEach-Object { Write-Host -ForegroundColor Cyan ($_) } Break } # If we haven't specified a UnitId, find the first free/empty slot for the new disk: if ($null -eq $UnitId) { $SlotFound = $false Foreach ($slot in $DiskSlots) { $Disk = $VmDisks | Where-Object { $_.UnitNumber -eq $slot } if (!$Disk) { $SlotFound = $true $UnitId = $slot break } } if (!$SlotFound) { Write-Error ("Could not find an available slot to add a disk to bus of type $($BusType) and controller number $($BusId), exiting.") Break } } # Check if a disk already exists at the location we're attempting to use for the new disk: if ($VmDisks | Where-Object { $_.UnitNumber -eq $UnitId}) { Write-Error ("A disk already exists on VM '$($VM.Name)' at UnitId:$($UnitId) on bus:$($BusId) of type '$($BusType)', cannot add a new one, exiting.") Break } Write-Host ("Adding new disk to VM '$($VM.Name)', size:$($SizeMB)MB type '$($BusType)' on bus:$($BusId) at UnitId:$($UnitId).") # Set default namespace on returned XML to www.vmware.com/vcloud/v1.5 $nsm = New-Object System.Xml.XmlNamespaceManager($xmlvm.NameTable) $vcloudNS = "http://www.vmware.com/vcloud/v1.5" $nsm.AddNamespace($null,$vcloudNS) # Set the 'Modified' attribute on VmSpecSection as being changed: [void]$xmlvm.Vm.VmSpecSection.SetAttribute("Modified","true") # Define XML elements, attributes and values to be created: $DSElt = $xmlvm.CreateElement("DiskSettings",$vcloudNS) $XMLElts = [ordered]@{ SizeMb = $SizeMB UnitNumber = $UnitId BusNumber = $BusId AdapterType = $BusType.value__ ThinProvisioned = "true" StorageProfile = $DiskSP overrideVmDefault = if ($DefaultSP) { "false" } else { "true" } } if ($iops) { $XMLElts += @{iops = $iops} } $XMLElts.Keys | ForEach-Object { $DSSub = $xmlvm.CreateElement($_,$vcloudNS) if ($_ -eq 'StorageProfile') { [void]$DSSub.SetAttribute("href",$DiskSP.href) [void]$DSSub.SetAttribute("name",$DiskSP.Name) } else { $DSSubVal = $xmlvm.CreateTextNode($XMLElts.Item($_)) [void]$DSSub.AppendChild($DSSubVal) } [void]$DSElt.AppendChild($DSSub) } [void]$xmlvm.Vm.VmSpecSection.DiskSection.AppendChild($DSElt) # Update the VM with the new disk parameters: $SetVMDiskXMLParams = @{VM = $VM;BodyXml = $xmlvm.InnerXml;TaskTimeout = $TaskTimeout} if ($SkipCertificateCheck) { $SetVMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $result = Set-VMDiskXML @SetVMDiskXMLParams return $result } # Add-CIVMDisk Function # Function to remove a hard disk from a VM, deleted hard disks will be permanently removed and the contents lost. # VM must be powered-off for disks to be removed Function Remove-CIVMDisk { Param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$VM, [Parameter(Mandatory=$true)][BusTypes]$BusType, [Parameter(Mandatory=$true)][int]$BusId, [Parameter(Mandatory=$true)][int]$UnitId, [switch]$SkipCertificateCheck, [int]$TaskTimeout = 30, [switch]$Confirm ) if ($VM.Status -ne 'PoweredOff') { Write-Error ("VM '$($VM.Name)' must be powered off before a disk can be removed, exiting."); return $false } $VMDiskXMLParams = @{VM = $VM} if ($SkipCertificateCheck) { $VMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $xmlvm = Get-VMDiskXML @VMDiskXMLParams # Check to see if the disk we've specified to remove exists in the VM: $DiskToRemove = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings | Where-Object { (($_.AdapterType -eq $BusType.value__) -and ($_.BusNumber -eq $BusId) -and ($_.UnitNumber -eq $UnitId)) } if (!$DiskToRemove) { Write-Error ("Cannot find a disk on VM '$($VM.Name)' with Controller Type '$($BusType)' on Bus:$($BusId) at Unit:$($UnitId), exiting."); return $false } Write-Host -ForegroundColor Cyan ("Found a disk on VM '$($VM.Name)', Controller Type '$($BusType)' on Bus:$($BusId) at Unit:$($UnitId).") if (!$Confirm) { Write-Host -ForegroundColor Green ("Disk will not be removed, re-run this command with the -Confirm switch to actually remove/delete this disk.") Break } else { Write-Host -ForegroundColor Red ("-Confirm switch was specified, this disk will now be permenantly deleted.") } # Set the 'Modified' attribute on VmSpecSection as being changed: [void]$xmlvm.Vm.VmSpecSection.SetAttribute("Modified","true") # Prune the disk to be deleted from the DiskSection XML: $DiskNode = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings | Where-Object { $_.DiskId -eq $DiskToRemove.DiskId } [void]$DiskNode.ParentNode.RemoveChild($DiskNode) # Update the VM to remove the disk: $SetVMDiskXMLParams = @{VM = $VM;BodyXml = $xmlvm.InnerXml;TaskTimeout = $TaskTimeout} if ($SkipCertificateCheck) { $SetVMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $result = Set-VMDiskXML @SetVMDiskXMLParams return $result } # Remove-CIVMDisk Function Function Update-CIVMDiskSize { Param( [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$VM, [Parameter(Mandatory=$true)][BusTypes]$BusType, [Parameter(Mandatory=$true)][int]$BusId, [Parameter(Mandatory=$true)][int]$UnitId, [Parameter(Mandatory=$true)][string]$NewDiskSize, [switch]$SkipCertificateCheck, [int]$TaskTimeout = 30 ) # Get XML representation of this VM's disks $VMDiskXMLParams = @{VM = $VM} if ($SkipCertificateCheck) { $VMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $xmlvm = Get-VMDiskXML @VMDiskXMLParams # Find the disk to be resized: $DiskToResize = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings | Where-Object { (($_.AdapterType -eq $BusType.value__) -and ($_.BusNumber -eq $BusId) -and ($_.UnitNumber -eq $UnitId)) } if (!$DiskToResize) { Write-Error ("Could not find a disk on VM '$($VM.Name)' with BusType '$($BusType)' at Bus:$($BusId) and Unit:$($UnitId) to resize, exiting."); return $false } $NewSizeMB = GetDiskSizeMB -DiskSize $NewDiskSize if ($NewSizeMB -le $DiskToResize.SizeMb) { Write-Error ("Can't reduce size of disk on VM '$($VM.Name)' with BusType '$($BusType)' at Bus:$($BusId) and Unit:$($UnitId) from $($DiskToResize.SizeMb)MB to $($NewSizeMB)MB, this command can only increase the size of disks, exiting.") return $false } Write-Host ("Resizing disk on VM '$($VM.Name)' with BusType '$($BusType)' at Bus:$($BusId) and Unit:$($UnitId) from $($DiskToResize.SizeMb)MB to $($NewSizeMB)MB.") # Set the 'Modified' attribute on VmSpecSection as being changed: [void]$xmlvm.Vm.VmSpecSection.SetAttribute("Modified","true") # Set the new size in the XML respresentation of the VM disks: $DiskNode = $xmlvm.Vm.VmSpecSection.DiskSection.DiskSettings | Where-Object { $_.DiskId -eq $DiskToResize.DiskId } $DiskNode.sizeMb = $NewSizeMB # Update the VM to resize the disk: $SetVMDiskXMLParams = @{VM = $VM;BodyXml = $xmlvm.InnerXml;TaskTimeout = $TaskTimeout} if ($SkipCertificateCheck) { $SetVMDiskXMLParams += @{'SkipCertificateCheck'=$true}} $result = Set-VMDiskXML @SetVMDiskXMLParams return $result } # Update-CIVMDiskSize Function # Export cmdlets from this module to PS: Export-ModuleMember -Function 'Get-CIVMDisk' Export-ModuleMember -Function 'Add-CIVMDisk' Export-ModuleMember -Function 'Update-CIVMDiskSize' Export-ModuleMember -Function 'Remove-CIVMDisk' |