Hyper-V Key-Value Pair Data Exchange Part 2: Implementation

In part one of this series, we talked about Hyper-V’s Key-Value Pair (KVP) data exchange feature and got an idea for how it works. As I mentioned in that piece, one of the reasons you don’t see much about it is because it can be difficult to use.

In this post, I’m going to provide you with some PowerShell modules that are intended to alleviate most of the pain. Once you’ve implemented these modules on your systems, you’ll be able to pass data back and forth from guests to hosts. What that data is, and what happens when it is received by the target system, is for your imagination to concoct. If PowerShell scripting isn’t your strong suit, I first suggest that you become acquainted. The primer in our Hyper-V and PowerShell series can set you on your way. You should also search the Internet for scripts designed by others that can accelerate implementation of your automation solutions.

Skip to part 3: Linux

Credits

If I ever figured out how to do everything you see in this article, it would have taken me a lot longer to get there without the writings of Taylor Brown and Boe Prox.

Taylor Brown’s source is mentioned in the comments for the relevant functions, I have lifted his “ProcessWMIJob” function nearly verbatim. The changes that I made were more to make it script-friendly as opposed to being meant for an interactive session. You can see the original ProcessWMIJob function on this blog article. The original script for the Add, Modify, and Remove KVP functions from WMI in a Hyper-V host is found in this blog article. That script was later copied to this TechNet article. To some degree, it might be said that my article refines that script into something more approachable and universally usable. If you’re looking for coding samples for using KVP exchange in Linux guests, that TechNet article is where you want to go.

Boe Prox’s work is not included directly in any of my modules, but is referred to heavily in the supporting sections.

The Modules

I’ll start by presenting the modules so that those of you with PowerShell experience can copy/paste them and be on your way. Explanations for each will follow.

The Host Module

This module is intended to be used on Hyper-V hosts. It exposes the following functions:

  • Get-VMWmiObject: Given the name of a virtual machine, returns the WMI object that represents it.
  • Get-VMKvpGuestToHost: Lists one or more of the KVPs transmitted from the guest to the host.
  • Get-VMKvpGuestToHostInstrinsic: Lists one or more of the KVPs transmitted from the guest to the host that are automatically generated by the Hyper-V Data Exchange service.
  • Get-VMKvpHostToGuest: Lists one or more of the KVPs transmitted from the host to the guest.
  • Send-VMKvpHostToGuest: Creates or modifies a KVP to be sent to a guest from the host. In case you jumped here without reading part one, be aware that the KVP is case-sensitive. If you use a key name that differs from an existing key by only capitalization, a duplicate KVP will be created but the guest will only see the most recent one. This is due to a flaw in the design of KVP Data Exchange and there’s nothing I can do about it short of forcing you to use all-uppercase or all-lowercase key names.
  • Remove-VMKvpHostToGuest: Deletes a host-to-guest KVP. Remember that these cannot be deleted from within the guest. This cmdlet has the same case-sensitivity caveat note as Send-VMKvpHostToGuest. Remove-KvpHostToGuest will almost always report success, even when it didn’t find a matching KVP to remove. I considered performing some pre-flight checking, but opted against it. You may want this cmdlet to work even when the virtual machine is powered off, in which case Get-VMKvpHostToGuest will not operate. I encourage you to use Get-VMKvpHostToGuest for verification.

Each of these has detailed Get-Help text.

Installation Instructions

  1. Navigate to C:WindowsSystem32WindowsPowerShellv1.0Modules.
  2. Create a folder named Hyper-V-KVP-Host.
  3. Inside that folder, create two empty text files: Hyper-V-KVP-Host.psd1 and Hyper-V-KVP-Host.psm1.
  4. Paste the contents of the following code blocks into the respective files.
Hyper-V-KVP-Host.psd1
#
# Module manifest for module 'Hyper-V-KVP-Host'
#
# Generated by: Eric Siron
#
# Generated on: 1/15/2016
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'Hyper-V-KVP-Host'

# Version number of this module.
ModuleVersion = '1.0'

# ID used to uniquely identify this module
GUID = 'f162eccd-b0a0-498b-99ac-3cb0d711de65'

# Author of this module
Author = 'Eric Siron'

# Company or vendor of this module
CompanyName = 'Altaro Software'

# Copyright statement for this module
Copyright = '(c) 2016 Altaro Software. All rights reserved.'

# Description of the functionality provided by this module
Description = 'Facilitates KVP data exchange between a Hyper-V host and its guests.'

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '4.0'

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module
# CLRVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @('Hyper-V')

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module
FunctionsToExport = @('Get-VMWmiObject', 'Get-VMKvpGuestToHost', 'Get-VMKvpGuestToHostIntrinsic', 'Get-VMKvpHostToGuest', 'Send-VMKvpHostToGuest', 'Remove-VMKvpHostToGuest')

# Cmdlets to export from this module
#CmdletsToExport = '*'

# Variables to export from this module
#VariablesToExport = '*'

# Aliases to export from this module
#AliasesToExport = '*'

# DSC resources to export from this module
# DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        # Tags = @()

        # A URL to the license for this module.
        # LicenseUri = ''

        # A URL to the main website for this project.
        # ProjectUri = ''

        # A URL to an icon representing this module.
        # IconUri = ''

        # ReleaseNotes of this module
        # ReleaseNotes = ''

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}

Hyper-V-KVP-Host.psm1

<#
	Hyper-V-KVP-Host Module

	Place on Hyper-V hosts to send and receive KVP data to and from guests.
	Written by Eric Siron
	(c) 2016 Altaro Software
#>

$KVPHostVirtualizationNamespace = 'rootvirtualizationv2'

function New-VMNameComputerPair
{
	<#
	.SYNOPSIS
		Accepts a single virtual machine name and computer name and combines them into an object.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	param(
		[Parameter(Mandatory=$true)][String]$VMName,
		[Parameter(Mandatory=$true)][String]$ComputerName
	)
	Write-Verbose -Message 'Creating custom VMNameComputerPair object...'
	$VMNameComputerPair = New-Object -TypeName PSObject
	Add-Member -InputObject $VMNameComputerPair -MemberType NoteProperty -Name VMName -Value $VMName
	Add-Member -InputObject $VMNameComputerPair -MemberType NoteProperty -Name ComputerName -Value $ComputerName
	$VMNameComputerPair
}

function Get-VMNameComputerPairsFromStrings
{
	<#
	.SYNOPSIS
		Accepts an array of virtual machine names and a computer name and combines them into a merged object array.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	param(
		[Parameter(Mandatory=$true)][String[]]$VMNames,
		[Parameter(Mandatory=$true)][String]$ComputerName
	)
	foreach($VMName in $VMNames)
	{
		Write-Verbose -Message ('Requesting custom VMNameComputerPair object for virtual machine {0} on {1}...' -f $VMName, $ComputerName)
		New-VMNameComputerPair -VMName $VMName -ComputerName $ComputerName
	}
}

