Expanding System Volume C Drive of Windows Hyper-V Virtual Machine

There are three three steps to expand a disk of a virtual machine:

  1. Connect to the host of the target guest vm to obtain the virtual disk path
  2. Expand the virtual disk using provided utilities
  3. 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

Leave a Reply

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