Restoring Virtual Machine Creation Time

Table of contents

When using PowerShell to build reports about your virtual infrastructure, have you run into situations where you run a command like this:

get-vm -ComputerName chi-hvr2 | sort CreationTime | Select name,creationtime

Only to end up with output like this figure?

What’s up with that creation time? If you run client Hyper-V on Windows 8 or Windows 8.1 you might be more likely to see this type of result although I have seen this behavior on Hyper-V server as well. Of course, if you stage a virtual machine on Windows 8.1 and later move it to a server, you will most likely carry over this problem. Perhaps this doesn’t bother you but data discrepancies like this bother me. So after a bit of research I found a few references to a bug on client Hyper-V where the creation time property is not captured.

The creation time property is stored, or should be stored, in the virtual machine configuration file. This is an XML file which means we can use PowerShell to find the value and if necessary create it. Let me use a virtual machine on my Windows 8.1 client to demonstrate.

PS C:> $VM = get-vm dev01

The configuration file is an XML file that uses the virtual machines ID, which I can pull from the $VM variable.

PS C:> $VM | select configurationlocation,id

ConfigurationLocation                                       Id
---------------------                                       --
D:VMDev01                                                 26abea54-55f8-4501-91d1-769fbe381c4f

Using PowerShell, I can construct the path and read in the contents as an XML document.

PS C:> [xml]$config = get-content "$($vm.ConfigurationLocation)virtual machines$($vm.id).xml"

The creation time is in the properties node.

PS C:> $config.configuration.properties.creation_time

type                                                        #text
----                                                        -----
bytes                                                       aUkVnjOLzwE=

You can also use Select-XML to verify the property exists.

PS C:> $config | select-xml -xpath "//creation_time"

Node                                    Path                                    Pattern
----                                    ----                                    -------
creation_time                           InputStream                             //creation_time

The problem is that for some virtual machines, this node is missing and hence the error.

But first, you may be wondering about the node value. In my example aUkVnj0Lzew= doesn’t look like any date time value to me.

PS C:> $text = 'aUkVnjOLzwE='

That value is a string representation of an array of bytes which represents the number of ticks since December 29, 1600 11:59:59PM. To translate I can convert to the string back into an array of bytes.

PS C:> [byte[]]$barray = [convert]::FromBase64String($text)

Next, I can turn the byte array into a value.

PS C:> $converted

130475968028821865

This is the number of ticks so all I need to do is add them to the December 31, 1600 date.

PS C:> ([datetime]"12/31/1600 23:59:59").AddTicks($converted)

Wednesday, June 18, 2014 8:26:41 PM

So that’s when this virtual machine was created which I can see here:

PS C:> $vm.creationtime

Wednesday, June 18, 2014 4:26:42 PM

Well, almost. The creation time I calculated is in Universal Time. But that is easy to accommodate by adding my offset from UTC.

PS C:> ([datetime]"12/31/1600 23:59:59").AddTicks($converted).addhours(-4)

Wednesday, June 18, 2014 4:26:41 PM

I show you all of this because to correct the missing creation date problem, all you need to do is insert the creation_time node with the proper value and save the file. But don’t panic, I have a function that will do the heavy lifting for you.

#requires -version 3.0
#requires -module Hyper-V