function Get-VMNameComputerPairsFromVMs
{
	<#
	.SYNOPSIS
		Accepts an array of virtual macines and coverts them into an array of virtual machine names and associated computer names.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	param(
		[Parameter(Mandatory=$true)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM
	)

	$VMSet = @()
	foreach ($VMObject in $VM)
	{
		Write-Verbose -Message ('Requesting custom VMNameComputerPair object for virtual machine {0} on {1}...' -f $VMObject.Name, $VMObject.ComputerName)
		$VMSet += New-VMNameComputerPair -VMName $VMObject.Name -ComputerName $VMObject.ComputerName
	}
	$VMSet
}


function Get-VMWmiObject
{
	<#
	.SYNOPSIS
		Retrieves a WMI object that represents a virtual machine.
	.DESCRIPTION
		Given a virtual machine object or name, returns the relevant WMI object.
	.PARAMETER VMName
		The name of a virtual machine. Cannot be used with VM.
	.PARAMETER VM
		A virtual machine object. Cannot be used with VMName or ComputerName
	.PARAMETER ComputerName
		The host that owns the virtual machine. If not specified, the local host will be assumed.
	.OUTPUTS
		System.Management.ManagementObject
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String]$VMName,
		[Parameter(Mandatory=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine]$VM,
		[Parameter(ParameterSetName='ByName', Position=2)][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMName = $VM.Name
			$ComputerName = $VM.Name
		}
	}

	process
	{
		Write-Verbose -Message ('Requesting WMI object that represents VM {0} on {1}' -f $VMName, $ComputerName)
		$VMWMIObject = Get-WmiObject -ComputerName $ComputerName -Namespace $KVPHostVirtualizationNamespace -Class Msvm_ComputerSystem -Filter "ElementName = '$VMName'"
		if(-not($VMWMIObject))
		{
			Write-Error -Message ('VM {0} not found on computer {1}' -f $VMName, $ComputerName)
		}
		else
		{
			$VMWMIObject
		}
	}
}

function Get-VMMS
{
	<#
	.SYNOPSIS
		Retrieves the WMI interface for the Hyper-V Virtual Machine Management Service (VMMS). Not exported.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	param
	(
		[Parameter()][String]$ComputerName='.'
	)
	process
	{
		Write-Verbose -Message ('Requesting access to Hyper-V Virtual Machine Management Service on {0} via WMI' -f $ComputerName)
		Get-WmiObject -ComputerName $ComputerName -Namespace $KVPHostVirtualizationNamespace -Class Msvm_VirtualSystemManagementService
	}
}

function Process-WMIJob
{
	<#
	.SYNOPSIS
		The KVP functions of VMMS return a WMI job. This function processes the jobs and determines the result.
		Not exported.
	.NOTES
		Most of this function was written by Taylor Brown.
		Source: http://blogs.msdn.com/b/taylorb/archive/2008/06/18/hyper-v-wmi-rich-error-messages-for-non-zero-returnvalue-no-more-32773-32768-32700.aspx
		Modifications were made for parameter/variable naming clarity and to streamline for use in this KVP project.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	param
	(
		[Parameter(ValueFromPipeline=$true)][System.Management.ManagementBaseObject]$WmiResponse,
		[Parameter()][String]$WmiClassPath = $null,
		[Parameter()][String]$MethodName = $null,
		[Parameter()][String]$VMName,
		[Parameter()][String]$ComputerName
	)
	
	process
	{
		$ErrorCode = 0

		if($WmiResponse.ReturnValue -eq 4096)
		{
			$Job = [WMI]$WmiResponse.Job

			while ($Job.JobState -eq 4)
			{
				
				Write-Progress -Activity ('Modifying KVPs on VM {0} on {1}' -f $VMName, $ComputerName) -Status ('{0}% Complete' -f $Job.PercentComplete) -PercentComplete $Job.PercentComplete
				Start-Sleep -Milliseconds 100
				$Job.PSBase.Get()
			}

			if($Job.JobState -ne 7)
			{
				if ($Job.ErrorDescription -ne "")
				{
					throw $Job.ErrorDescription
				}
				else
				{
					$ErrorCode = $Job.ErrorCode
				}
				Write-Progress $Job.Caption "Completed" -Completed $true
			}
		}
		elseif ($WmiResponse.ReturnValue -ne 0)
		{
			$ErrorCode = $WmiResponse.ReturnValue
		}

		if($ErrorCode -ne 0)
		{
			if($WmiClassPath -and $MethodName)
			{
				$PSWmiClass = [WmiClass]$WmiClassPath
				$PSWmiClass.PSBase.Options.UseAmendedQualifiers = $true
				$MethodQualifiers = $PSWmiClass.PSBase.Methods[$MethodName].Qualifiers
				$IndexOfError = [System.Array]::IndexOf($MethodQualifiers["ValueMap"].Value, [String]$ErrorCode)
				if($IndexOfError -ne "-1")
				{
					'Error Code: {0}, Method: {1}, Error: {2}' -f $ErrorCode, $MethodName, $MethodQualifiers["Values"].Value[$IndexOfError]
				}
				else
				{
					'Error Code: {0}, Method: {1}, Error: Message Not Found' -f $ErrorCode, $MethodName
				}
			}
		}
	}
}


