Hyper-v Snapshots part 2.

In my last post I explained how snapshots work and gave a little bit of PowerShell for creating a one . In the post before that I talked about creating a generic  choose-tree function. What I wanted was to be able to call Choose-tree List_Of_Items First_Item PathPropertyName, ParentPropertyName, PropertytoDisplay and get a tree view I can choose from: like this

     $folders=dir -recurse | where-object {$_ -is [system.io.directoryInfo]}
    choose-tree $folders (get-Item (get-location) ) "PSPath" "PsParentPath" "Name"

    0    +windowsPowershell  
    1    | |--Nivot  
    2    | |--Pics
    Which one ?: 2

The key thing in this is that PowerShell lets us use variables/parameters to hold field names. The logic is pretty simple. Take an array of items, and tell the function which one to start at, Output that item, if it has any Children call the function recursively for each of them. Checking for children is where this ability to pass property names is important, because I can use  where-object {$_.$parent -eq $startat.$path.ToString()} to say "where the field I said holds the parent, matches the field I said holds the path" I put a "ToString" on the end because I found it doesn't like being passed "path.path" and that's needed for some WMI items; toString() returns the path in this case but it's safe for strings too. When the function calls itself it specifies how many levels deep in the tree the current item is - each level of recursion adds 1 to $indent in the function.

I wrote an "Out tree" before doing "choose-tree", most of the code below is associated with making choices When processing the topmost item it sets up a counter to allow the user to choose the items, and because the output order might not be the same as the input order it also sets up an array to hold ordered items. Once all the child items have been processed it prompts the user for a selection  and returns the item at that position in the array. 

The only thing that I've done here that is out of the ordinary for me is I don't like using the -f operator on strings because it makes for unreadable code. "{0, -4}" -f $counter. Says "Put argument at position 0 into this string , right justified to 4 characters", which is just what I need, and I've put in the rest of the output line into the same construction.

    Function Choose-Tree
   {Param ($items, $startAt, $path=("Path"), $parent=("Parent"), 
    $label=("Label"), $indent=0, [Switch]$multiple)
    if ($Indent -eq 0)  {$Global:treeCounter = -1 ;  $Global:treeList=@() } 
    $Global:treeCounter++ 
    $Global:treeList=$global:treeList + @($startAt) 
    $children = $items | where-object {$_.$parent -eq $startat.$path.ToString()} 
    if   ($children -ne $null) 
         { $leader = "| " * ($indent)
           "{0,-4} {1}+{2} "-f  $Global:treeCounter, $leader , $startAt.$label | Out-Host
           $children | sort-object $label |
                       ForEach-Object {Choose-Tree -Items $items -StartAt $_ -Path $path `   
                      -parent $parent -label $label -indent ($indent+1)} 
     }
    else { $leader = "| " * ($indent-1)         
           "{0,-4} {1}|--{2} "-f  $global:Treecounter, $leader , $startAt.$Label  | out-Host }
    if ($Indent -eq 0) {if ($multiple) { $Global:treeList[ [int[]](Read-Host "Which one(s) ?").Split(",")] }
                        else           {($Global:treeList[ (Read-Host "Which one ?")]) }  
                      } 
   }

And so to snapshots.  

The parent partition and every child MsVM_ComputerSystem WMI object which represents it. The "Name" field in this object is actually a GUID. There is a second object MsVM_VirtualSystemSettingData: each VM, and each of its snapshots has one of these objects. The Settings data object for VM itself has it's GUID in both the InstanceID and systemName fields, but the Snapshots have their own instanceID with the VMs GUID in the system name field. As with most of my functions I things up so I can pass the MsVM_ComputerSystem object or pass a string and use Get-VM  to convert it. Then I it's one Get-WMObject operation to get the Snapshots.

    Function Get-VMSnapshot
   {Param( $VM=$(Throw "You must specify a VM") )

    if ($VM -is [String]) {$VM=(Get-VM -machineName $VM) }

    Get-WmiObject -NameSpace root\virtualization -Query "Select * From MsVM_VirtualSystemSettingData 
                          Where systemName='$($VM.name)' and instanceID <> 'Microsoft:$($VM.name)' " 
   } 

Time to combine choose-tree and get-snapshot to choose my snapshots from a tree. As I said above , the Name field is actually a GUID, and the display name for the Snapshot is in the elementName . So I display the tree of choices,starting with the "Root" snapshot. [Note I'm aware that I don't cope with the situation where you delete a root snapshot with two children and get two roots]

    Function Choose-VMSnapshot
   {Param ($VM=$(Throw "You must specify a VM"))
    $snapshots=(Get-VMSnapshot $VM )
    Choose-Tree -items $snapshots -startAt ($snapshots | where{$_.parent -eq $null}) `
        -path "Path" -Parent "Parent" -label "elementname"
   }

Now I can choose my snapshots, it's easy to tell a function like Remove-snapshot or apply-snapshot what I want. Here's Remove-Snapshot , which I can call with something like

Remove-snapshot -snapshot (choose-Snapshot Core). Pretty simple stuff, I use the variable pointing to to the virtual System Management Service, as I did when creating a new snapshot this time I just need to invoke the RemoveVirtualSystemSnapshot method. As with the new snapshot it should return 4096 for "started processing in the background" , and I return the Job ID.

    Function Remove-VMSnapshot 
   {Param( $snapshot=$(Throw "You must specify a snapshot") ) 
    $arguments=($snapshot,$Null) 
    $result=$VSMgtSvc.psbase.InvokeMethod("RemoveVirtualSystemSnapshot",$arguments) 
    if ($result -eq 4096){ $arguments[1] } 
    else                  {"Error, code:" + $result} 
   }

Finally I might want to apply a snapshot , and this needs us to specify the VM and snapshot. I've written this so that if the Snapshot is omitted the user is prompted to select it. It's the same process again, except this time we use the ApplyVirtualSystemSnapshot Method

    Function Apply-VMSnapshot
   {Param( $VM=$(Throw "You must specify a VM"), $SnapShot=(choose-VMsnapshot $VM))
    if ($VM -is [String]) {$VM=(Get-VM -machineName $VM) }
    $arguments=@($VM,$snapshot,$null)
    $result=$VSMgtSvc.psbase.InvokeMethod("ApplyVirtualSystemSnapshot", $arguments)   
    if ($result -eq 0) {"Success"}
    elseif ($result -eq 4096) {"Job Started" | out-host
                               $arguments[2]}
    else {"failed"}
   } 

[Update. There were a couple of bits of PowerShell 2.0 in the above. In 1.0 you can't call the .InvokeMethod  method of a WMI object directly, you have to call it via .psbase]

If you're wondering what to with the Virtual Hard disks I showed I'll get round to that soon.

Technorati Tags: Microsoft,Windows Server 2008,Hyper-v,Virtualization,Powershell