Table of contents
In the past I’ve written about how to identify old or obsolete virtual machines based on the time stamp of the associated VHD or VHDX file. The PowerShell techniques in that script get the job done and there’s nothing necessarily wrong with it, although it does make an assumption that you can reach the disk file remotely. But, as I was researching a problem, which will be the subject of another article, I discovered a new way to identify when a virtual machine was last turned on. In fact, I also found how to find when the virtual machine was last turned off and when its state changed, say from running to paused. The key is in the virtual machine configuration file.
Virtual machine configurations are stored as XML files. The file name is the same as the virtual machine’s guid, or ID. You can use the Get-VM cmdlet in PowerShell to discover the ID and location.
PS C:> get-vm chi-client02 -ComputerName chi-hvr2 | select id,configurationlocation Id ConfigurationLocation -- --------------------- 6164b819-d828-425c-823a-561d96ec0975 C:VMCHI-Client02
The location is relative to the Hyper-V server, or in my case CHI-HVR2. Beneath that location there should be a folder called Virtual Machines. Here’s how I can use PowerShell remoting to see the files.
PS C:> invoke-command { dir 'c:vmchi-client02virtual machines'} -ComputerName chi-hvr2 Directory: C:vmchi-client02virtual machines Mode LastWriteTime Length Name PSComputerName ---- ------------- ------ ---- -------------- d---- 4/7/2014 11:00 AM 6164B819-D828-425C-823A-561D96EC0975 chi-hvr2 -a--- 4/8/2014 12:17 PM 58408 6164B819-D828-425C-823A-561D96EC0975.XML chi-hvr
Now, I am now so much interested in the time stamp on the xml file as I am as to what is inside it. It turns out that Hyper-V records values for when the virtual machine was last on, last off and when its state last changed. As far as I can tell that information is not surfaced anywhere in the Hyper-V Manager or even through PowerShell. Fortunately, PowerShell and XML play well together. Let me get the remote xml file and save it locally as an XML document variable.
PS C:> $vm = get-vm chi-client02 -ComputerName chi-hvr2 PS C:> [xml]$config = invoke-command { get-content "$($using:vm.configurationlocation)Virtual Machines$($using:vm.id).xml"} -ComputerName $vm.computername PS C:> $config xml configuration --- ------------- version="1.0" encoding="UTF-16" standalone="yes" configuration
Now to find those last settings.
PS C:> $config | select-xml -XPath "//last_powered_on_time|//last_powered_off_time|//last_state_change_time" Node Path Pattern ---- ---- ------- last_powered_off_time InputStream //last_powered_on_time|//last_powere... last_powered_on_time InputStream //last_powered_on_time|//last_powere... last_state_change_time InputStream //last_powered_on_time|//last_powere...
That’s what we’re after. Note that the node names are case-sensitive because this is XML. But what is the value?
PS C:> $config | select-xml -XPath "//last_powered_on_time" | select -ExpandProperty node type #text ---- ----- integer 130413564635668426
The #text property indicates when this virtual machine was last powered on. But I’m betting you have no idea what that value really means. It is the number of ticks since December 31, 1600 11:59PM. No, that is not a typo. But it doesn’t matter because we can still represent that date in PowerShell.
PS C:> [datetime]$d = "12/31/1600 11:59:59 PM"
The reason is so that we can add the number of ticks to $d to get the last power on time.
PS C:> $ticks = ($config | select-xml -XPath "//last_powered_on_time").node.'#text' PS C:> $d.AddTicks($ticks) Monday, April 7, 2014 3:01:02 PM
I enclosed the #text property in single quotes because of the # character. But we’re done, right? It turns out that date is in universal or UTC time. It doesn’t take my time zone into account. But instead of hard-coding an adjustment, I’ll let PowerShell do it dynamically. First I need to know my current offset which I can discover from WMI.
PS C:> Get-CimInstance -ClassName Win32_TimeZone Bias SettingID Caption ---- --------- ------- -300 (UTC-05:00) Eastern Time (US & Canada)
The bias is the number of minutes. So I’ll create a variable for my UTC offset in minutes and add it to the UTC date time value.
PS C:> $UTCOffset= (Get-CimInstance -ClassName Win32_TimeZone -Property Bias).Bias PS C:> $UTC= $d.AddTicks($ticks) PS C:> $UTC.AddMinutes($UTCOffset) Monday, April 7, 2014 10:01:02 AM
Now I know when this virtual machine was last powered on in my local time. I can repeat the process for the other Last* settings in the XML configuration. Naturally you don’t want to have to do these types of calculations manually so I wrote you a PowerShell function.
#requires -version 3.0 #requires -module Hyper-V Function Get-VMLastTime { <# .Synopsis Get special dates for a Hyper-V virtual machine .Description This command will read the XML configuration file for a given Hyper-V virtual machine and get some values that are not normally visible: LastOn the time the virtual machine was last powered on LastOff the time the virtual machine was last powered down LastChange the time the virtual machine's state change, say from running to off. Additionally, the command will show you a timespan for when the virtual machine's state last changed. If the virtual machine is running this will have a value of 0.00:00:00. Virtual machines that have never been powered on will have a value of -1.00:00:00. .Example PS C:> get-vmlasttime chi-client02 -comp chi-hvr2 VMName : CHI-Client02 State : Off LastOn : 4/7/2014 10:01:02 AM LastOff : 4/8/2014 11:17:02 AM LastChange : 4/8/2014 11:17:02 AM LastUse : 150.03:44:56.5874097 Computername : chi-hvr2 .Example PS Scripts:> get-vmlasttime -Computername chi-hvr2 | where state -ne running | Select VMName,Last* | format-table VMName LastOn LastOff LastChange LastUse ------ ------ ------- ---------- ------- Web03 6/18/2014 5:58:24 AM 6/19/2014 7:24:26 AM 6/19/2014 7:24:26 AM 78.07:39:15.2009942 Web02 6/18/2014 5:58:24 AM 6/19/2014 7:24:19 AM 6/19/2014 7:24:19 AM 78.07:39:21.6099380 Web01 12/31/1600 6:59:59 PM 6/17/2014 1:05:38 PM 6/17/2014 1:05:38 PM -1.00:00:00 Dev02 6/19/2014 7:43:18 AM 8/28/2014 4:41:10 PM 8/28/2014 4:41:10 PM 7.22:22:31.3071186 CHI-TEST 9/5/2014 11:43:57 AM 8/22/2014 7:34:24 AM 9/5/2014 1:32:35 PM 01:31:06.2942361 CHI-Client02 4/7/2014 10:01:02 AM 4/8/2014 11:17:02 AM 4/8/2014 11:17:02 AM 150.03:46:39.5460388 .Example PS C:> get-vmlasttime -Computername chi-hvr2 | where {$_.LastUse -ge (New-Timespan -days 90)} VMName : CHI-Client02 State : Off LastOn : 4/7/2014 10:01:02 AM LastOff : 4/8/2014 11:17:02 AM LastChange : 4/8/2014 11:17:02 AM LastUse : 150.03:44:56.5874097 Computername : chi-hvr2 .Notes Last Updated: Sept 5, 2014 Version : 1.0 Learn more: PowerShell in Depth: An Administrator's Guide (http://www.manning.com/jones6/) PowerShell Deep Dives (http://manning.com/hicks/) Learn PowerShell in a Month of Lunches (http://manning.com/jones3/) Learn PowerShell Toolmaking in a Month of Lunches (http://manning.com/jones4/) **************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .Link Get-VM #> [cmdletbinding()] Param( [Parameter(Position=0,HelpMessage="Enter a VM Name", ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateNotNullorEmpty()] [Alias("vmname")] [string]$Name = "*", [ValidateNotNullorEmpty()] [alias("cn")] [string]$Computername = $env:COMPUTERNAME ) Begin { Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" #what is the offset in minutes from UTC? $UTCOffset= (Get-CimInstance -ClassName Win32_TimeZone -Property Bias).Bias #time is calculated from this date [datetime]$d = "12/31/1600 11:59:59 PM" #define a hashtable of parameters to splat to Get-VM $vmParams = @{ ErrorAction="Stop" Name="" } #script block to execute $sb = { Param($config) if (Test-Path -Path $config) { #read the content [xml]$xml = get-content $config $xmlName = $xml.configuration.properties.name.InnerText $off = $xml.configuration.properties.last_powered_off_time.InnerText $on = $xml.configuration.properties.last_powered_on_time.InnerText $changed = $xml.configuration.properties.last_state_change_time.InnerText #create a hashtable of values $hash = @{ LastPowerOn = $on LastPowerOff =$off LastChange = $changed } $hash } else { Write-warning "Could not find $config" } } #close scriptblock #Hashtable of parameters to splat to Invoke-Command $icmParam=@{ ScriptBlock=$sb ArgumentList= @() } #if computername is not the local host add it to the parameter set if ($Computername -AND ($Computername -ne $env:COMPUTERNAME)) { Write-Verbose "Searching on $computername" $vmParams.Add("Computername",$Computername) #create a PSSession for Invoke-Command Try { Write-Verbose "Creating temporary PSSession" $tmpSession = New-PSSession -ComputerName $Computername -ErrorAction Stop $icmParam.Add("Session",$tmpSession) } Catch { Throw "Failed to create temporary PSSession to $computername." } } } #begin Process { if ($name -is [string]) { Write-Verbose -Message "Getting virtual machine(s)" $vmParams.Name=$name Try { write-verbose ($vmparams | out-string) $vms = Get-VM @vmParams } Catch { Write-Warning "Failed to find a VM or VMs with a name like $name" throw $_ #bail out Return } } elseif ($name -is [Microsoft.HyperV.PowerShell.VirtualMachine] ) { #otherwise we'll assume $Name is a virtual machine object Write-Verbose "Found one or more virtual machines matching the name" $vms = $name } else { #invalid object type Write-Error "The input object was invalid." #bail out return } foreach ($vm in $vms) { Write-Verbose "Processing $($vm.name)" <# Can't use Join-Path #> $vmpath = "$($vm.configurationLocation)Virtual Machines" $config = "$vmpath$($vm.VMId).xml" Write-verbose $config #add the path to the config file as an parameter for the scriptblock $icmParam.ArgumentList= @($config) $detail = Invoke-Command @icmParam if ($detail) { write-verbose ($detail | out-string) [datetime]$on = $d.AddTicks($detail.LastPoweron).addMinutes($UTCOffset) [datetime]$off = $d.AddTicks($detail.LastPoweroff).addMinutes($UTCOffset) [datetime]$changed =$d.AddTicks($detail.Lastchange).addMinutes($UTCOffset) [pscustomobject]@{ VMName = $vm.Name State = $vm.State LastOn = $on LastOff = $off LastChange = $changed LastUse = if ($vm.state -eq "Off" -AND $on -le "1/1/1601" ) { New-TimeSpan -Days -1 } elseif ($vm.state -ne "running") { (Get-Date) - $changed } else { New-Timespan } Computername = $vm.ComputerName } } #if detail else { Write-Warning "There was a problem retrieving XML information for $($vm.name)" } Remove-Variable detail } #foreach vm } #process End { #remove temp PSSession if found if ($tmpSession) { Write-Verbose "Removing temporary PSSession" $tmpSession | Remove-PSSession } Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } #end } #end function
My script assumes you have PowerShell remote access to the Hyper-V server. The function writes an object for each virtual machine showing you its current state, last times as well as a last use age property. This property is the age based on when the virtual machine’s state last changed.
PS C:> get-vmlasttime chi-client02 -Computername chi-hvr2 VMName : CHI-Client02 State : Off LastOn : 4/7/2014 10:01:02 AM LastOff : 4/8/2014 11:17:02 AM LastChange : 4/8/2014 11:17:02 AM LastUse : 150.03:44:56.5874097 Computername : chi-hvr2
You can use this function for all sorts of reporting. Although I expect you will want to limit your queries to machines that are not running.
PS C:> get-vm -ComputerName chi-hvr2 | where {$_.state -ne 'running'} | get-vmlasttime -Computername chi-hvr2| out-gridview -title Offline
When you are piping something from Get-VM to Get-VMLastTime be sure to specify the Hyper-V server name. Here’s my result sorted by the LastOn property.
A LastUse value of 0 means the vm is running and a value -1 indicates a virtual machine that has never been powered on. That might be helpful to know.
PS C:> get-vmlasttime -comp chi-hvr2 | where {$_.Lastuse -lt 0} VMName : Web01 State : Off LastOn : 12/31/1600 6:59:59 PM LastOff : 6/17/2014 1:05:38 PM LastChange : 6/17/2014 1:05:38 PM LastUse : -1.00:00:00 Computername : chi-hvr2
Or find virtual machines that haven’t been powered on in a given number of days. You have to remember to filter with a timespan object.
PS C:> get-vmlasttime -comp chi-hvr2 | where {$_.Lastuse -ge (new-timespan -days 90)} VMName : CHI-Client02 State : Off LastOn : 4/7/2014 10:01:02 AM LastOff : 4/8/2014 11:17:02 AM LastChange : 4/8/2014 11:17:02 AM LastUse : 150.03:52:04.2096517 Computername : chi-hvr2
But I urge you to be cautious on how you use this tool. You might have a virtual machine that powered on a year ago and was shut down yesterday for maintenance or something. That’s where the LastUse property I added can come in handy. Otherwise, there’s really no limit to how you can use this information, or how you might want to extend it. You might want to include uptime or when the virtual machine was created.
I hope you find the function useful and even more so I hope you expanded your PowerShell knowledge.