function Get-Kvp
{
	<#
	.SYNOPSIS
		Gets host-to-guest or guest-to-host KVP.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true)][System.Management.ManagementObject]$VMWMIObject,
		[Parameter()][String[]]$Key,
		[Parameter()][String][ValidateSet('HostToGuest', 'GuestToHost', 'GuestToHostIntrinsic')]$KVPSet
		)

	begin
	{
		$MSVM_CS_ENABLED_STATE_RUNNING = 2
	}

	process
	{
		$VMWMIObject.Get()
		if ($VMWMIObject.EnabledState -eq $MSVM_CS_ENABLED_STATE_RUNNING)
		{
			$Keys = @()
			if($Key.Count)
			{
				foreach($KeyName in $Key)
				{
					$Keys += $KeyName.ToLower()
				}
			}

			$KVPs = @()

			Write-Verbose -Message ('Retrieving root KVP element from {0} on {1}...' -f $VMWMIObject.ElementName, $VMWMIObject.PSComputerName)
			$KVPRootObject = $VMWmiObject.GetRelated('Msvm_KvpExchangeComponent')
			switch($KVPSet)
			{
				'HostToGuest' {
					Write-Verbose -Message ('Retrieving host-to-guest KVPs from {0} on {1}...' -f $VMWMIObject.ElementName, $VMWMIObject.PSComputerName)
					$XMLKVPItems = $KVPRootObject.GetRelated('Msvm_KvpExchangeComponentSettingData').HostExchangeItems
				}
				'GuestToHost' {
					Write-Verbose -Message ('Retrieving guest-to-host KVPs from {0} on {1}...' -f $VMWMIObject.ElementName, $VMWMIObject.PSComputerName)
					$XMLKVPItems = $KVPRootObject.GuestExchangeItems
				}
				'GuestToHostIntrinsic' {
					Write-Verbose -Message ('Retrieving intrinsic guest-to-host KVPs from {0} on {1}...' -f $VMWMIObject.ElementName, $VMWMIObject.PSComputerName)
					$XMLKVPItems = $KVPRootObject.GuestIntrinsicExchangeItems
				}
			}

			foreach ($XMLKVPItem in $XMLKVPItems)
			{
				Write-Verbose -Message ('Extracting key...')
				$KVPKey = ([XML]$XMLKVPItem).SelectSingleNode("/INSTANCE/PROPERTY[@NAME='Name']/VALUE/child::text()").Value
				if($Key.Count -and -not ($Keys.Contains(($KVPKey.ToLower()).Trim())))
				{
					Write-Verbose -Message ('This key ({0} is not in the explicit search set, skipping.' -f $KVPKey)
					continue
				}
				Write-Verbose -Message ('Retrieving value for {0}...' -f $KVPKey)
				$KVPValue = ([XML]$XMLKVPItem).SelectSingleNode("/INSTANCE/PROPERTY[@NAME='Data']/VALUE/child::text()").Value
				Write-Verbose -Message ('Building output object...')
				$KVPItem = New-Object -TypeName PSObject
				Add-Member -InputObject $KVPItem -MemberType NoteProperty -Name 'VMName' -Value $VMWMIObject.ElementName
				Add-Member -InputObject $KVPItem -MemberType NoteProperty -Name 'Key' -Value $KVPKey
				Add-Member -InputObject $KVPItem -MemberType NoteProperty -Name 'Value' -Value $KVPValue
				$KVPs += $KVPItem
			}
			$KVPs
		}
		else
		{
			Write-Warning -Message ('VM {0} on host {1} is not running. KVPs cannot be retrieved.' -f $VMWMIObject.ElementName, $VMWMIObject.PSComputerName)
		}
	}
}

function Get-VMKvpGuestToHost
{
	<#
	.SYNOPSIS
		Retrieves one or more guest-to-host KVPs from a specific virtual machine.
	.DESCRIPTION
		Retrieves one or more guest-to-host KVPs from a specific virtual machine.
	.PARAMETER VMName
		The name of the virtual machine whose KVPs are to be retrieved. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object whose KVPs are to be retrieved. Cannot be used with VMName or ComputerName.
	.PARAMETER ComputerName
		The name of the Hyper-V host that owns the virtual machine. If not specified, the local host will be used.
	.PARAMETER Key
		One or more case-insensitive key names.
		If this parameter is specified, only KVPs with matching key names will be returned.
		If this parameter is not specified, all KVPs will be returned.
	.OUTPUTS
		Zero or more custom objects with properties "VMName", "Key", and "Value"
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String[]]$VMName,
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
		[Parameter(ParameterSetName='ByName', Position=2)][Parameter(ParameterSetName='ByVM')][String[]]$Key,
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$VMWMIObject = Get-VMWmiObject -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName
		
			if($VMWMIObject)
			{
				Get-Kvp -VMWMIObject $VMWmiObject -Key $Key -KVPSet GuestToHost
			}
		}
	}
}

function Get-VMKvpGuestToHostIntrinsic
{
	<#
	.SYNOPSIS
		Retrieves one or more of the intrinsic guest-to-host KVPs from a specific virtual machine.
	.DESCRIPTION
		Retrieves one or more of the intrinsic guest-to-host KVPs from a specific virtual machine.
		These values are automatically generated by the Data Exchange service within the guest and contain information about the guest operating system.
	.PARAMETER VMName
		The name of the virtual machine whose KVPs are to be retrieved. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object whose KVPs are to be retrieved. Cannot be used with VMName or ComputerName.
	.PARAMETER ComputerName
		The name of the Hyper-V host that owns the virtual machine. If not specified, the local host will be used.
	.PARAMETER Key
		One or more case-insensitive key names.
		If this parameter is specified, only KVPs with matching key names will be returned.
		If this parameter is not specified, all KVPs will be returned.
	.OUTPUTS
		Zero or more custom objects with properties "VMName", "Key", and "Value"
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String]$VMName,
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine]$VM,
		[Parameter(ParameterSetName='ByName', Position=2)][Parameter(ParameterSetName='ByVM')][String[]]$Key,
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$VMWMIObject = Get-VMWmiObject -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName

			if($VMWMIObject)
			{
				Get-Kvp -VMWMIObject $VMWmiObject -Key $Key -KVPSet GuestToHostIntrinsic
			}
		}
	}
}

function Get-VMKvpHostToGuest
{
	<#
	.SYNOPSIS
		Retrieves one or more host-to-guest KVPs from a specific virtual machine.
	.DESCRIPTION
		Retrieves one or more host-to-guest KVPs from a specific virtual machine.
	.PARAMETER VMName
		The name of the virtual machine whose KVPs are to be retrieved. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object whose KVPs are to be retrieved. Cannot be used with VMName or ComputerName.
	.PARAMETER ComputerName
		The name of the Hyper-V host that owns the virtual machine. If not specified, the local host will be used.
	.PARAMETER Key
		One or more case-insensitive key names.
		If this parameter is specified, only KVPs with matching key names will be returned.
		If this parameter is not specified, all KVPs will be returned.
	.OUTPUTS
		Zero or more custom objects with properties "VMName, "Key", and "Value"
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String[]]$VMName,
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
		[Parameter(ParameterSetName='ByName', Position=2)][Parameter(ParameterSetName='ByVM')][String[]]$Key,
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$VMWMIObject = Get-VMWmiObject -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName

			if($VMWMIObject)
			{
				Get-Kvp -VMWMIObject $VMWmiObject -Key $Key -KVPSet HostToGuest
			}
		}
	}
}

function Send-VMKvpHostToGuest
{
	<#
	.SYNOPSIS
		Sends a KVP to a virtual machine.
	.DESCRIPTION
		Sends a KVP from the specified host to the specified guest. If the key does not exist, it will be created.
	.PARAMETER VMName
		The name of the virtual machine where the KVP is to be created or modified. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object where the KVP is to be created or modified. Cannot be used with VMName or ComputerName.
	.PARAMETER ComputerName
		The name of the Hyper-V host that owns the virtual machine. If not specified, the local host will be used.
	.PARAMETER Key
		Name of the key to be used. If it does not exist, it will be created.
	.PARAMETER Value
		Value to be stored in the KVP data sent to the virtual machine. If not specified, the value will be empty.
	.OUTPUTS
		NONE
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String[]]$VMName,
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=2)][Parameter(Mandatory=$true, ParameterSetName='ByVM')][String[]]$Key,
		[Parameter(Position=3)][String]$Value='',
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$VMWMIObject = Get-VMWmiObject -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName
			$VMMS = Get-VMMS -ComputerName $VMWMIObject.PSComputerName
			$KVPDataItem = ([wmiclass]"\$($VMMS.ClassPath.Server)$($VMMS.ClassPath.NamespacePath):Msvm_KvpExchangeDataItem").CreateInstance()
			$KVPDataItem.Name = $Key
			$KVPDataItem.Data = $Value
			$KVPDataItem.Source = 0

			$KVPTransmitJob = $VMMS.ModifyKvpItems($VMWmiObject, $KVPDataItem.GetText(1))
			try
			{
				$WMIJobResult = Process-WMIJob -WmiResponse $KVPTransmitJob -WmiClassPath $VMMS.ClassPath -MethodName 'ModifyKvpItems' -VMName $VMWMIObject.ElementName -ComputerName $VMWMIObject.PSComputerName
			}
			catch
			{
				$KVPTransmitJob = $VMMS.AddKvpItems($VMWmiObject, $KVPDataItem.GetText(1))
				$WMIJobResult = Process-WMIJob -WmiResponse $KVPTransmitJob -WmiClassPath $VMMS.ClassPath -MethodName 'AddKvpItems' -VMName $VMWMIObject.ElementName -ComputerName $VMWMIObject.PSComputerName
			}
			if($WMIJobResult) # return will be empty if everything is OK
			{
				Write-Error -Message ('Setting key {0} to {1} from VM {2} on {3} failed: {4}' -f $Key, $Value, $VMWMIObject.ElementName, $VMWMIObject.PSComputerName, $WMIJobResult)
			}
		}
	}
}

function Remove-VMKvpHostToGuest
{
	<#
	.SYNOPSIS
		Removes a host-to-guest KVP from a virtual machine.
	.DESCRIPTION
		Removes a host-to-guest KVP from a virtual machine.
	.PARAMETER VMName
		The name of the virtual machine whose KVP is to be deleted. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object whose KVP is to be deleted. Cannot be used with VMName or ComputerName.
	.PARAMETER ComputerName
		The name of the Hyper-V host that owns the virtual machine. If not specified, the local host will be used.
	.PARAMETER Key
		Name of the key to be used. If it does not exist, it will be created.
	.PARAMETER Value
		Value to be stored in the KVP data sent to the virtual machine. If not specified, the value will be empty.
	.OUTPUTS
		NONE
	.NOTES
		If the specified KVP does not exist, that alone will not cause the function to return $FALSE.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String[]]$VMName,
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='ByVM', Position=1)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=2)][Parameter(Mandatory=$true, ParameterSetName='ByVM')][String[]]$Key,
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$VMWMIObject = Get-VMWmiObject -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName
			if($VMWMIObject)
			{
				$VMMS = Get-VMMS -ComputerName $VMWMIObject.PSComputerName

				$KVPDataItem = ([wmiclass]"\$($VMMS.ClassPath.Server)$($VMMS.ClassPath.NamespacePath):Msvm_KvpExchangeDataItem").CreateInstance()
				$KVPDataItem.Name = $Key
				$KVPDataItem.Data = [String]::Empty
				$KVPDataItem.Source = 0

				$KVPRemoveJob = $VMMS.RemoveKvpItems($VMWMIObject, $KVPDataItem.GetText(1))
				$WMIJobResult = Process-WMIJob -WmiResponse $KVPRemoveJob -WmiClassPath $VMMS.ClassPath -MethodName 'RemoveKvpItems' -VMName $VMWMIObject.ElementName -ComputerName $VMWMIObject.PSComputerName

				if($WMIJobResult) # return will be empty if everything is OK
				{
					Write-Error -Message ('Removal of {0} from {1} on {2} failed: {3}' -f $Key, $VMWMIObject.ElementName, $VMWMIObject.PSComputerName, $WMIJobResult)
				}
			}
		}
	}
}

 

The Guest Module

This module is intended to be used on Hyper-V guests. It exposes the following functions:

  • Get-VMKVPHostToGuest: Lists one or more of the KVPs that have been transmitted to the guest from the host.
  • Get-VMKVPGuestToHost: Lists one or more of the KVPs that the guest has transmitted to the host.
  • Send-VMKVPGuestToHost: Creates or modifies the specified KVP in the appropriate registry location, causing it to be transmitted to the host.
  • Remove-VMKVPGuestToHost: Removes a guest-to-host KVP.

As with the host module, the guest module’s cmdlets have detailed help viewable in Get-Help.

Installation Instructions

  1. Navigate to C:WindowsSystem32WindowsPowerShellv1.0Modules.
  2. Create a folder named Hyper-V-KVP-Guest.
  3. Inside that folder, create two empty text files: Hyper-V-KVP-Guest.psd1 and Hyper-V-KVP-Guest.psm1.
  4. Paste the contents of the following code blocks into the respective files.

Hyper-V-KVP-Guest.psd1

#
# Module manifest for module 'Hyper-V-KVP-Guest'
#
# Generated by: Eric Siron
#
# Generated on: 1/15/2016
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'Hyper-V-KVP-Guest'

# Version number of this module.
ModuleVersion = '1.0'

# ID used to uniquely identify this module
GUID = 'f66ce9a3-9b2e-4879-b5f7-0ad10ef318a4'

# Author of this module
Author = 'Eric Siron'

# Company or vendor of this module
CompanyName = 'Altaro Software'

# Copyright statement for this module
Copyright = '(c) 2016 Altaro Software. All rights reserved.'

# Description of the functionality provided by this module
# Description = ''

# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module
# CLRVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module
FunctionsToExport = @('Get-VMKVPHostToGuest', 'Get-VMKVPGuestToHost', 'Send-VMKVPGuestToHost', 'Remove-VMKVPGuestToHost')

# Cmdlets to export from this module
#CmdletsToExport = '*'

# Variables to export from this module
#VariablesToExport = '*'

# Aliases to export from this module
#AliasesToExport = '*'

# DSC resources to export from this module
# DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        # Tags = @()

        # A URL to the license for this module.
        # LicenseUri = ''

        # A URL to the main website for this project.
        # ProjectUri = ''

        # A URL to an icon representing this module.
        # IconUri = ''

        # ReleaseNotes of this module
        # ReleaseNotes = ''

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}

Hyper-V-KVP-Guest.psd1

<#
	Hyper-V-KVP-Guest Module

	Place on Hyper-V guests to send and receive KVP data to and from the host.
	Written by Eric Siron
	(c) 2016 Altaro Software
#>

$HVKVPBaseName = 'SOFTWARE\Microsoft\Virtual Machine\'
$HVKVPInboundExchangeKeyName = $HVKVPBaseName + 'External'
$HVKVPOutboundExchangeKeyName = $HVKVPBaseName + 'Guest'

function Get-RegistryHKLM
{
	<#
	.SYNOPSIS
		Opens the root HKEY_LOCAL_MACHINE_KEY registry key.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	.NOTES
		The dependent functions included in the original module know if they need to check for administrator credentials.
		If this function is extracted, permission checks may fail.
	#>
	#requires -Version 4

	process
	{
		if([System.Environment]::Is64BitOperatingSystem)
		{
			Write-Verbose -Message 'Opening 64-bit HKEY_LOCAL_MACHINE...'
			[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64)
		}
		else
		{
			Write-Verbose -Message 'Opening HKEY_LOCAL_MACHINE...'
			[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry32)
		}
	}
}

function Get-RegistryHKLMKVP
{
	<#
	.SYNOPSIS
		Returns key-value pairs from specified registry key path. Relies on Get-RegistryHKLM and only works on items below that root.
		Not exported; utility function that exists because PowerShell 4 has no capabilities that enable method inheritance.
	.NOTES
		The dependent functions included in the original module know if they need to check for administrator credentials.
		If this function is extracted, permission checks may fail.
	#>
	#requires -Version 4

	param
	(
		[Parameter(Mandatory=$true)][String]$KeyPath,
		[Parameter()][String[]]$Key = @()
	)
	
	process
	{
		$KVPs = @()

		$RootKey = Get-RegistryHKLM
		
		Write-Verbose -Message ('Acquiring read-only access to registry key {0}{1}...' -f $RootKey, $KeyPath)
		$SubKey = $RootKey.OpenSubKey($KeyPath, $false)
		foreach ($KeyName in $SubKey.GetValueNames())
		{
			if($Key.Count -and -not($Key.Contains($KeyName)))
			{
				Write-Verbose -Message ('Key {0} is not among the explicitly defined search items. Skipping.' -f $KeyName)
				continue
			}
			Write-Verbose -Message 'Creating custom KVP output object.'
			$KVP = New-Object -TypeName PSObject
			Add-Member -InputObject $KVP -MemberType NoteProperty -Name 'Key' -Value $KeyName
			Add-Member -InputObject $KVP -MemberType NoteProperty -Name 'Value' -Value $SubKey.GetValue($KeyName)
			$KVPs += $KVP
		}
		Write-Verbose -Message ('Releasing read-only access to registry key {0}{1}...' -f $RootKey, $KeyPath)
		$SubKey.Close()
		$KVPs
	}
}


function Get-VMKVPHostToGuest
{
	<#
	.SYNOPSIS
		Gets key-value pairs of data that the Hyper-V host has transmitted to this virtual machine.
	.DESCRIPTION
		Gets key-value pairs of data that the Hyper-V host has transmitted to this virtual machine.
	.PARAMETER Key
		If included, only key-value pairs whose key names match will be returned.
	.OUTPUTS
		Zero or more custom objects with properties "Key" and "Value".
	#>
	#requires -Version 4

	param
	(
		[Parameter()][String[]]$Key = @()
	)
	Get-RegistryHKLMKVP -KeyPath $HVKVPInboundExchangeKeyName -Key $Key
}

function Get-VMKVPGuestToHost
{
	<#
	.SYNOPSIS
		Gets key-value pairs of data that this guest has made available to its host.
	.DESCRIPTION
		Gets key-value pairs of data that this guest has made available to its host.
	.PARAMETER Key
		If included, only key-value pairs whose key names match will be returned.
	.OUTPUTS
		Zero or more custom objects with properties "Key" and "Value".
	#>
	#requires -Version 4

	param
	(
		[Parameter()][String[]]$Key = @()
	)
	Get-RegistryHKLMKVP -KeyPath $HVKVPOutboundExchangeKeyName -Key $Key
}

function Send-VMKVPGuestToHost
{
	<#
	.SYNOPSIS
		Creates or updates a key-value pair of data to be made available to this virtual machine's host.
	.DESCRIPTION
		Creates or updates a key-value pair of data to be made available to this virtual machine's host.
	.PARAMETER Key
		The name of the key in the key-value pair to be set or created.
	.PARAMETER Value
		The value of the key-value pair to be set or created. If this parameter is not specified, the value will be empty.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator

	param
	(
		[Parameter(Mandatory=$true)][String]$Key,
		[Parameter()][String]$Value = ''
	)

	$RootKey = Get-RegistryHKLM
	Write-Verbose -Message ('Acquiring read/write access to registry key {0}{1}...' -f $RootKey, $HVKVPOutboundExchangeKeyName)
	$OutboundKey = $RootKey.OpenSubKey($HVKVPOutboundExchangeKeyName, $true)
	$OutboundKey.SetValue($Key, $Value)
	Write-Verbose -Message ('Releasing read/write access to registry key {0}{1}...' -f $RootKey, $HVKVPOutboundExchangeKeyName)
	$OutboundKey.Close()
}

function Remove-VMKVPGuestToHost
{
	<#
	.SYNOPSIS
		Removes a host-to-guest key-value pair.
	.DESCRIPTION
		Removes a host-to-guest key-value pair. The related registry key will be removed and the host will no longer be able to query for the key or its value.
	.PARAMETER Key
		The name of the key-value pair to be removed.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator

	param
	(
		[Parameter(Mandatory=$true)][String]$Key
	)

	$RootKey = Get-RegistryHKLM
	Write-Verbose -Message ('Acquiring read/write access to registry key {0}{1}...' -f $RootKey, $HVKVPOutboundExchangeKeyName)
	[Microsoft.Win32.RegistryKey]$OutboundKey = $RootKey.OpenSubKey($HVKVPOutboundExchangeKeyName, $true)
	$OutboundKey.DeleteValue($Key)
	Write-Verbose -Message ('Releasing read/write access to registry key {0}{1}...' -f $RootKey, $HVKVPOutboundExchangeKeyName)
	$OutboundKey.Close()
}

Module Discussion

These modules have been marked to only operate under PowerShell 4.0, although I admit that’s mostly because I didn’t want to add in script to check for administrator privileges. I don’t think there’s anything in them that won’t operate under PS 3.0 aside from all of the #requires -RunAsAdministrator lines, so you should be able to downgrade these modules fairly easily if necessary. I’d prefer that you bring your hosts and guests up to at least PowerShell 4.0, if they aren’t there already. To check the installed version, run this at a PowerShell prompt: $PSVersionTable. If your version is below 4.0, go to the Microsoft downloads page and search for “Windows Management Framework”. Version 5 is the latest, but as of this writing it is so badly broken that it has been removed from the downloads page. I have heard of a handful of compatibility issues for PowerShell-dependent applications when upgrading to newer PowerShell releases, so you might want to look into that for any such apps that you have.

The host module is also designed to work with version 2 of the Hyper-V WMI namespace. It will be immediately compatible with Hyper-V Server 2012 and later and all versions of Client Hyper-V. I think the module will work for 2008 R2, but you’ll need to modify the $KVPVirtualizationNamespace = ‘rootvirtualizationv2’ line of Hyper-V-KVP-Host.psm1 to read $KVPVirtualizationNamespace = ‘rootvirtualization’. I highlighted the line in the source script pasted above, it’s very near the top.

The Host Module

For the most part, I genericized Taylor Brown’s script so that it could be run on demand with any guest and KVP. I also performed a fair bit of refactoring to transition it from short and clever script to readable and maintainable script. For the programming/scripting purists, I apologize for leaving in the magic numbers for the job states. I couldn’t find a definitive explanation of the JobState enumerator that lined up with the numbers that Brown used in his ProcessWMIJob and I didn’t want to guess, so I just left them as I found them.

The functions that are exported from the module should be very simple to figure out. I think tab completion exposure of the parameters should be sufficient to understand what to do, but they are Get-Help friendly just in case. The functions that are not exported by the module might be of interest to those scripters that would like to work with WMI and VMMS, as they do the real work; the exported functions are merely wrappers.

The Guest Module

The guest module contains work that is substantially more original. Truly, working with KVP on the guest side is just a matter of manipulating registry keys. I considered leaving that to you readers to sort out, but PowerShell’s functionality for working with the registry is a special kind of awful. I trust that you’ll find this module to be much more friendly.

As with the host module, I don’t think there’s anything in it that truly requires PowerShell version 4.0 since it is almost exclusively .Net calls, but you do need to be a local administrator to write to the KVP registry sections and I didn’t want to include a checking function. You could remove the #requires and everything should be OK as long as you run the script with administrative privileges.

Exploiting the Modules

Both modules work fine as-is.

KVP Basic Demo

KVP Basic Demo

I’m guessing that you want a little more, such as automation. OK.

Simple Automation

Open up a PowerShell prompt or the ISE inside a guest that has the Hyper-V-KVP-Guest module, and paste this into it:

$KVPReceivedActionScriptBlock = {
    $OutputPath = "$($env:SystemDrive)Temp"
    $OutputFile = $OutputPath + 'kvp-incoming.log'

    if (-not (Test-Path -Path $OutputPath -PathType Container))
    {
        New-Item -Path $OutputPath -ItemType Directory
    }

    if (-not (Test-Path -Path $OutputFile -PathType Leaf))
    {
        New-Item -Path $OutputFile -ItemType File
    }

    $KVPItems = Get-VMKVPHostToGuest
    foreach ($KVPItem in $KVPItems)
    {
        $ReceivedText = '{0}: Key: "{1}", Value: "{2}"' -f (Get-Date), $KVPItem.Key, $KVPItem.Value
    }
    Add-Content -Path $OutputFile -Value $ReceivedText 
}

$RegistryWatchQuery = 'SELECT * FROM RegistryKeyChangeEvent WHERE Hive = "HKEY_LOCAL_MACHINE"  AND KeyPath="SOFTWARE\Microsoft\Virtual Machine\External"'

Register-WmiEvent -Query $RegistryWatchQuery -Action $KVPReceivedActionScriptBlock -SourceIdentifier 'KVPWatcher'

If you’re using the ISE, press F5 to execute it. Do not close the ISE or the PS session. Switch to the host, which must have the Hyper-V-KVP-Host module. Inside the ISE or a PS session, there, enter something like the following:

Send-VMKvpHostToGuest -VMName svmanage -Key 'BlogDemo' -Value 'Testing data to file'

Inside the guest, your system drive should now have a “Temp” folder if it didn’t before, and inside that temp folder should be a file named “kvp-incoming.log”, and that file should be a pure text file that contains something like:

1/18/2016 8:05:17 PM: Key: "BlogDemo", Value: "Testing data to file"

The bad news is, that only works for as long as the PowerShell session is left open. I’m assuming you’d like something a bit more permanent. OK.

Creating a Permanent Guest-Side KVP Watcher

The following has been adapted from Boe Prox’s article on permanent WMI registrations. Something that I need to make clear up front is that this will only cause an action when a registry entry changes or is created anew. If you attempt to test by repeatedly transmitting the same KVP without modifying the value, the sending host will report success while the receiving guest reports nothing.

Inside the guest, run the following inside ISE or at an interactive PowerShell prompt. It only has to be done once, but it’s something you’ll probably want to save to use on other VMs:

$KVPRegistryEventFilter = ([WmiClass]"\.rootsubscription:__EventFilter").CreateInstance()
$KVPRegistryEventFilter.Name = 'KVPRegistryFilter'
$KVPRegistryEventFilter.QueryLanguage = 'WQL'
$KVPRegistryEventFilter.Query = 'SELECT * FROM RegistryKeyChangeEvent WHERE Hive = "HKEY_LOCAL_MACHINE"  AND KeyPath="SOFTWARE\Microsoft\Virtual Machine\External"'
$KVPRegistryEventFilter.EventNamespace = 'rootcimv2'
$KVPRegistryEventFilter.Put()

There will be output indicating that the WMI event instance was created, which should look something like the following:

Path          : \.rootsubscription:__EventFilter.Name="KVPRegistryFilter"
RelativePath  : __EventFilter.Name="KVPRegistryFilter"
Server        : .
NamespacePath : rootsubscription
ClassName     : __EventFilter
IsClass       : False
IsInstance    : True
IsSingleton   : False

You can verify its existence at any time (helpful if you forget whether or not you created it on a given VM):

Get-WMIObject -Namespace rootSubscription -Class __EventFilter

What you have done is tell Windows to trigger an event whenever that particular registry branch changes. Now what you need to do is subscribe to that event. In .Net, that’s called an event handler. In WMI, its an event consumer.

$ScriptPath = 'C:ScriptsKVPResponse.ps1'

$KVPRegistryEventConsumer = ([WmiClass]"\.rootsubscription:CommandLineEventConsumer").CreateInstance()
$KVPRegistryEventConsumer.Name = 'KVPRegistryConsumer'
$KVPRegistryEventConsumer.ExecutablePath = 'C:WindowsSystem32WindowsPowerShellv1.0powershell.exe'
$KVPRegistryEventConsumer.CommandLineTemplate = '-NonInteractive -File {0}' -f $ScriptPath
$KVPRegistryEventConsumer.Put()

You will get some output to the screen to verify that the consumer was created. It will look similar to the output when you created the event. You can verify the existence of the consumer at any time:

Get-WMIObject -Namespace rootSubscription -Class __EventConsumer

You’ll need to supply the item designated in $ScriptPath. You can snip out the action part from the previous section for testing purposes, if you’d like. That’s recreated here for your convenience:

$OutputPath = "$($env:SystemDrive)Temp"
$OutputFile = $OutputPath + 'kvp-incoming.log'

if (-not (Test-Path -Path $OutputPath -PathType Container))
{
	New-Item -Path $OutputPath -ItemType Directory
}

if (-not (Test-Path -Path $OutputFile -PathType Leaf))
{
	New-Item -Path $OutputFile -ItemType File
}

$KVPItems = Get-VMKVPHostToGuest
foreach ($KVPItem in $KVPItems)
{
	$ReceivedText = '{0}: Key: "{1}", Value: "{2}"' -f (Get-Date), $KVPItem.Key, $KVPItem.Value
}
Add-Content -Path $OutputFile -Value $ReceivedText

At this point, you have an event, and you have an event consumer. The final step is to connect the consumer to the event in an act called binding.