Function Set-VMCreationTime {

<#
.Synopsis
Update the Hyper-V virtual machine CreationTime property
.Description
This command can be used to set a Hyper-V virtual machine's CreationTime property. This value is stored in the XML configuration file and sometimes it is missing. This especially seems to be the case with virtual machines created on Windows 8 or 8.1. This command will use the the creation time of the XML configuration as the virtual machine creation date. You may not see the new creation time until you start a new PowerShell session.

This command supports -WhatIf and -Verbose.

******************
* VERY IMPORTANT *
******************
The Hyper-V Management service will be briefly STOPPED while the configuration file is updated. If you are updating many virtual machines, this will bounce the service repeatedly.

USE AT YOUR OWN RISK AND HAVE ADEQUATE BACKUPS WITH VERIFIED RESTORES.
.Example 
PS C:> Set-VMCreationTime dev02 -Computername chi-hvr2

Name         : Dev02
CreationTime : 6/19/2014 8:41:46 AM
ComputerName : CHI-HVR2

.Example
PS C:> get-vm -comp Server01 | where creationtime -lt "1/1/1601" | set-vmcreationtime -Computername Server01 | format-table

Name                           CreationTime                  ComputerName
----                           ------------                  ------------
Test VM Alpha                  11/26/2013 9:30:22 AM         Server01
Test VM Bravo                  11/26/2013 9:30:23 AM         Server01
Test01                         6/19/2014 2:46:47 PM          Server01
Ubuntu 13 x86                  11/26/2013 8:20:13 AM         Server01
WebTest                        6/18/2014 2:44:46 PM          Server01
WebTest01                      6/19/2014 3:12:34 PM          Server01
Win2012R2-Core-Baseline        11/26/2013 9:33:34 AM         Server01
Win2012R2-Core-Baseline-x64    11/27/2013 10:55:26 AM        Server01
Win8Demo                       6/18/2014 8:58:51 PM          Server01

Update all virtual machines with no creation time.
.Notes
Last Updated: September 8, 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(SupportsShouldProcess)]
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)"  

    #define a hashtable of parameters to splat to Get-VM
    $vmParams = @{
      ErrorAction="Stop"
      Name=""
    }
    #script block to execute
    $sb = {
        [cmdletbinding(SupportsShouldProcess)]
        Param([string]$config,[boolean]$V,[boolean]$W)

        if ($v) {
          $VerbosePreference = "Continue"
        }
        #set the WhatIf preference locally
        $WhatIfPreference = $W

         #time is calculated from this date
        [datetime]$d = "12/31/1600 11:59:59 PM"

        if (test-Path -Path $config) {

            #get the contents of the configuration file as an XML document
            [xml]$xml = Get-Content -Path $config

            #only process if there is no Creation_Time Node
            if (-Not ($xml.configuration.properties.creation_time.'#text')) {
                $Created = (get-item $config).CreationTimeUTC
                Write-Verbose "Updating creation time to $Created"

                [int64]$ticks = ($Created - $d).ticks
                #convert number of ticks into an array of bytes
                [byte[]]$bytes = [BitConverter]::GetBytes($ticks)

                #convert byte array to string
                $binString = [convert]::ToBase64String($bytes)

                #create a new XML node
                $creationtime = $xml.CreateNode("element","creation_time","")
                $creationtime.SetAttribute("type","bytes")
                $Creationtime.InnerText = $binString

                write-verbose ($creationtime | out-string)
                $xml.configuration.properties.AppendChild($creationtime) | Out-null
                #the vmms service must be stopped in order to save the file

                if ($PSCmdlet.ShouldProcess($config)) {          
                    Write-Verbose "Stopping vmms service"
                    Stop-Service -Name vmms

                    Write-Verbose "Saving configuration"
                    $xml.Save($config)

                    Write-Verbose "Starting vmms service"
                    Start-Service -Name vmms

                 #give the vmms service a chance to restart
                 Start-Sleep -Seconds 2

                 $vmname = $xml.configuration.properties.name.InnerText
                 Write-Verbose "Refreshing $vmname"

                 Get-VM -Name $vmname | Select Name,CreationTime,Computername
              } #should process
            } #if no creation_time node
            else {
                Write-Verbose "Creation_Time Node already defined"
            }
        }
        else {
            Write-Warning "Can't find $config"
        }

    } #close scriptblock

     #Hashtable of parameters to splat to Invoke-Command
     $icmParam=@{
       ScriptBlock=$sb
       ArgumentList= @()
       HideComputername = $True
     }

    #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."
        }
    }
    else {
      $icmParam.add("Computername",$Computername)
    }

} #begin

Process {

    if ($name -is [string]) {
        Write-Verbose -Message "Getting virtual machine(s)"
        $vmParams.Name=$name
        Try {
            $vms = Get-VM @vmParams 
        } #Try
        Catch {
            Write-Warning "Failed to find a VM or VMs with a name like $name"
            #bail out
            Return
        } #Catch
    } #if name is a string
    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
    } #elseif VM object
    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
        #pass verbose parameter to scriptblock
        if ($VerbosePreference -eq "Continue") { $v = $True } else { $v = $false }
        $icmParam.ArgumentList= @($config,$v,$WhatIfPreference)

        Invoke-Command @icmParam | Select * -ExcludeProperty RunspaceID

    } #foreach vm

} #process

End {
    #remove temp PSSession if found
    if ($tmpSession) {
        Write-Verbose "Removing temporary PSSession"
        #always remove the session
        $tmpSession | Remove-PSSession -WhatIf:$False
    }
    Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end

} #end function

This function uses PowerShell remoting to edit the XML file because the configuration location is relative to the Hyper-V server. The function uses the creation date and time for the XML configuration file as the new value, if it is missing. Fortunately, in PowerShell file objects have a property that reflects the creation time as a UTC datetime.

PS C:> dir "$($vm.ConfigurationLocation)virtual machines$($vm.id).xml" | select CreationTime* 

CreationTime                                       CreationTimeUtc
------------                                       ---------------
6/18/2014 4:26:41 PM                               6/18/2014 8:26:41 PM

