Get-Virtual Machine Last Use – Revisited

Table of contents

Recently I posted an article on using PowerShell to find when a virtual machine was last used. If you missed the original article, you might want to check it out as the concepts haven’t really changed. When I first wrote my PowerShell function, I made an assumption that it had to run locally. And for the most part that is still true. In order to get the actual VHD or VHDX file I need to be on the server because the path is relative to the server. Although sometimes you can run commands remotely from your desktop and get some information.

PS C:> get-vm chi-dc01 -ComputerName hv01 | select id | get-vhd -ComputerName hv01

ComputerName : hv01
Path : D:VHDGlobomantics-DC_F1517EF1-534A-45F3-8228-969A87E391A7.avhd
VhdFormat : VHD
VhdType : Differencing
FileSize : 4704739328
Size : 16106127360
MinimumSize : 16105078784
LogicalSectorSize : 512
PhysicalSectorSize : 512
BlockSize : 2097152
ParentPath : D:VHDGlobomantics-DC.vhd
FragmentationPercentage :
Alignment : 1
Attached : False
DiskNumber :
IsDeleted : False
Number :

But this doesn’t show me time stamps on the file. To get that I still need Get-Item which has to run locally, that is, on the server. But since I want to do as much management as I can remotely I revisited my original function and came up with a version that allows you to specify a server name.

#requires -version 3.0
#requires -modules Hyper-V

Function Get-VMLastUse {

<#
.Synopsis
Find a virtual machine last use date.
.Description
This command will write a custom object to the pipeline which should indicate
when the virtual machine was last used. The command finds all hard drives that
are associated with a Hyper-V virtual machine and selects the first one. The
assumption is that if the virtual machine is running the hard drive file will
be changed and the first hard drive listed will most likely be the system drive. 
The function retrieves the last write time property from the first VHD or VHDX 
file to determine how long it has been since the file was last used. If the 
virtual machine is currently running the last use time will be 0:00:00.

You can pipe a collection of Hyper-V virtual machines or specify a virtual
machine name. Wildcards are supported. The default is to display last use data
for all virtual machines. 

You can run this on a Hyper-V server or from any domain member that has the 
Hyper-V management tools installed, such as a Windows 8 computer. The command
uses PowerShell remoting to retrieve the disk information.
.Parameter Name
The name of a Hyper-V virtual machine or a VM object. You can pipe Get-VM
to this command.
.Parameter Computername
The name of the server to query. The default is the local host. If you pipe
a Get-VM command that queries a remote computer, the computer name will 
automatically be used.
.Example
PS C:> Get-vmlastuse xp*

VMName             CreationTime               LastUse                    LastUseAge                
------             ------------               -------                    ----------                
XP Lab             3/3/2013 1:05:29 PM        7/14/2013 9:07:19 AM       33.00:57:04.8442216  

Get last use information for any virtual machine starting with XP.
.Example
PS C>> get-vmlastuse ubuntu* -computer HV01

VMName             CreationTime               LastUse                    LastUseAge                
------             ------------               -------                    ----------                
Ubuntu 12 x86      3/3/2013 3:31:55 PM        6/25/2013 8:26:00 AM       52.01:47:42.9022213 

Get the Ubuntu VM from server HV01.
.Example
PS C:> get-vm -computer HV01 | where {$_.state -eq 'off'} | get-vmlastuse

VMName             CreationTime               LastUse                    LastUseAge                
------             ------------               -------                    ----------                
10961A-LON-CL1     3/15/2013 6:08:54 AM       8/13/2013 5:06:02 PM       2.17:02:13.8564362        
10961A-LON-DC1     3/15/2013 6:08:09 AM       8/13/2013 3:22:44 PM       2.18:45:32.0323689        
10961A-LON-SVR1    3/15/2013 6:09:32 AM       8/13/2013 3:21:36 PM       2.18:46:40.0599579        
CHI-APP01          6/5/2013 12:49:28 PM       8/16/2013 8:48:54 AM       01:19:21.9799246
CHI-Client02       3/3/2013 3:31:42 PM        8/3/2013 7:48:14 PM        12.14:20:02.4111888       
CHI-DEV01          5/29/2013 4:18:21 PM       8/16/2013 9:32:30 AM       00:35:46.8916567     
... 

Get last use information for any virtual machine that is currently off on a remote
Hyper-V server.
.Example
PS C:> get-vmlastuse -computer HV01 | Sort LastUseAge | Out-Gridview -title "Last Use Report"

Get last use information for all virtual machines on server HV01, sorted by age.
All results will be displayed with Out-Gridview.
.Notes
version 2.0
Brought to you by Altaro http://altaro.com/hyper-v

New to PowerShell? Try Learn PowerShell 3 in a Month of Lunches

****************************************************************
* 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.             *
****************************************************************

.Inputs
String or Hyper-V Virtual Machine
.Outputs
Custom object
.Link
Get-VM
Get-Item
#>

[cmdletbinding()]
Param (
[Parameter(Position=0,
HelpMessage="Enter a Hyper-V virtual machine name",
ValueFromPipeline,ValueFromPipelinebyPropertyName)]
[ValidateNotNullorEmpty()]
[alias("vm")]
[object]$Name="*",
[Parameter(ValueFromPipelinebyPropertyname)]
[alias("cn")]
[string]$Computername
)

Begin {
    Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"  

    #define a hashtable of parameters to splat to Get-VM
    $vmParams = @{
      ErrorAction="Stop"
    }
    #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
        }
        Catch {
            Throw "Failed to create temporary PSSession to $computername."
        }
    }
} #begin

