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}
 
   Else
      {#Get the job object
      $job=[WMI]$result.job
 
      #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
          $job=[WMI]$result.job}
 
       #A jobstate of "7" means success
       if ($job.JobState -eq 7)
          {write-host $successString}
       Else
          {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
   $CimObj 
} 
 
# 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)
 
do 
   {
   
   # 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
   write-host
   $promptResult = $host.ui.PromptForChoice("", $message, $options, 0)
   write-host 
 
   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)

Cheers,
Ben