Save to My DOJO
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.
Not a DOJO Member yet?
Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!
14 thoughts on "Get Virtual Machine Last On and Off Time"
I am amazed that Microsoft only provide this method for understanding when a VM was last powered on?
that if you have 500 hyper v servers you have to trawl through the CSV’s of these servers to identify when a file was last accessed
Risible by Microsoft
i am sure with vMware you can run a PS1 script against vCentre
and that is why vMware will always beat MS in virtualization
You still need to query the host server. But here’s a better way that queries the virtual machine information via WMI.
#specify the name of Hyper-V Host
$computername = $env:COMPUTERNAME
$ns = "RootVirtualizationv2"
$vms = get-ciminstance -Namespace $ns -ClassName msvm_computersystem -filter "Caption = 'Virtual Machine'" -ComputerName $computername
foreach ($vm in $vms) {
if ($vm.onTimeinMilliseconds -gt 0) {
$Running = $True
}
else {
$Running = $False
}
[pscustomobject]@{
Computername = $computername.toUpper()
VMName = $vm.elementName
IsRunning = $Running
Created = $vm.InstallDate
Age = (Get-Date) - $vm.InstallDate
LastStateChange = $vm.TimeOfLastStateChange
ChangeAge = (Get-Date) - $vm.TimeOfLastStateChange
}
} #foreach vm
FWIW, VMware doesn’t store this information with their VMs at all. You have to check the host’s event log and hope that it hasn’t rolled off. If the VM might have moved, you have to ask all the hosts that it might have lived on. That technique would work with Hyper-V as well, but Jeff’s method requires a lot less flailing around and isn’t at the mercy of the event log’s capacity.
Hi Jeffery its a good piece of information.
I have a situation like each day how much time the Virtual Machine got shutdown or switched off in a cloud environment I need to find, And according to that usage calculation to be made.
So Get-VMLastTime cmdlets might help me to get the data.
I have question like Get-VMLastTime where I can execute like in Hyepr-V machine or VIrtual machine manager
I executed in Virtual Machine manager and am getting these warning
PS C:Usershrnayak> get-vm | Get-VMLastTime
WARNING: Could not find Virtual Machines{some-name}.xml
I need to revise this for the current version of Hyper-V.
How do you get this last powered off time information from the new Hyper-V configuration file .vmcx that is now in binary format?
I’m not sure. There have been changes to Hyper-V since I wrote the original code. It needs to be updated.
Hi Jeffery.
Congrats by this function… it´s amazing.
I have one question. I am running a HyperV on a Windows2012Rrv R2 server and looks like the backup operation have been changing the values last_powered_off_time.
Is it make sense?
Stumbled upon this page today as I was looking for Last On/Off info. Has anything changed in the last 2 years or is this script still the best way to gather that information?
I think I need to revisit this topic.
Hello, Thanks for the Great Article.
But it’s not working on Hyper-v 2016 :/
Do you know how to get Last On and Off Time information about vm’s on Hyper-v 2016 ??
Hi Kamil,
I don’t think there are any backward compatibility issues which would prevent this from working, assuming you have the correct PowerShell modules installed. Could you please share the error you are seeing?
Thanks,
Symon Perriman
Altaro Editor
It looks a lot has changed in Hyper-V since I wrote this code. Looks like I need to revisit it.