There are three three steps to expand a disk of a virtual machine:
- Connect to the host of the target guest vm to obtain the virtual disk path
- Expand the virtual disk using provided utilities
- Expand the volume within OS of the guest VM (Windows)
# Simple Version
# Expand disk in Hyper-V
$vmName='TEST-WINDOWS'
$diskIndex=0
$newSize='100GB'
$virtualDisks=get-vm $vmName | Select-Object -expand HardDrives
$targetDisk=$virtualDisks[$diskIndex]
$vmVolumeCurrentSize=(get-vhd $targetDisk.Path).Size
$resizableOnline=if($targetDisk.ControllerType -eq 'SCSI'){$true}else{$false}
if($resizableOnline -and $vmVolumeCurrentSize -lt $($newSize/1)){
Resize-VHD -Path $targetDisk.Path -SizeBytes $($newSize/1)
}else{
write-warning "Controller type of $($targetDisk.ControllerType) requires that $vmName be taken offline to perform the disk resizing operations"
}
# Expand Disk within Guest VM (Windows), assuming no firewall blocks and same domain
$vmName='TEST-WINDOWS'
$driveLetter='C'
Write-Host "Expanding $driveLetter for $vmName"
$session=new-pssession -computername $vmName
Invoke-Command -Session $session -ScriptBlock{
param($driveLetter)
# Resize volume to its available maximum
try{
Update-HostStorageCache
$max=(Get-PartitionSupportedSize -DriveLetter $driveLetter).SizeMax
Resize-Partition -DriveLetter $driveLetter -Size $max -ea Stop
return $true
}catch{
write-warning $_
return $false
}
} -Args $driveLetter
Remove-PSSession $session
# resizeVirtualDiskUncPath.ps1
# Version 0.0.1
#
# Functions:
# a. Find the host of the target VM
# b. Collect virtual disk information of target VM
# c. Resize virtual disk
#
# Note:
# The current script doesn't work 100% because of 2nd-hop issues. UNC Paths cannot be accessed via WinRm Sessions.
# Therefore, this post is useful to perform discovery prior to manually console onto the host to perform disk expansions.
$vmName='testWindows.kimconnect.com'
$diskIndex=0
$newSize='100GB'
function getHyperVHostsInForest{
function includeRSAT{
$ErrorActionPreference='stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
#$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
$rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
$rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
$rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
$rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
$rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
# This command does not work on Windows 2012R2
#$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
#Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
#NT\CurrentVersion.
#At line:1 char:2
#+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
# + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
$releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
$osVersion=[System.Environment]::OSVersion.Version
[double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)"
$osName=(Get-WmiObject Win32_OperatingSystem).Name
#$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
# 1 {'client'}
# 2 {'domaincontroller'}
# 3 {'memberserver'}
# }
$windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
switch ($releaseId){
1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
}
switch ($osVersionMajorMinor){
{$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
{$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
{$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
{$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
}
if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
try{
# If OS is Windows Server, then install RSAT using a different method
if ($osName -match "^Microsoft Windows Server") {
# This sequence has confirmed to be valid on Windows Server 2008 R2 and above
Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
Import-Module ServerManager
Add-WindowsFeature RSAT-AD-PowerShell
}
else{
Write-Verbose "This sequence targets Windows Client versions"
$destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
Write-Host "Downloading RSAT from $link..."
Start-BitsTransfer -Source $link -Destination $destinationFile
$fileCheck=Get-AuthenticodeSignature $destinationFile
if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
$wusaCommand = $destinationFile + " /quiet"
Write-host "Installing RSAT - please wait..."
Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
}
return $true
}
catch{
write-warning "$($error[0].Exception)"
return $false
}
}else{
Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
return $true
}
}
function listAllHyperVNodes($verbose=$true){
try{
$timer=[System.Diagnostics.Stopwatch]::StartNew()
$domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}
foreach ($domain in $domains){
#[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
$allClusters=(get-cluster -domain $domain).Name
if($verbose){
$elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
write-host "Minutes elapsed $elapsed`: cluster names collected"
}
$nodes=$allClusters|%{try{Get-ClusterGroup -Cluster $_ -ea SilentlyContinue}catch{}}
if($verbose){
$elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
write-host "Minutes elapsed $elapsed`: Hyper V node names collected"
}
$hyperVs=($nodes|Group-Object -Property OwnerNode).Name
if($verbose){
$elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
write-host "Minutes elapsed $elapsed`: Hyper V node names collected - Done!"
}
}
return $hyperVs
}catch{
Write-Error $_
return $false
}
}
function sortArrayStringAsNumbers([string[]]$names){
$hashTable=@{}
foreach ($name in $names){
#[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
#$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
$x=.{[void]($name -match '(?:.(\d+)+)$');($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft(8,'0')}
$hashTable.Add($name,$x)
}
$sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
return $sorted
}
try{
$null=includeRSAT;
$hyperVHosts=listAllHyperVNodes
$hyperVHostNames=sortArrayStringAsNumbers $hyperVHosts
return $hyperVHostNames
}catch{
Write-Error $_
return $false
}
}
function getVmHost($vmName){
try{
if (Test-NetConnection $vmname -CommonTCPPort WINRM -InformationLevel Quiet) {
$hostName=invoke-command $vmName -scriptblock {
(get-item "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters").GetValue('HostName')
} -ErrorAction Stop
}elseif([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$vmName)){
$hostName=([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$vmName)).OpenSubKey('SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters').GetValue('PhysicalHostName')
}else{
Write-Host "$vmName not reachable" -BackgroundColor DarkRed
}
return $hostName
}catch{
Write-Warning $_
write-host "Program will now search through all Hyper-V hosts for $vmName... This may take awhile"
$hyperVHostNames=getHyperVHostsInForest
foreach ($hyperVHost in $hyperVHostNames){
$hostFound=invoke-command -computername $hyperVHost -scriptblock{
param ($vmName)
Get-VM $vmName -ea Ignore
} -Args $vmName
if($hostFound){
return $hyperVHost
}
}
}
}
$vmHost=getVmHost $vmName
$virtualDisks=invoke-command -computername $vmHost -scriptblock {
param ($vmname)
get-vm $vmName | Select-Object -expand HardDrives
# Get-VM -VMName $vmName | Select-Object VMId | Get-VHD
# Getting the mounted storage instance for the path '\\UNCPATH\diskname.vhdx' failed.
# You do not have permission to perform the operation. Contact your administrator if you believe you should have permission to perform this operation.
# + CategoryInfo : PermissionDenied: (:) [Get-VHD], VirtualizationException
# + FullyQualifiedErrorId : AccessDenied,Microsoft.Vhd.PowerShell.Cmdlets.GetVHD
# + PSComputerName : HYPER-V-TEST
} -args $vmName
write-host "Assuming C:\ volume matches the first virtual disk at index $diskIndex"
$targetDisk=$virtualDisks|select-object -first $($diskIndex+1)
# [uint64]$vmVolumeCurrentSize=get-vhd $targetDisk
# get-vhd : Getting the mounted storage instance for the path '\\UNCPATH\diskname.vhdx' failed.
# The operation cannot be performed while the object is in use.
# At line:1 char:1
# + get-vhd $targetDisk.Path
# + ~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : ResourceBusy: (:) [Get-VHD], VirtualizationException
# + FullyQualifiedErrorId : ObjectInUse,Microsoft.Vhd.PowerShell.Cmdlets.GetVHD
##################### This part isn't working due to 2nd-hop issues ################################
# The following lines cannot be executed if the VM is online and target disk is via UNC Path
$resizableOnline=if($targetDisk.ControllerType -eq 'SCSI'){$true}else{$false}
if($resizableOnline -and $vmVolumeCurrentSize -lt $($newSize/1)){
Resize-VHD -Path $targetDisk.Path -SizeBytes $($newSize/1)
}else{
write-warning "Controller type of $($targetDisk.ControllerType) requires that $vmName be taken offline to perform the disk resizing operations"
}
##### Current workaround would be to RDP/Console onto the Hyper-V Host to perform disk expansion ####
# Expand Disk in Windows
$driveLetter='C'
$session=New-PSSession -ComputerName $vmHost
if($session){
Write-Host "Expanding $driveLetter for $fileServerRole currently owned by $roleOwner...";
Invoke-Command -Session $session -ScriptBlock{
param($driveLetter)
# Resize volume to its available maximum
try{
Update-HostStorageCache
$max=(Get-PartitionSupportedSize -DriveLetter $driveLetter).SizeMax
Resize-Partition -DriveLetter $driveLetter -Size $max -ea Stop
return $true
}catch{
write-warning $_
return $false
}
} -Args $driveLetter
}
Remove-PSSession $session
Categories: