PowerShell: Hyper-V Guest-VM C-Volume Expansion

# 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}
#>

Leave a Reply

Your email address will not be published. Required fields are marked *