My function takes the UTC value and turns it into the string representation of the byte array of ticks since December 31, 1600. If there is no creation_time node in the XML file, the function creates the node, sets the value and adds it to the XML document. But there is one major caveat with this process. In order to update the XML configuration file, the virtual machine service must be stopped on the Hyper-V host. When the service is running all of the XML configuration files are locked and you can’t save the updated file back to disk. To be absolutely clear, when you use this function, the Hyper-V virtual machine service will be temporarily stopped and restarted. It is only off for the briefest of moments while the updated configuration file is written to disk. But I’m assuming that you will only need to use this function to remediate server-based virtual machines on a rare case by case basis. On client Hyper-V you might need to use it more frequently.

Let me then revisit the original virtual machine and correct it with my function. First, I need to load the function into my PowerShell session by dot-sourcing it.

PS C:> . C:scriptsSet-VMCreationTime.ps1

The function supports –WhatIf so you can see what it would do without making and changes or restarting services.

From the verbose output you can see what date time value will be used and what it will look like in the XML file. Once satisfied, I can run the command without –Whatif.

PS C:> Set-VMCreationTime -vmname dev02 -Computername chi-hvr2

Name         : Dev02
CreationTime : 6/19/2014 8:41:46 AM
ComputerName : CHI-HVR2

Done! The output come from running Get-VM in the remote session on the Hyper-V host so I can verify the change. I do that because there is an issue with Get-VM in PowerShell. If you have run Get-VM at least once in your PowerShell session, some properties, like CreationTime, don’t get refreshed the second time you get the virtual machine.

PS C:> get-vm dev02 -ComputerName chi-hvr2 | select name,creationtime

Name                                                        CreationTime
----                                                        ------------
Dev02                                                       12/31/1600 7:00:00 PM

But if I start new PowerShell session I will see the new creation time. Here’s a quick way to test.

PS C:> powershell -noprofile {get-vm dev02 -ComputerName chi-hvr2 | select name,creationtime}

Name                                                        CreationTime
----                                                        ------------
Dev02                                                       6/19/2014 8:41:46 AM

So if you’ve been annoyed by invalid virtual machine creation times, now you have an answer. There are other examples in the comment-based help. You can run the command as often as you want because it will only make a change if the Creation_Time node is missing under Properties. As with all things PowerShell, please make sure you first test this in a non-production environment. I hope you’ll let me know how this works out for you.

 

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!

11 thoughts on "Restoring Virtual Machine Creation Time"

  • Jon Matcho says:

    Excellent work, thank you.

    I *think* that my checkpoints, which also showed Created: 12/31/1600…, actually became “valid” data and so this script did not work on those VMs. In other words, there IS a Creation_Time node for those machines BUT it contains 12/31/1600. Instead of punting if the node exists, the script should examine the value. I don’t know PowerShell well enough to update line #84 for you…

    Worked great everywhere else. Thanks!

  • jose Cassana says:

    Thanks for this detail explanation of this annoying issue . I had this on W2K12 server and i fix it, using alternative editing directly the xml configuration files. All vm in error that the date were 1600 was because the missing creationtime.
    Why is this parameter missing ? maybe sometime MS will explain it.

  • ARe says:

    Hello, Really nice script, but it doesn’t work with a Cluster. Please for help to fix that strange bug.

  • Andyt says:

    There is only one disadvantage of that script. It doesn’t work for Windows 10 Hyper-V Host. I have one Hyper-V Guest with configuration level 7. It seems to me there are no more xml file for configuration.

  • Ram says:

    Will it be possible to upload the ps script anywhere? I tried copying and saving it as ps1. The script does not run. May be something wrong with copying process.

    • You should be able to copy and paste the code snippets into a ps1 file. You need to make sure you have an execution policy that permits running scripts. You also have to dot source the script file to load the function into your session.

      PS> . .ps1
      PS> Set-VMCreationTime -vmname dev02 -Computername chi-hvr2

  • Jon Matcho says:

    Excellent work, thank you.

    I *think* that my checkpoints, which also showed Created: 12/31/1600…, actually became “valid” data and so this script did not work on those VMs. In other words, there IS a Creation_Time node for those machines BUT it contains 12/31/1600. Instead of punting if the node exists, the script should examine the value. I don’t know PowerShell well enough to update line #84 for you…

    Worked great everywhere else. Thanks!

  • Jean-Sebastien Carle says:

    This script works great on a non clustered Hyper-V server… however, once Failover Clustering comes into play, the XML files are not longer available (or so it seems).

    • Zoran says:

      Hi Jean-Sebastien,
      To use a script on a failover cluster, you can store the script on a file share which is accessible by every node. The file share itself does not need to be clustered, although that would also provide highly-available access to that script.

      Thanks,
      Symon Perriman
      Altaro Editor

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.

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.