Process {
    if ($name -is [string]) {
        Write-Verbose -Message "Getting virtual machine(s)"
        $vmParams.Add("Name",$name)
        Try {
            $vms = Get-VM @vmParams 
        }
        Catch {
            Write-Warning "Failed to find a VM or VMs with a name like $name"
            #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) {

      #if VM is on a remote machine using PowerShell remoting to get the information
      Write-Verbose "Processing $($vm.name)"
      $sb = {
      param([string]$Path,[string]$vmname)
      Try {
          $diskfile = Get-Item -Path $Path -ErrorAction Stop
          $diskFile | Select-Object @{Name="LastUse";Expression={$diskFile.LastWriteTime}},
          @{Name="LastUseAge";Expression={(Get-Date) - $diskFile.LastWriteTime}} 
      }
      Catch {
        Write-Warning "$($vmname): Could not find $path."
       }
      } #end scriptblock

      #get first drive file
      $diskpath= $vm.HardDrives[0].Path

      #only proceed if a hard drive path was found
      if ($diskpath) {
      $icmParam=@{
       ScriptBlock=$sb
       ArgumentList= @($diskpath,$vm.name)
      }
       Write-Verbose "Getting details for $(($icmParam.ArgumentList)[0])"
      if ($vmParams.computername) {
        $icmParam.Add("Session",$tmpSession)
      }

      $details = Invoke-Command @icmParam 
      #write a custom object to the pipeline
      $objHash=[ordered]@{
        VMName=$vm.name
        CreationTime=$vm.CreationTime
        LastUse=$details.LastUse
        LastUseAge=$details.LastUseAge
      }

      #if VM is running set the LastUseAge to 0:00:00
      if ($vm.state -eq 'running') {
         $objHash.LastUseAge= New-TimeSpan -hours 0
      }

      #write the object to the pipeline
      New-Object -TypeName PSObject -Property $objHash

      } #if $diskpath
      Else {
            Write-Warning "$($vm.name): No hard drives defined."
      } 
     }#foreach
} #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

This function uses PowerShell remoting and Invoke-Command to retrieve the necessary file information. If you specify a remote computername, the function will create a temporary PSSession and use it with Invoke-Command. It will be removed at the end of the command. More than likely your remote server is enabled for PowerShell remoting so this should just work. I also added some better error handling for virtual machines with missing hard disks or those that had none attached.

My theory, and I haven’t found anything better, is that the first disk will be the system disk and will thus reflect an updated timestamp when the virtual machine is started. As I was testing I noticed that virtual machines that had been on for awhile, actually looked like they had not been used in a while which is misleading. In my function I decided that if the virtual machine is running to set the Last Use Age to 0. I think this better reflects the age of my virtual machines. Once the function is loaded into my PowerShell session I can run a command like this.

PS C:> get-vmlastuse –computername HV01 | Out-Gridview -Title "VM Aging"

Which produces a result like this.

I can click on column headings to sort or apply additional filters. When I ran the command I saw warnings about missing disk files. The virtual machines in this output, like “XP Lab” have a hard disk file defined but something has happened to the file since the virtual machine was created. Certainly something for me to investigate.

You can still run this version of my function on the server and it will work just fine. But if you have the Hyper-V module installed locally, you can run it and specify the name of your Hyper-V server. For me, remote management is the name of the game and PowerShell makes it a fun game to play.

 

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

5 thoughts on "Get-Virtual Machine Last Use – Revisited"

  • Tony Liang says:

    Hello Jeffery,

    Thank you sososososo much for posting this blog , It’s brilliant !
    But when I use this , I get a problem. I run this script on the server.
    Some of the “Creation Time” showing ” 1/1/1601 8:00:00 AM” , How could this happen ?

    Thanks again

  • tshk says:

    I have a Clustered Hyper-V Scenario with 4 members, when i run the script:

    PS C:Tools> .Get-VMLastUse -Computername desacluster03

    I get nothing! Any ideas?

    • You need to dot source the ps1 file

      . c:toolsget-vmlastuse.ps1
      There is a space between the period and the path to the file

      Then you can run the function

      Get-VMLastUse -compurtername foo

Leave a comment or ask a question

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

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

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.