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}
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