Talking about “Host-only KVPs”– or “How do you store random stuff in a VM configuration file” [Hyper-V Script]

Recently I have had a couple of different people approach me with this problem:

They are developing software to manage / work with virtual machines – and they need a way to associate custom data with a virtual machine.  The problem that they face is how to do this in a way that will survive if the virtual machine gets backed up, failed over, live migrated, snapshotted, etc…

Thankfully there is a simple answer.  In Windows Server 2008 R2 we extended the Key-Value Pair (KVP) functionality to include the concept of “host-only” KVPs.  Simply put – these are key value pairs that get stored in the virtual machine configuration file and can be written / read from the parent partition; but never get sent into the virtual machine.  The way this works is that you use AddKvpItems to add a Msvm_KvpExchangeDataItem where the source field has been set to “4” – which indicates that this is a host-only KVP.

Combining this with some of my existing code from my general DVD tool script and my parent KVP script – results in this hand “all purpose host-only KVP script”:

# Function for handling WMI jobs / return values
Function ProcessResult($result, $successString, $failureString)
   #Return success if the return value is "0"
   if ($result.ReturnValue -eq 0)
      {write-host $successString} 
   #If the return value is not "0" or "4096" then the operation failed
   ElseIf ($result.ReturnValue -ne 4096)
      {write-host $failureString "  Error value:" $result.ReturnValue}
      {#Get the job object
      #Provide updates if the jobstate is "3" (starting) or "4" (running)
      while ($job.JobState -eq 3 -or $job.JobState -eq 4)
         {write-host $job.PercentComplete "% complete"
          start-sleep 1
          #Refresh the job object
       #A jobstate of "7" means success
       if ($job.JobState -eq 7)
          {write-host $successString}
          {write-host $failureString
          write-host "ErrorCode:" $job.ErrorCode
          write-host "ErrorDescription" $job.ErrorDescription}
# Filter for parsing XML data
filter Import-CimXml 
   # Create new XML object from input
   $CimXml = [Xml]$_ 
   $CimObj = New-Object -TypeName System.Object 
   # Iterate over the data and pull out just the value name and data for each entry
   foreach ($CimProperty in $CimXml.SelectNodes("/INSTANCE/PROPERTY[@NAME='Name']")) 
         $CimObj | Add-Member -MemberType NoteProperty -Name $CimProperty.NAME -Value $CimProperty.VALUE 
   foreach ($CimProperty in $CimXml.SelectNodes("/INSTANCE/PROPERTY[@NAME='Data']")) 
         $CimObj | Add-Member -MemberType NoteProperty -Name $CimProperty.NAME -Value $CimProperty.VALUE 
   # Display output
# Prompt for the Hyper-V Server to use
$HyperVServer = Read-Host "Specify the Hyper-V Server to use (enter '.' for the local computer)"
# Prompt for the virtual machine to use
$VMName = Read-Host "Specify the name of the virtual machine"
# Get the management service
$VMMS = gwmi Msvm_VirtualSystemManagementService -namespace root\virtualization -computername $HyperVServer
# Get the virtual machine object
$VM = gwmi MSVM_ComputerSystem -namespace root\virtualization -computername $HyperVServer | where {$_.ElementName -eq $VMName} 
# Get the virtual machine setting data
$VSSD = $VM.getRelated("Msvm_VirtualSystemSettingData") | where {$_.SettingType -eq 3}
# Setup parameters for main menu prompt
$message = "What do you want to do with host-only KVPs?"
$list = New-Object System.Management.Automation.Host.ChoiceDescription "&List", "List the current host-only KVPs."
$add = New-Object System.Management.Automation.Host.ChoiceDescription "&Add", "Add a new host-only KVP / update an existing one."
$delete = New-Object System.Management.Automation.Host.ChoiceDescription "&Delete", "Delete a host-only KVP."
$quit = New-Object System.Management.Automation.Host.ChoiceDescription "&Quit", "Exit the HostKVPTools script."
$options = [System.Management.Automation.Host.ChoiceDescription[]]($list, $add, $delete, $quit)
   # Setting up $KVPSettingData needs to be done inside the loop - as this value needs to be
   # refreshed after any add / modify / delete operation
   # Get KVP settings object
   $Query = "Associators of {$VSSD} Where ResultClass=Msvm_KvpExchangeComponentSettingData"
   $KvpSettingData = gwmi -Query $Query -namespace root\virtualization -computername $HyperVServer 
   # Ask the user what they want to do with the host-only KVPs
   $promptResult = $host.ui.PromptForChoice("", $message, $options, 0)
   switch ($promptResult)
         0  {# Display existing host-only KVPs
                $KvpSettingData.HostOnlyItems | Import-CimXml 
         1 {# Add a new host-only KVP / update an existing host-only KVP
                # Prompt for the name for the new KVP
                $NewKVPName = Read-Host "Specify the name of the KVP to add / update"
                # Prompt for the KVP data
                $NewKVPData = Read-Host "Specify the data for the KVP"
                # Create new Msvm_KvpExchangeDataItem object
                $wmiClassString = "\\" + $HyperVServer + "\root\virtualization:Msvm_KvpExchangeDataItem"
                $newKvpExchangeDataItem = ([WMIClass]$wmiClassString).CreateInstance()
                # Populate the KVP data item - a source of "4" indicates that it is "host-only"
                $newKvpExchangeDataItem.Name = $NewKVPName
                $newKvpExchangeDataItem.Data = $NewKVPData
                $newKvpExchangeDataItem.Source = 4
                # Check to see if we can find the key
                $matchingString = '<PROPERTY NAME="Name" TYPE="string"><VALUE>' + $NewKVPName
                $existingKVP = $KvpSettingData.HostOnlyItems | ? {$_ -match $matchingString}
                # If the key exists - modify it.  If not, create it.
                if ($existingKVP) 
                    {$result = $VMMS.ModifyKvpItems($Vm, $newKvpExchangeDataItem.GetText(1))}
                else {$result = $VMMS.AddKvpItems($Vm, $newKvpExchangeDataItem.GetText(1))}
                # Handle the results                
                ProcessResult $result "The host-only KVP has been added." "Failed to add host-only KVP."
         2 {# Delete an existing host-only KVP
                # Prompt for the name for the KVP to delete
                $KVPName = Read-Host "Specify the name of the KVP to delete"
                # Check to see if we can find the key
                $matchingString = '<PROPERTY NAME="Name" TYPE="string"><VALUE>' + $KVPName
                $existingKVP = $KvpSettingData.HostOnlyItems | ? {$_ -match $matchingString}
                # If the key exists - remove it.
                if ($existingKVP) 
                    {$result = $VMMS.RemoveKvpItems($Vm, $existingKVP)
                     ProcessResult $result "The host-only KVP has been deleted." "Failed to delete host-only KVP."}
                else {write-host "No host-only KVP exists with that name"}
until ($promptResult -eq 3)