# Hyper-V-Guest-VM-Volume-Expansion.ps1
# Set variables:
$vmName="TESTWindows.intranet.kimconnect.net";
$driveLetter="C";
$newSize="160GB";
function selectDisk($vmname){
$paths=(get-vm $vmName | select -expand HardDrives).Path #this function is backward compatible to legacy Hyper-V
function pickItem{
do {
$flag=$false
for ($i=0;$i -lt $paths.count;$i++){
write-host "$i`t: $($paths[$i])"
}
[int]$pick=Read-Host -Prompt "`n---------------------------------------`nPlease pick an item from the above list`n---------------------------------------`n"
if($pick -lt $paths.count -and $pick -gt -1){
write-host "Picked index is: $pick"
$flag=$true }
}
until ($flag)
return $paths[$pick]
}
$selectedDisk=pickItem
return $selectedDisk
}
function backupAndExpandVhdx{
param($vmName,$sourceDisk,$newSize)
# Remove all snapshots prior to performing disks expansion
try {
Get-VMSnapshot -VMName $vmName | Remove-VMSnapshot
}
catch{
write-warning "Unable to remove VM snapshots as precautionary to disk expansions. Program now aborts."
return $false
}
function backupVmDisk{
param($vmName,$sourceDisk)
$ErrorActionPreference='stop'
try{
write-host "Creating a 'backup' folder in parent folder..."
$parentFolder=split-path $sourceDisk
$fileName=split-path $sourceDisk -Leaf
$backupFolder="$parentFolder\backup"
New-Item -ItemType Directory -Force -Path $backupFolder
write-host "Copying source file $sourceDisk into backup directory $backupFolder\$fileName..."
copy-item $sourceDisk -Destination "$backupFolder\$fileName"
write-host "Backup has been created as '$backupFolder\$fileName'`r`nPlease remove this item after successful validations." -ForegroundColor Yellow
return "$backupFolder\$fileName"
}
catch{
write-warning "$($error[0])"
return $false
}
}
$backupFile=backupVmDisk $vmName $sourceDisk
if($backupFile){
[uint64]$vmVolumeCurrentSize=(get-vhd $sourceDisk).Size
if(!($newSize)){
write-host "As $newSize is not specified, now setting its value to double its original size"
$newSizeBytes=$vmVolumeCurrentSize*2
}else{
$newSizeBytes=$newSize/1;
}
if ($newSizeBytes -gt $vmVolumeCurrentSize){
try{
write-host "Attempting to resize $sourceDisk to $newSizeBytes bytes"
Resize-VHD -Path $sourceDisk -SizeBytes $newSizeBytes -EA Stop;
write-host "$sourceDisk has been resized to $newSizeBytes bytes successfully."
return $backupFile
}
catch{
Write-Warning "$($error[0])"
return $false
}
}
else{
write-warning "New size`t: $newSizeBytes must be larger than current size`t: $vmVolumeCurrentSize"
return $false
}
}
else{
write-warning "$($error[0])"
return $false
}
}
function pingHost{
param($computername="localhost",$waitTime=30)
write-host "Pinging $computername and retry until wait-timer expires.."
$t=0;
do {
write-host "Pinging $ComputerName..."
$hostIsPingable=if(Test-Connection $ComputerName -Count 1 -Delay 1 -ErrorAction SilentlyContinue){$true}else{$false}
if(!($hostIsPingable)){
write-host -nonewline ".";
$t+=5;
sleep 5;
}
}while (!($hostIsPingable) -and $t -le $waitTime)
return $hostIsPingable
}
# Connect to Windows OS of Guest-VM & Resize volume to its available maximum
function expandWindowsVolume{
param($ComputerName,$driveLetter="C",$newSize="160GB",$credential)
$ErrorActionPreference='stop'
$localComputerName=$env:computername
$localIps=([System.Net.Dns]::GetHostAddresses($env:computername)|?{$_.AddressFamily -eq "InterNetwork"}|%{$_.IPAddressToString;}|out-string).Trim();
$isLocal=if($ComputerName -match "(localhost|127.0.0.1|$localComputerName)" -or $localIps -match $ComputerName){$true}else{$false}
function expandVolume{
param($driveLetter,$newSize="160GB")
write-host "Refreshing HostStorageCache before attempting to expand drive letter $driveLetter..."
Update-HostStorageCache
# Before expansion
$targetDrive = (gwmi -Class win32_volume -Filter "DriveType=3" -ea stop| ?{$_.DriveLetter -eq "$driveLetter`:"}|`
Select-object @{Name="Letter";Expression={$_.DriveLetter}},`
@{Name="Label";Expression={$_.Label}},`
@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}},`
@{Name = "Available"; Expression = {"{0:N2} GiB" -f ($_.FreeSpace/1073741824)}},`
@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}}`
| ft -autosize | Out-String).Trim()
write-host "Before expansion:`r`n$targetDrive"
$newSizeGb=[math]::round($newSize /1Gb, 4);
Write-Host "`r`nNow attempting to resize $driveLetter to $newSizeGB`GB...`r`n";
$max=(Get-PartitionSupportedSize -DriveLetter $driveLetter).SizeMax;
$maxGb=[math]::round($max/1GB,4);
if ($newSizeGb -gt $maxGb){
try{
Resize-Partition -DriveLetter $driveLetter -Size $max -ea Stop
Update-HostStorageCache # After expansion validation
$targetDrive = (gwmi -Class win32_volume -Filter "DriveType=3" -ea stop| ?{$_.DriveLetter -eq "$driveLetter`:"}|`
Select-object @{Name="Letter";Expression={$_.DriveLetter}},`
@{Name="Label";Expression={$_.Label}},`
@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}},`
@{Name = "Available"; Expression = {"{0:N2} GiB" -f ($_.FreeSpace/1073741824)}},`
@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}}`
| ft -autosize | Out-String).Trim()
Write-Host "After expansion:`r`n$targetDrive."
return $true
}
catch{
write-warning "$($error[0])"
return $false
}
}
else{
write-warning "There appears to be an error with the value: $newSize."
return $false
}
}
if ($isLocal){
try{
write-host "attempting to expand volume $driveLetter to $newSize..."
expandVolume -driveLetter $driveLetter -newSize $newSize;
return $true
}
catch{
write-warning "$($error[0])"
return $false
}
}
else{
try{
$maxRetries=3
$attempts=0
do{
$attempts++;
$session=new-pssession -ComputerName $vmName -Credential (get-credential)
}while(!$session -or $attempts -le $maxRetries)
if($session){
$result=invoke-command -Session $session -Credential $credential -ScriptBlock{
param($expandVolume,$driveLetter,$newSize)
[ScriptBlock]::Create($expandVolume).Invoke($driveLetter,$newSize);
}-Args ${function:expandVolume},$driveLetter,$newSize
Remove-PSSession $session
return $result
}
else{
write-warning "Unable to invoke-command on $vmName"
return $false
}
}
catch{
write-warning "$($error[0])"
return $false
}
}
}
# This function assumes that a VMName is same as its Windows Name
function expandVolumeGuestVm{
param($vmName,$driveLetter="C",$newSize="160GB")
if(pingHost $vmName){
$sourceDisk=selectDisk $vmName
if($sourceDisk){
$backupFile=backupAndExpandVhdx $vmName $sourceDisk $newSize
}
if($backupFile){
$success=expandWindowsVolume $vmname $driveLetter $newSize
}
if ($success){
write-host "Cleanup routine`t: removing $backupfile..."
remove-item $backupFile
}
else{
write-warning "There were errors in the Windows Volume Expansion process. Please restore volume $driveLetter from $backupFile manually"
}
}
}
expandWindowsVolume $vmname $driveLetter $newSize
# Outdated code...
# Hyper-V-Guest-VM-C-Drive-Expansion.ps1
# Step 1: perform a backup of Guest-VM default disk file then increase its disk allocation size
# Step 2: take a snapshot of VM and expand C-Drive at the guest Windows OS level
# Step 3: validate results
# Step 4: run cleanup routine
# Set variables:
$vmName="SHEVER007";
$diskIndex=0; # Index 0 correspond to default volume C:\ in Windows
$driveLetter="C";
$newSize="250GB";
$snapshot1="BeforeExpandingVmdk"
$snapshot2="AfterExpandingVmdk"
$snapshot3="BeforeExpandingCDrive"
$snapshot4="AfterExpandingCDrive"
$backupVMDiskFile=$true
# Enable VMIntegration for Shutdown Commands
function ensureVMIntegrationForShutdown{
# Ensure that the VM Integration Service for Shutdown method was enabled
$vmIntegrationShutdownEnabled=(Get-VMIntegrationService -VMName $vmName | ?{$_.Name -eq "Shutdown"}).Enabled
if (!($vmIntegrationShutdownEnabled)){Enable-VMIntegrationService -VMName $vmName -Name "Shutdown"}
}
# Function to gracefully shutdown remote computer
function gracefulShutdown{
param($RemoteComputer)
# Ping computer, retry until wait-time expires
function pingHost{
param($computername="localhost",$waitTime=30)
$t=0;
do {
write-host "Pinging $ComputerName..."
$hostIsPingable=if(Test-Connection $ComputerName -Count 1 -Delay 1 -ErrorAction SilentlyContinue){$true}else{$false}
if(!($hostIsPingable)){
write-host -nonewline ".";
$t+=5;
sleep 5;
}
}while (!($hostIsPingable) -and $t -le $waitTime)
return $hostIsPingable
}
function enableRemoteWinRM{
Param([string]$computername)
Write-Host "checking $computername..."
function pingTest{
Param([string]$node)
try{
Return Test-Connection $node -Count 1 -Quiet -ea Stop;
}
catch{Return $False}
}
if (pingTest $computername){
if (!(Test-WSMan $computername -ea SilentlyContinue)){
if(!(get-command psexec)){
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install sysinternals -y
}
psexec.exe \\$computername -s C:\Windows\system32\winrm.cmd qc -quiet
}else{Write-Host "WinRM has been already enabled. No changes to WinRM have been made."}
}
Else{Write-Host "Unable to determine if WinRM is enabled on $computername`.`n Ping test has failed. Check if this computer is online and whether there's a firewall blocking of ICMP";}
}
# function to connect to remote machine and execute commands
function connectWinRmToShutdown{
param($remoteComputer)
do{
$session = New-PSSession -ComputerName $RemoteComputer
"Connecting to remote computer $computer..."
sleep -seconds 5
if ($session){"Connected."}
} until ($session.state -match "Opened")
invoke-command -session $session -scriptblock{
# Gracefully stop SQL if it is present
Get-service *SQL* | Where-Object {$_.status -eq "Running"} | %{Stop-Service -Name $_ -Force} | Out-Null
#force in this context will enable the process to shutdown dependent services prior to stopping the target service
# Gracefully stop Exchange if it is present
$exchangeServices=Get-Service | Where-Object {$_.DisplayName –like "Microsoft Exchange *"};
if($exchangeServices){
net stop msexchangeadtopology /y;
net stop msexchangefba /y;
net stop msftesql-exchange /y;
net stop msexchangeis /y;
net stop msexchangesa /y;
net stop iisadmin /y;
net stop w3svc /y;
$exchangeServices | Stop-Service -Force;
}
# Finally shutdown the OS via Windows native method
Stop-Computer $env:computername -Force
}
$pingResult=pingHost -computername $RemoteComputer -waitTime 30
if (!($pingResult)){write-host "$RemoteComputer is now offline."}
}
# Attempt to connect to remote machine via WinRM
try{
enableRemoteWinRM -computername $RemoteComputer
connectWinRmToShutdown -remoteComputer $RemoteComputer
}catch{
write-host $Error;
break;
}
}
# Make a snapshot as checkpoints
function createSnapshot{
param(
$vmName,
$snapshotName="$(Get-Date -Format 'yyyy-MM-dd-hhmmss')"
)
# take snapshot
Checkpoint-VM -Name $vmName -SnapshotName $snapshotName
# Verify snapshot
get-vmsnapshot $vmname
}
function backupVmDisk{
param($vmName,$diskIndex=0)
# Make a backup of existing disk
if (!($vmVolume)){$GLOBAL:vmVolume=get-vm $vmName | select -expand HardDrives | select -Index $diskIndex}
if (!($vmVolumeFile)){$GLOBAL:vmVolumeFile=$vmVolume.Path}
# Create a backup folder in parent folder
$parentFolder=split-path $vmVolumeFile
$fileName=split-path $vmVolumeFile -Leaf
$backupFolder="$parentFolder\backup"
New-Item -ItemType Directory -Force -Path $backupFolder
# Copy source file into backup directory
copy $vmVolumeFile -Destination "$backupFolder\$fileName"
}
# Expand the targeted disk
function expandVmDisk{
param($vmName,$diskIndex,$newSize)
# Remove all snapshots prior to performing disks expansion
try {
Get-VMSnapshot -VMName $vmName | %{Remove-VMSnapshot -VMName $vmName -Name $_.Name}
}
catch{
write-host "unable to remove all snapshots. Aborting Remove-VMSnapshot commands."
break;
}
# Gather some global variables
$GLOBAL:vmVolume=get-vm $vmName | select -expand HardDrives | select -Index $diskIndex
$GLOBAL:vmVolumeFile=$vmVolume.Path
#if (!($vmVolume)){$GLOBAL:vmVolume=get-vm $vmName | select -expand HardDrives | select -Index $diskIndex}
#if (!($vmVolumeFile)){$GLOBAL:vmVolumeFile=$vmVolume.Path}
# if $newSize is not specified, set disk default value to double its original size
if(!($newSize)){
[double]$vmVolumeCurrentSize=($vmVolume | get-vhd).Size
$newSizeBytes=$vmVolumeCurrentSize*2
}else{
$newSizeBytes=$newSize/1;
}
if ($newSizeBytes -gt $vmVolumeCurrentSize){
$success=Resize-VHD -Path $vmVolumeFile -SizeBytes $newSizeBytes -InformationAction SilentlyContinue;
if ($success){write-host "$vmVolumeFile has been resized to $newSize successfully."}
}
}
# Ping computer, retry until wait-time expires
function pingHost{
param($computername="localhost",$waitTime=30)
$t=0;
do {
write-host "Pinging $ComputerName..."
$hostIsPingable=if(Test-Connection $ComputerName -Count 1 -Delay 1 -ErrorAction SilentlyContinue){$true}else{$false}
if(!($hostIsPingable)){
write-host -nonewline ".";
$t+=5;
sleep 5;
}
}while (!($hostIsPingable) -and $t -le $waitTime)
return $hostIsPingable
}
# Connect to Windows OS of Guest-VM & Resize volume to its available maximum
function expandWindowsVolume{
param($ComputerName,$driveLetter="C",$newSize="160GB")
$localComputerName=$env:computername
$localIps=([System.Net.Dns]::GetHostAddresses($env:computername)|?{$_.AddressFamily -eq "InterNetwork"}|%{$_.IPAddressToString;}|out-string).Trim();
$isLocal=if($ComputerName -match "(localhost|127.0.0.1|$localComputerName)" -or $localIps -match $ComputerName){$true}else{$false}
function expandVolume{
param($driveLetter,$newSize="160GB")
Update-HostStorageCache
# Before
$cDrive = (gwmi -Class win32_volume -Filter "DriveType!=5" -ea stop| ?{$_.DriveLetter -eq "$driveLetter`:"}|`
Select-object @{Name="Letter";Expression={$_.DriveLetter}},`
@{Name="Label";Expression={$_.Label}},`
@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}},`
@{Name = "Available"; Expression = {"{0:N2} GiB" -f ($_.FreeSpace/1073741824)}},`
@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}}`
| ft -autosize | Out-String).Trim()
write-host "Before:`r`n$cDrive"
$newSizeGb=[math]::round($newSize /1Gb, 4);
Write-Host "`r`nNow attempting to resize $driveLetter to $newSizeGB`GB...`r`n";
$max=(Get-PartitionSupportedSize -DriveLetter $driveLetter).SizeMax;
$maxGb=[math]::round($max/1GB,4);
if ($newSizeGb -gt $maxGb){Resize-Partition -DriveLetter $driveLetter -Size $max}else{write-host "There appears to be an error with the value: $newSize."}
# After
Update-HostStorageCache
$cDrive = (gwmi -Class win32_volume -Filter "DriveType!=5" -ea stop| ?{$_.DriveLetter -eq "$driveLetter`:"}|`
Select-object @{Name="Letter";Expression={$_.DriveLetter}},`
@{Name="Label";Expression={$_.Label}},`
@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}},`
@{Name = "Available"; Expression = {"{0:N2} GiB" -f ($_.FreeSpace/1073741824)}},`
@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}}`
| ft -autosize)
Write-Host "`r`n$driveLetter has been resized to $newSize."
}
if(pingHost -computername $ComputerName){
if ($isLocal){
expandVolume -driveLetter $driveLetter -newSize $newSize;
}else{
invoke-command -computername $ComputerName -ScriptBlock{
param($expandVolume,$driveLetter,$newSize)
[ScriptBlock]::Create($expandVolume).Invoke($driveLetter,$newSize);
}-Args ${function:expandVolume},$driveLetter,$newSize
}
}else{
write-host "$ComputerName is not reachable via ICMP"
}
}
function cleanupRoutine{
param($vmName,$vmdkFile)
# Cleanup snapshots upon successful completion of disk resizing steps
Remove-VMSnapshot -VMName $vmName -Name $snapshot4
Remove-VMSnapshot -VMName $vmName -Name $snapshot3
Remove-VMSnapshot -VMName $vmName -Name $snapshot2
Remove-VMSnapshot -VMName $vmName -Name $snapshot1
# Delete backup VMDK file
if (test-path "$vmdkFile.bak" -ErrorAction SilentlyContinue){remove-item "$vmdkFile.bak" -Force -ErrorAction SilentlyContinue}
# disconnect any active pssessions
$activeExchangeOnlineSessionIds=(Get-PSSession |?{$_.ConfigurationName -eq "Microsoft.Exchange"}).Id
if($activeExchangeOnlineSessionIds){
Remove-PSSession -id $activeExchangeOnlineSessionIds;
write-host "session ID $activeExchangeOnlineSessionIds is disconnected."
}
}
# Only run cleanup routine upon confirmation
function cleanup{
cleanupRoutine -vmName $vmName -vmdkFile $vmDefaultDVolumeFile
}
# Optional: revert all changes
function revertGuestVmDiskChanges{
param($vmName,$vmDefaultDVolumeFile)
# Optional: revert changes to checkpoint
# Restore-VMSnapshot -name $snapshotName -VMName $vmName
stop-vm -Name $vmName -Force
Rename-Item $vmDefaultDVolumeFile "$vmDefaultDVolumeFile.bad"
Rename-Item "$vmDefaultDVolumeFile.bak" $vmDefaultDVolumeFile
start-vm $vmName
}
function backupThenExpandVirtualDisk{
if($backupVMDiskFile){backupVmDisk -vmName $vmName -diskIndex $diskIndex}
expandVmDisk -vmName $vmName -newSize $newSize -diskIndex $diskIndex
createSnapshot -vmName $vmName -snapshotName $snapshot2
start-vm $vmName
}
function snapshotThenExpandWindowsVolume{
createSnapshot -vmName $vmName -snapshotName $snapshot3
expandWindowsVolume -ComputerName $vmName -driveLetter $driveLetter -newSize $newSize
createSnapshot -vmName $vmName -snapshotName $snapshot4
write-host "Please Run 'cleanup' command after manual validation has completed."
}
function expandVolumeGuestVmHyperV{
ensureVMIntegrationForShutdown
createSnapshot -vmName $vmName -snapshotName $snapshot1
$computerIsShutdown=gracefulShutdown -RemoteComputer $vmName
#stop-vm -Name $vmName
if ($computerIsShutdown){
backupThenExpandVirtualDisk;
$hostPingable=pingHost -computername $vmName
if ($hostPingable){
snapshotThenExpandWindowsVolume;
}else{write-host "Manually bring $vmName online, then run 'snapshotThenExpandWindowsVolume'."}
}else{write-host "Manually shutdown the Guest VM, then run 'expandVolumeGuesVmHyperV' again."}
}
write-host "Run 'expandVolumeGuestVmHyperV' when ready."
# Cloning
function cloneVM{
param($sourceVhdx,$destinationVhdx,$newName,$ram="4096MB")
New-vm -Name $newName -MemoryStartupBytes $ram -VHDPath $destinationVhdx
}
<# An example anomaly
$sherver="SHEVER007"
Get-VMSnapshot -VMName $sherver | %{Remove-VMSnapshot -VMName $sherver -Name $_.Name}
$path="C:\ClusterStorage\Volume1\VHD\"
# These were 160GB
$disks="SHEVER007-0623-419E-9DBA-C1AE5AAF04C8.avhdx","SHEVER007-2806-4CD2-AA9D-BD2D2A74C51E.avhdx","SHEVER007-5A73-4717-A1D5-C6FAE11EED67.avhdx","SHEVER007-312B-409A-93EE-BE51DA842462.avhdx","SHEVER007-8F48-4B35-802E-D7DD8CA0EA8B.avhdx"
# These were 250GB
C:\ClusterStorage\Volume1\VHD\SHEVER007-21C6-4164-8943-3A92E1B58F6F.avhdx
C:\ClusterStorage\Volume1\VHD\SHEVER007.vhdx
$size="250GB"/1
$disks|%{Resize-VHD -Path "$path$_" -SizeBytes $size -InformationAction SilentlyContinue}
Get-VMSnapshot -VMName "SHEVER007" | %{Remove-VMSnapshot -VMName "SHEVER007" -Name $_.Name}
$disks="SHEVER007-0623-419E-9DBA-C1AE5AAF04C8.avhdx"
SHEVER007-2806-4CD2-AA9D-BD2D2A74C51E.avhdx
SHEVER007-5A73-4717-A1D5-C6FAE11EED67.avhdx
SHEVER007-312B-409A-93EE-BE51DA842462.avhdx
SHEVER007-8F48-4B35-802E-D7DD8CA0EA8B.avhdx
SHEVER007-21C6-4164-8943-3A92E1B58F6F.avhdx
C:\ClusterStorage\Volume1\VHD\SHEVER007.vhdx
$size="250GB"/1
$disks|%{Resize-VHD -Path $_ -SizeBytes $size -InformationAction SilentlyContinue}
#>
Categories: