Table of contents
There is one function whose absence in all official Hyper-V tools is blatantly obvious, and that is the ability to gracefully restart virtual machines. A Restart-VM function was introduced in 2012, but it just hard stops the VM before turning it back on. I suppose it has uses, but I’ve never found a good one. So, I set out to create my own. Here are the results.
Script details
The core functionality of the script is very basic. It performs a graceful shutdown of the VM, waits for it to be in an Off state, then turns it back on. It can work against multiple VMs at once, and runs each process in a separate background job so that the virtual machine restarts occur simultaneously. There are a number of things to be aware of with this script, so please read carefully before using.
This Script Should be Dot-Sourced
The first is that it was scripted to be dot-sourced. If you simply execute it, nothing will happen. The reason I created it that way is that this script uses the same name as the existing Restart-VM function, so it will create a shadow of it. This means that while this script is loaded, calling Restart-VM will use this function instead of the built-in cmdlet.
In order to dot-source a script, you just enter a period, a space, and the path to the script file. So, if you save this as C:ScriptsRestart-VM.ps1:
. C:ScriptsRestart-VM.ps1
From that point onward, this script would be called any time you use Restart-VM within the same session. What I think would work best is if you automatically loaded it into your PowerShell profile so that it’s always present rather than accidentally running the built-in cmdlet and crashing VMs.
There are a lot of resources available to help you auto-load scripts into your profile. I use something similar to method #5 on this blog post. Simple instructions are to go to %userprofile%Documents and create a subfolder named WindowsPowerShellProfile. Inside that folder, create a file named Microsoft.PowerShell_profile.ps1. If you also want it to load when you start the PowerShell ISE, you have to create a file named Microsoft.PowerShellISE_profile.ps1. Inside the file(s), insert a line that dot-sources the script file. You can also do this with PowerShell:
New-Item -Path $env:USERPROFILEDocumentsWindowsPowerShell -Type Directory Set-Content -Path $env:USERPROFILEDocumentsWindowsPowerShellMicrosoft.PowerShell.profile.ps1 -Content ". C:ScriptsRestart-VM.ps1"
If you use PowerShell Remoting, you’ll need a workaround to get a profile to load in remote sessions. Directions are included in Kiquenet’s response in this Stack Overflow thread, which is, unfortunately, not the accepted answer.
All of the Base Cmdlet’s Parameters are Shadowed, but Some Work Differently
I read somewhere that if you’re going to write a function that shadows a built-in cmdlet that it should include all the same parameters so that people using it don’t have to change any scripts that depend on it. I did that with this function. However, the output isn’t going to be identical because the function behaves differently.
Force: The built-in cmdlet uses the Force parameter to suppress confirmation prompts. Otherwise, it asks you on each and every VM if you want to perform the restart. Since this script is much less dangerous and because of the way that I implemented the restart function, it will not ask you for confirmation. If the virtual machine has the Shutdown integration service enabled and responding, it will restart the VM without question. If the VM does not have the service enabled or it is not responding, then it will throw a warning and skip the VM. You will need to specify Force if you want VMs like that to restart, because it won’t be a graceful shutdown. Be aware that this only uses the built-in Stop-VM cmdlet. If the containing vmwp.exe process is hung, you’ll need to deal with that the hard way.
AsJob: I didn’t try this with the built-in cmdlet, but I think it just returns a single job no matter how many VMs are specified. My function returns a separate job for each VM that it is restarting. These jobs will be named in the format: Restart-VM-HostName–VMName–VMId.
Verbose: I didn’t look at their verbose output, but I rolled my own for this one. It’s not as descriptive as I would like it to be, but that’s because I don’t know if there’s a way to return verbose output from a background job.
The Script
There is some more explanatory text to come, but if you’ve absorbed the above then you’re ready for the script itself. Save the contents of the following to a .PS1 file. I wrote it with the expectation that you would use Restart-VM.ps1, but it doesn’t really matter.
#requires -Version 3 #requires -Modules Hyper-V function Restart-VM { <# .SYNOPSIS Restarts one or more virtual machines by performing an orderly shutdown and startup sequence. .DESCRIPTION Restarts one or more virtual machines by performing an orderly shutdown and startup sequence. Shadows the built-in Restart-VM, which does not contain the same functionality. .PARAMETER Name A string array containing the name(s) of the virtual machine to restart. .PARAMETER ComputerName The name(s) of the computer(s) that host(s) the target virtual machine(s). If not specified, the local host will be used. .PARAMETER AsJob Places the restart operation in a job and returns immediately. Cannot be included with Passthru. .PARAMETER Force If the virtual machine is not running integration services, or if the integration services do not respond promptly, performs a Turn Off operation. If not specified, only virtual machines with responding integration services will be restarted. .PARAMETER VM The virtual machine object to be restarted. .PARAMETER Timeout Maximum number of seconds to wait for the target system to shut down gracefully before performing a Turn Off operation. Default is eternity. If multiple virtual machines are specified, each has its own separate timer. .PARAMETER ForceTimeout Number of seconds to wait for a forced turn off to work before assuming the worker process is hung. Default and minimum is 5 seconds. If multiple virtual machines are specified, each has its own separate timer. Has no effect if Force is not also specified. .PARAMETER Passthru Causes the script to emit a virtual machine object that represents the VM that was restarted. Cannot be included with AsJob. .OUTPUTS None by default. If -Passthru is specified, Microsoft.HyperV.PowerShell.VirtualMachine. If -AsJob is specified, System.Management.Automation.PSRemotingJob. .NOTES Author: Eric Siron Copyright: (C) 2014 Altaro Software Version 1.0.2 Authored Date: November 15, 2014 1.0.2 revision: April 27, 2016: Minor update to VM selection logic. 1.0.1 revision: November 26, 2014: Minor fix to timeout logic. .LINK https://virtualizationdojo.com/hyper-v/free-script-restart-vm/ .EXAMPLE C:PS> Restart-VM svtest Description ----------- Restarts the virtual machine named test on the local host. .EXAMPLE C:PS> Restart-VM svhungsystem -Force Description ----------- Restarts the virtual machine named svhungsystem on the local host even if it is not responding. .EXAMPLE C:PS> Get-VM -Name svtrouble | Restart-VM -Force -ForceTimeout 30 Description ----------- Forces the virtual machine named svtrouble to shut down, but extends the maximum wait time from 5 seconds to 30 seconds. .EXAMPLE C:PS> Restart-VM svserver1, svserver2 -ComputerName svhv1 -Timeout 60 Description ----------- Restarts VMs named svserver1 and svserver2 on Hyper-V host svhv1. If either does not shut down within 60 seconds, it is forcefully turned off. .EXAMPLE C:PS> Get-ClusterVM | Restart-VM Description ----------- Using the output of Get-ClusterVM (another free script from Altaro), restarts all cluster VMs. #> [CmdletBinding(DefaultParameterSetName='ByName', SupportsShouldProcess=$true)] param( [Alias("VMName")] [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName='ByName', Mandatory=$true, Position=1)] [String[]]$Name, [Alias("VMHost")] [Parameter(ValueFromPipelineByPropertyName=$true, Position=2, ParameterSetName='ByName')] [String[]]$ComputerName = @(, $env:COMPUTERNAME), [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch]$AsJob, [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch]$Force, [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName='ByVM')] [Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM, [Parameter(ValueFromPipelineByPropertyName=$true)] [UInt32]$Timeout = 0, [Parameter(ValueFromPipelineByPropertyName=$true)] [UInt32]$ForceTimeout = 5, [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch]$Passthru ) BEGIN { ### Globals ### $JobList = @() ### Script blocks ### $RestartScript = { param( [Parameter(Position=1)][Object]$VMObject, # deserialized and cannot be used as a VM object [Parameter(Position=2)][UInt32]$Timeout, [Parameter(Position=3)][UInt32]$ForceTimeout, [Parameter(Position=4)][Switch]$Force, [Parameter(Position=5)][Switch]$Passthru ) ### Constants ### New-Variable -Name VMStateRunning -Value 2 -Option Constant New-Variable -Name VMStateTurnedOff -Value 3 -Option Constant New-Variable -Name MinimumForceTimeout -Value 5 -Option Constant ### Functions ### function TurnOff { param( [Parameter()][Object]$VMObject, # deserialized and cannot be used as a VM object [Parameter()][UInt32]$Timeout, [Parameter()][ValidateSet("Shutdown", "TurnOff")][String]$Mode ) switch($Mode) { "Shutdown" { Stop-VM -ComputerName $VMObject.ComputerName -Name $VMObject.Name -Force } "TurnOff" { Stop-VM -ComputerName $VMObject.ComputerName -Name $VMObject.Name -Force -TurnOff } } $TimeoutCounter = 0 $Continue = $true while($Continue) { Start-Sleep -Seconds 1 $Continue = ((Get-WmiObject -Namespace rootvirtualizationv2 -Class Msvm_ComputerSystem -ComputerName $VMObject.ComputerName -Filter "Name = '$($VMObject.VMId.Guid.ToString().ToUpper())'").EnabledState -ne $VMStateTurnedOff) if($Timeout -and $Continue) { $TimeoutCounter++ if($TimeoutCounter -ge $Timeout) { $false return } } } $true } if($ForceTimeout -lt $MinimumForceTimeout) { $ForceTimeout = $MinimumForceTimeout } $ForceRequired = $true # this will be set off if a graceful shutdown is successful $StopAttempted = $false # output from get-vm isn't always reliable, ask WMI if the VM is on if((Get-WmiObject -Namespace rootvirtualizationv2 -Class Msvm_ComputerSystem -ComputerName $VMObject.ComputerName -Filter "Name = '$($VMObject.VMId.Guid.ToString().ToUpper())'").EnabledState -eq $VMStateRunning) { # The VM is on. Check for the shutdown integration service. $ShutdownIntegrationStatus = Get-VMIntegrationService -Name "Shutdown" -ComputerName $VMObject.ComputerName -VMName $VMObject.VMName if($ShutdownIntegrationStatus.Enabled) { if($ShutdownIntegrationStatus.PrimaryOperationalStatus -eq "Ok") { $StopAttempted = $true if(TurnOff -VMObject $VMObject -Timeout $Timeout -Mode "Shutdown") { $ForceRequired = $false } else { Write-Warning "Graceful shutdown for $($VMObject.Name) was unsuccessful." } } } if($Force -and $ForceRequired) { $StopAttempted = $true Write-Verbose -Message "Attempting to turn off (hard shut down) $($VMObject.VMName) on $($VMObject.ComputerName)" if(-not(TurnOff -VMObject $VMObject -Timeout $ForceTimeout -Mode "TurnOff")) { Write-Warning -Message "Unable to turn off $($VMObject.VMName)" } } if($ForceRequired -and -not $Force) { Write-Warning -Message "VM $($VMObject.VMName) can only be restarted with the -Force switch" } # start the VM if the script tried to turn it off and its status is off if((Get-WmiObject -Namespace rootvirtualizationv2 -Class Msvm_ComputerSystem -ComputerName $VMObject.ComputerName -Filter "Name = '$($VMObject.VMId.Guid.ToString().ToUpper())'").EnabledState -eq $VMStateTurnedOff) { Start-VM -ComputerName $VMObject.ComputerName -Name $VMObject.Name if($Passthru) { # just passing the object back will return a deserialized object. allow the calling thread to retrieve the object [String]::Join(",", ($VMObject.Computername, $VMObject.Name)) } } elseif($StopAttempted) { Write-Error "VM $($VMObject.Name) cannot be started because it was not successfully turned off" } } } } PROCESS { if($Name -eq $null -and $VM -eq $null) { throw("Must select at least one virtual machine to restart.") } if($AsJob -and $Passthru) { throw("AsJob and Passthru cannot be specified together") } if ($PSCmdlet.ParameterSetName -eq 'ByName') { $VM = @() foreach($VMHost in $ComputerName) { foreach($VMName in $Name) # $Name is the input parameter for virtual machine name(s) { try { Write-Verbose -Message "Checking for $VMName on $VMHost" $VM += Get-VM -ComputerName $VMHost -Name $VMName -ErrorAction Stop } catch { Write-Verbose -Message "No VM with the name $VMName is present on host $VMHost" } } } } foreach($VMObject in $VM) { Write-Verbose -Message "Performing restart of $($VMObject.Name) on $($VMObject.ComputerName)" if($PSCmdlet.ShouldProcess("$($VMObject.Name) on $($VMObject.ComputerName)", "Restart")) { $JobList += Start-Job -ScriptBlock $RestartScript -Name "Restart-VM-$($VMObject.ComputerName)-$($VMObject.Name)-$($VMObject.VMId)" -ArgumentList $VMObject, $Timeout, $ForceTimeout, $Force, $Passthru } } } END { if($JobList) { if($AsJob) { $JobList } else { $ResultsArray = @() Wait-Job -Job $JobList | Out-Null foreach($Job in $JobList) { $ResultsArray += Receive-Job -Job $Job } $JobList | Remove-Job if($Passthru) { foreach($Result in $ResultsArray) { try { $VMItem = $Result.Split(",") Get-VM -ComputerName $VMItem[0] -Name $VMItem[1] -ErrorAction Stop } catch { ; # really nothing to do about it } } } else { $ResultsArray } } } } }
More Information
This script does include complete help. However, you’ll discover that you have to use it a little differently than you’re accustomed to. Just calling Get-Help Restart-VM results in the following:
What you need to do is: Get-Help Restart-VM -Category Function . This will then show you the help as you expect. You can use -Full, -Example, etc.
Parameters
- AsJob: Returns to the PowerShell prompt immediately. The jobs for all VMs that are being restarted will be left in the pipeline.
- ComputerName: The host of the virtual machine(s) to be restarted. You can use an array, and each will be scanned for the named VMs. Cannot be used with -VM.
- Confirm: If you use this as -Confirm:$true, it will prompt before restarting any VM.
- Force: If any virtual machine does not have the Shutdown integration service installed or enabled, or if it is not responding, then this parameter will cause that virtual machine to be forcibly turned off.
- ForceTimeout: By default, the script will wait up to 5 seconds after instructing a VM to be forcibly turned off to attempt to turn it back on. ForceTimeout allows you to set a higher maximum timeout, in seconds.
- Name: The name(s) of the virtual machine(s) to be restarted. Cannot be used with -VM.
- Passthru: All restarted VM objects will be left in the pipeline.
- VM: One or more virtual machine object(s) to be restarted. You can pipeline items such as the output from Get-VM. Cannot be used with -ComputerName or -Name.
- Whatif: Shows you which virtual machine(s) the script would attempt to restart.