$KVPEventBinding = ([WmiClass]"\.rootsubscription:__FilterToConsumerBinding").CreateInstance()
$KVPEventBinding.Filter = $KVPRegistryEventFilter
$KVPEventBinding.Consumer = $KVPRegistryEventConsumer
$KVPEventBinding.Put()

As before, you’ll get some output telling you that the object has been created. You can look at your bindings any time:

Get-WMIObject -Namespace rootSubscription -Class __FilterToConsumerBinding

I’m sure you’ll want to try it out. You’ll find that everything works immediately. No service restarts or system reboots necessary.

Verifying that the Guest Heard You

Remember how I said that the event only fires when the registry changes, so you can’t just send the same data repeatedly? Well, I can help out with that, too. I’ve crafted a script that I use to ensure that each send is unique without me having to do anything fancy. You can drop this right into the Hyper-V-KVP-Host module if you like, which will make it always available. Don’t forget to add “Send-VMKVPCommandToGuest” into the ExportedFunctions line of the Hyper-V-KVP-Host.psd1 file (I highlighted that line for you above)!

function Send-VMKvpCommandToGuest
{
	<#
	.SYNOPSIS
		Creates or modifies a host-to-guest KVP with a timestamp to ensure it is unique.
	.DESCRIPTION
		Creates or modifies a host-to-guest KVP with a timestamp to ensure it is unique.
		The current host time is converted into ticks and the resulting number is prepended to the supplied value prior to transmission.
	.PARAMETER VMName
		The name of the virtual machine the KVP is to be created or modified on. Cannot be used with VM.
	.PARAMETER ComputerName
		The name of the virtual machine's current owning host. Cannot be used with VM.
	.PARAMETER VM
		The virtual machine object on which the the KVP is to be created or modified. Cannot be used with VMName or ComputerName.
	.PARAMETER Command
		The command to be sent to the virtual machine. This will be combined into the value portion of the KVP.
	.PARAMETER Arguments
		One or more strings to be supplied as arguments along with the command. These will be combined into the value portion of the KVP.
	.PARAMETER Verify
		If specified, expects the guest to reflect the command in the guest-to-host key. Returns nothing if successful, an error otherwise.
	.OUTPUTS
		If successful, no output.
		On failure, errors are written to the error stream. If -Verify is specified and the verification process fails, the error will specify as such.
	#>
	#requires -Version 4
	#requires -RunAsAdministrator
	#requires -Modules Hyper-V

	[CmdletBinding(DefaultParameterSetName='ByName')]
	param
	(
		[Parameter(Mandatory=$true, ParameterSetName='ByName', Position=1)][String[]]$VMName,
		[Parameter(ParameterSetName='ByVM', Position=2)][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
		[Parameter(Mandatory=$true, Position=3)][String]$Command,
		[Parameter(Position=4)][String[]]$Arguments = @(),
		[Parameter()][Switch]$Verify,
		[Parameter(ParameterSetName='ByName')][String]$ComputerName='.'
	)

	begin
	{
		$HostToGuestCommandKey = 'VMCommand'
		$GuestAckKey = 'VMAcknowledgement'
		$CommandDelayInMilliseconds = 1000

		if($PSCmdlet.ParameterSetName -eq 'ByName')
		{
			$VMSet = Get-VMNameComputerPairsFromStrings -VMNames $VMName -ComputerName $ComputerName
		}
	}
	 
	process
	{
		if($PSCmdlet.ParameterSetName -eq 'ByVM')
		{
			$VMSet = Get-VMNameComputerPairsFromVMs -VM $VM
		}

		if ($Arguments.Count -gt 0)
		{
			$CommandString = $Command + ';' + [String]::Join(";", $Arguments)
		}
		else
		{
			$CommandString = $Command
		}

		foreach($VMNameComputerPair in $VMSet)
		{
			$Error.Clear()
			$CommandTicks = (Get-Date).Ticks.ToString()

			$ThisVMCommandString = $CommandTicks + ',' + $CommandString

			Send-VMKvpHostToGuest -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName -Key $HostToGuestCommandKey -Value $ThisVMCommandString

			if(-not $Error.Count)
			{
				if($Verify)
				{
					Start-Sleep -Milliseconds $CommandDelayInMilliseconds
					if ($ThisVMCommandString -ne (Get-VMKvpGuestToHost -VMName $VMNameComputerPair.VMName -ComputerName $VMNameComputerPair.ComputerName -Key $GuestAckKey).Value)
					{
						Write-Error -Message ('Command string "{0}" was successfully sent to {1} on {2}, but verify failed.' -f $CommandString, $VMNameComputerPair.VMName, $VMNameComputerPair.ComputerName)
					}
				}
			}
		}
	}
}

As-is, this will modify or create a key named “VMCommand”. The value will be the host’s timestamp for the command was issued, in ticks, followed by whatever you supplied as the -Command parameter, optionally followed by the item(s) that you supplied for the -Arguments parameter. For example, entering the following in the host:

Send-VMKVPCommandToGuest -VMName svmanage -Command 'MakeSandwich' -Arguments 'bacon', 'lettuce', 'tomato'

Should produce something like the following in the guest:

PS C:> Get-VMKVPHostToGuest -Key VMCommand

Key                                                         Value
---                                                         -----
VMCommand                                                   635887545584514902,MakeSandwich;bacon;lettuce;tomato

Because the host’s clock is constantly changing and it would be ridiculously difficult to submit two commands so closely together that they have the same timestamp, this all but guarantees that you’ll trigger the event in the guest every time you send anything… even if it’s technically the exact same KVP with an identical value.

You might have noticed that there is a -Verify parameter. I included this because the KVP operation in the host can only tell you if the KVP was modified on the host’s virtual machine object, which is probably going to work as long as the guest exists and you have the correct permissions. It doesn’t tell you whether or not the guest operating system received the KVP. Change your KVPResponse.ps1 script to the following (you can just include it at the beginning if you don’t want to modify the existing contents):

$PreviousKVPCommand = (Get-VMKVPGuestToHost -Key 'VMAcknowledgement').Value
$KVPCommand = (Get-VMKVPHostToGuest -Key 'VMCommand').Value
if($PreviousKVPCommand -ne $KVPCommand)
{
	Send-VMKVPGuestToHost -Key 'VMAcknowledgement' -Value $KVPCommand
	# do processing stuff here
}
else
{
	# the command is identical to one already received
}

As shown, the “else” portion is unnecessary. Without it, duplicate commands will be ignored automatically and silently. I put it in to give you the option of handling them, if you wish. What you might choose to do is place a “return” statement in the else which would allow you to perform processing without nesting inside the “if” block. However, I would simplify that as:

$PreviousKVPCommand = (Get-VMKVPGuestToHost -Key 'VMAcknowledgement').Value
$KVPCommand = (Get-VMKVPHostToGuest -Key 'VMCommand').Value
if($PreviousKVPCommand -eq $KVPCommand)
{
    return
}

Send-VMKVPGuestToHost -Key 'VMAcknowledgement' -Value $KVPCommand

# do processing stuff here

Whatever works for you and your scripting style.

Note 1: If you’d rather use special KVP key names besides “VMCommand” and “VMAcknowledgement”, change them on lines 42 and 43, respectively, in the Send-VMKvpCommandToGuest function. I highlighted those lines in the above source to make them easy to find. Don’t forget to change the corresponding keys in your guest’s response script.

Note 2: I normally process the registry changes using a binary .Net module, and it picks up the changes instantly. To make this work in the PowerShell module, I had to add a delay in the verify portion of the Send-VMKVPCommandToGuest function. This means that you could fairly easily inject a secondary command while it is still waiting for the delay, which would cause the earlier Verify to fail. I made the delay a full second, but feel free to lower the number and test. It’s on line 44, which is highlighted in the above source. If you have a newer, not overloaded host, your guests might respond quickly enough that you don’t need it at all. Your mileage may vary, and I recommend that you test thoroughly.

Parsing the Command

Of course, with the command KVP containing a mashed-together string, you need to be able to unmash it on the guest. Here is a skeleton to help you do that. Just place this script into the KVPResponse.ps1 after the above bit:

$CommandString = $KVPCommand.SubString($KVPCommand.IndexOf(',') + 1)
if($CommandString.IndexOf(';'))
{
	$Command = $CommandString.SubString(0, $CommandString.IndexOf(';'))
	$ArgumentString = $CommandString.SubString($CommandString.IndexOf(';') + 1)
	$Arguments = $ArgumentString.Split(';')
}
else
{
	$Command = $CommandString
	$ArgumentString = ''
	$Arguments = @()
}

Now, when using the Send-VMKvpCommandToGuest function, you’ll have a $Command variable with the name of the command, and an $Arguments array that might have arguments in it.

Other Notes

To wrap up, here are a few other things that might be of interest to you.

Watching VMs on the Host for KVP Changes

I have not personally tinkered with watching a virtual machine’s KVP from the host side. The basic query to do so is:

SELECT * FROM __InstanceModificationEvent WHERE TargetInstance ISA 'Msvm_ComputerSystem'

Use this with the samples in the “Create a Permanent KVP Watcher” section, but on the host. You will likely want to modify the query to only watch for specific virtual machines, and this event will trigger any time that anything at all changes on the virtual machine, not just the KVPs, but this is where you would begin your journey.

Removing Permanent WMI Watchers

The events, consumers, and bindings are permanent and will persist through reboots and function while no one is logged on. If you want to get rid of them, start with the binding. You can start by listing all bindings:

Get-WMIObject -Namespace rootSubscription -Class __FilterToConsumerBinding | select Filter

This will show you the names of all the filters on the system, something like this:

PS C:> Get-WMIObject -Namespace rootSubscription -Class __FilterToConsumerBinding | select Filter

Filter
------
__EventFilter.Name="KVPRegistryFilter"
__EventFilter.Name="SCM Event Log Filter"

The “Name” portion is what you want. Use it like this:

$KVPBinding = Get-WmiObject -Namespace rootsubscription -Class __FilterToConsumerBinding -Filter '__PATH LIKE "%KVPRegistryFilter%"'

All you need to do on your system is place the name shown in the output of the previous query between the two % signs in the Get-WMIObject line above, where I currently have “KVPRegistryFilter”. Then, you can clean up like this:

$KVPBinding.Delete()

After that, you can remove the event and the consumer in any order. They’re a bit easier. I’ll start with the event.

First, get the name of the event from the binding object:

PS C:> $KVPBinding.Filter
__EventFilter.Name="KVPRegistryFilter"

With the name in hand, select it from WMI:

$KVPFilter = Get-WmiObject -Namespace rootsubscription -Class __EventFilter -Filter 'Name="KVPRegistryFilter"'
$KVPFilter.Delete()

From the binding, get the name of the consumer as you did with the filter:

PS C:> $KVPBinding.Consumer
CommandLineEventConsumer.Name="KVPRegistryConsumer"

This one isn’t nice enough to show you the class that you need, but everything you need to know is in the script from earlier in this post. For some reason, WMI does not like to filter event consumers by name, so we’re back to using __PATH as we did with the binding:

$KVPConsumer = Get-WmiObject -Namespace rootsubscription -Class __EventConsumer -Filter '__PATH LIKE "%KVPRegistryConsumer%"'
$KVPConsumer.Delete()

And, that’s it. If you remember all the names that you used, you could combine all of these into a saved script without all of the searching and do the entire delete process in only six lines (three, if you get clever, but showing off like that is rarely rewarded).

Go Forth and Automate

At this point, you’re only restricted by what you can place into that KVPResponse.ps1 file. I look forward to seeing what people come up with.

More in this series:

Part 1: Explanation

Part 3: Linux

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!

7 thoughts on "Hyper-V Key-Value Pair Data Exchange Part 2: Implementation"

  • Tomasz says:

    Hi
    How to remove “Key-Value Pair Exchange” integration service from VMs?

    • Eric Siron says:

      That service is part of the operating system. Removal might be possible with “sc” but it’s not supported and the side effects are unknown. Use Hyper-V Manager to disable it if you really don’t want it.

  • Tomasz says:

    Hi
    How to remove “Key-Value Pair Exchange” integration service from VMs?

  • Joe O'Bremski says:

    I have a “cloud” type environment where I’m hosting multiple networks living on the same Windows Clusters. All the VMs from DCs to File server to SQL server to workstations are all completely separated by VLANs but living on the same cluster. As such no guest network can talk to the host environment but checking on disk space, backup statuses, etc. require Virtual Machine Connecting from the host into each VM to check there status x 20 networks.

    This KVP is a great way to have the guest VMs report up their status where the Host Hyper-V server can collect and display their information.

    I can manually or Task Schedule my reports to report disk space and other stats and the KvPs get set without issue.

    ARCServe our backup software has the ability to run a Pre and Post script. Pre script I want it to clear any error flags that might be set by saying Key = “BACKUP” Value = “GOOD” and when the job finishes you can have it run the Post script if the job failed of is incomplete Key = “BACKUP” Value = “ERROR”. When the pre or post scripts run I get a powershell error that “The term ‘Send-VMKVPGuestToHost’ is not recognized” as if the PSM1 and PSD1 files are not in the Hyper-V-KVP-Guest folder under c:windowssystem32windowspowershellv1.0modules…. But like I said they are and I can run them without issue from Powershell myself??

    Ideas? Is ARCServe running powershell in like a sand box and can’t access that Modules folder?

    • Eric Siron says:

      Hmm… not impossible for them to set up a sandbox, but a lot of work without a lot of return.
      Try adding Import-Module -Name Hyper-V-KVP-Host to the top of your host scripts and Import-Module -Name Hyper-V-KVP-Guest to the top of your guest scripts.

  • PRakash says:

    Hello Eric,

    Do you know How Can we get Operating systems of All VMs in Hyper-V Cluster to .csv or .txt

    Which Script and which Partmeters to be passed in that script .

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.

Microsoft 365 Security checklist - free eBook