HVDX.psm1
<#
HVDX.psm1 module header and shared functions 2022-05-27 #> function Test-IsWindows { [CmdletBinding()] Param() if (($PSVersionTable.PSEdition -eq 'Desktop') -or ($PSVersionTable.Platform -eq 'Win32NT')) { $true } else { $false } } function Split-KVPPool { [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [byte[]]$KVPPool ) if ($null -eq $KVPPool) { return @() } $keySize = 512 $valSize = 2048 Write-Verbose "splitting $($KVPPool.Count) bytes into KVPs" $ptr = 0 while (($KVPPool.Count - $ptr) -ge ($keySize + $valSize)) { $keyBytes = @() $valBytes = @() for ($i = 0; $i -lt $keySize; $i++) { if ($KVPPool[$ptr + $i] -ne 0) {$keyBytes += $KVPPool[$ptr + $i]} else { break } } for ($i = 0; $i -lt $valSize; $i++) { if ($KVPPool[$ptr + $keySize + $i] -ne 0) {$valBytes += $KVPPool[$ptr + $keySize + $i]} else { break } } $ptr += ($keySize + $valSize) $kvpname = [char[]]$keyBytes -join '' $kvpvalue = [char[]]$valBytes -join '' [PSCustomObject]@{ 'Key' = $kvpname 'Value' = $kvpvalue } } } function ConvertTo-KVPStruct { [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [object]$KVPObject ) if (($KVPObject.PSObject.Properties.Name -notcontains 'Key') -or ($KVPObject.PSObject.Properties.Name -notcontains 'Value')) { Write-Warning "Object must contain properties Key and Value!" } else { $keySize = 512 $valSize = 2048 $keyBytes = [byte[]]$KVPObject.Key.ToCharArray() $valBytes = [byte[]]$KVPObject.Value.ToCharArray() $KVPStruct = $keyBytes + (@(0) * ($keySize - $keyBytes.Count)) $KVPStruct += $valBytes + (@(0) * ($valSize - $valBytes.Count)) $KVPStruct } } function Join-KVPPool { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [object[]]$KVPPool ) $KVPData = @() $ok = $true foreach ($kvp in $KVPPool) { $KVPStruct = ConvertTo-KVPStruct -KVPObject $kvp if ($null -ne $KVPStruct) { $KVPData += $KVPStruct } else { $ok = $false } } if ($ok) { $KVPData } else { Write-Warning "There were error converting some objects!" } } <# Read-HostKVP insert, part of HVDX.psm1 2022-05-27 https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn798287(v=ws.11) #> function Read-HostKVP { <# .SYNOPSIS Reads Hyper-V Data Exchange KVPs from a specified guest VM running on the local Hyper-V Host. .DESCRIPTION Reads Hyper-V Data Exchange KVPs from a specified guest VM running on the local Hyper-V Host. .PARAMETER VMName Specifies the name of the VM from which to read the KVPs. .PARAMETER Name Specifies the name of the KVP. If omitted, all KVPs are returned. .PARAMETER HostKVP If specified, the function will read the KVP written by the host instead of those written from within the guest. .INPUTS None. You cannot pipe objects to this function. .OUTPUTS Array of custom objects having two properties: Key and Value. $null if no KVPs (or no matching KVP) have been found. .EXAMPLE PS> Read-HostKVP -VMName Server01 -Name GuestServiceStatus Key Value --- ----- GuestServiceStatus OK .EXAMPLE PS> Read-HostKVP -VMName Server01 -HostKVP Key Value --- ----- FileToProcess file01.txt ShutDownDate 2022-06-03 #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string]$VMName, [Parameter(Mandatory = $false)] [string]$Name, [Parameter(Mandatory = $false)] [switch]$HostKVP ) if (!(Test-IsWindows)) { Write-Warning 'This must be ran on a Hyper-V host!' return } $wmiq = "SELECT * FROM Msvm_ComputerSystem WHERE Name <> '$VMName' AND ElementName = '$VMName'" try { $vm = Get-WmiObject -Namespace "root\virtualization\v2" -Query $wmiq -EA Stop } catch { Write-Warning $_.Exception.Message return } if ($null -ne $vm) { Write-Verbose "VM found by WMI" if ($HostKVP) { try { $kvps = ($vm.GetRelated("Msvm_KvpExchangeComponent")[0].GetRelated("Msvm_KvpExchangeComponentSettingData")).HostExchangeItems Write-Verbose "Read $($kvps.Count) host KVPs" } catch { Write-Warning $_.Exception.Message return } } else { try { $kvps = $vm.GetRelated("Msvm_KvpExchangeComponent").GuestExchangeItems Write-Verbose "Read $($kvps.Count) guest KVPs" } catch { Write-Warning $_.Exception.Message return } } foreach ($kvp in $kvps) { try { $xmlkvp = [xml]$kvp $kvpname = $xmlkvp.INSTANCE.PROPERTY.Where({$_.NAME -eq "Name"})[0].VALUE Write-Verbose "Procesing KVP [$kvpname]" if ([string]::IsNullOrEmpty($Name) -or ($Name -eq $kvpname)) { [PSCustomObject]@{ 'Key' = $kvpname 'Value' = $xmlkvp.INSTANCE.PROPERTY.Where({$_.NAME -eq "Data"})[0].VALUE } } } catch { Write-Warning $_.Exception.Message continue } } } } <# Write-HostKVP insert, part of HVDX.psm1 2022-05-27 https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn798287(v=ws.11) #> function Write-HostKVP { <# .SYNOPSIS Writes Hyper-V Data Exchange KVPs to a specified guest VM running on the local Hyper-V Host. .DESCRIPTION Writes Hyper-V Data Exchange KVPs to a specified guest VM running on the local Hyper-V Host. Add, Update or Remove. .PARAMETER VMName Specifies the name of the VM to which to wirte the KVPs. .PARAMETER Name Specifies the name of the KVP. If omitted, all KVPs are returned. .PARAMETER Value Specifies the value for the KVP. .PARAMETER Remove If specified, the function will remove the KVP if it exists. .PARAMETER Force If specified, the function will remove or update an existing KVP. .INPUTS None. You cannot pipe objects to this function. .OUTPUTS None. #> [CmdletBinding(DefaultParameterSetName = 'Upsert')] Param( [Parameter(Mandatory = $true)] [string]$VMName, [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'Upsert')] [string]$Value, [Parameter(Mandatory = $false, ParameterSetName = 'Remove')] [switch]$Remove, [Parameter(Mandatory = $false)] [switch]$Force ) if (!(Test-IsWindows)) { Write-Warning 'This must be ran on a Hyper-V host!' return } $wmiq = "SELECT * FROM Msvm_ComputerSystem WHERE Name <> '$VMName' AND ElementName = '$VMName'" try { $vm = Get-WmiObject -Namespace "root\virtualization\v2" -Query $wmiq -EA Stop } catch { Write-Warning $_.Exception.Message return } if ($null -ne $vm) { Write-Verbose "VM found by WMI" try { $kvps = ($vm.GetRelated("Msvm_KvpExchangeComponent")[0].GetRelated("Msvm_KvpExchangeComponentSettingData")).HostExchangeItems Write-Verbose "Read $($kvps.Count) host KVPs" } catch { Write-Warning $_.Exception.Message return } $kvpExists = $false foreach ($kvp in $kvps) { try { $xmlkvp = [xml]$kvp $kvpname = $xmlkvp.INSTANCE.PROPERTY.Where({$_.NAME -eq "Name"})[0].VALUE Write-Verbose "Procesing KVP [$kvpname]" if ([string]::IsNullOrEmpty($Name) -or ($Name -eq $kvpname)) { $kvpExists = $true break } } catch { Write-Warning $_.Exception.Message continue } } if (-not $kvpExists -and -not $Remove) { # add new kvp $VmMgmt = Get-WmiObject -Namespace "root\virtualization\v2" -Class "Msvm_VirtualSystemManagementService" $kvpDataItem = ([WMIClass][String]::Format("\\{0}\{1}:{2}", $VmMgmt.ClassPath.Server, $VmMgmt.ClassPath.NamespacePath, "Msvm_KvpExchangeDataItem")).CreateInstance() $kvpDataItem.Name = $Name $kvpDataItem.Data = $Value $kvpDataItem.Source = 0 try { $null = $VmMgmt.AddKvpItems($Vm, $kvpDataItem.PSBase.GetText(1)) Write-Verbose "KVP item added successfully" } catch { Write-Warning $_.Exception.Message } } elseif ($kvpExists) { if ($Remove) { # remove the kvp if ($Force) { $VmMgmt = Get-WmiObject -Namespace "root\virtualization\v2" -Class "Msvm_VirtualSystemManagementService" $kvpDataItem = ([WMIClass][String]::Format("\\{0}\{1}:{2}", $VmMgmt.ClassPath.Server, $VmMgmt.ClassPath.NamespacePath, "Msvm_KvpExchangeDataItem")).CreateInstance() $kvpDataItem.Name = $Name $kvpDataItem.Data = [string]::Empty $kvpDataItem.Source = 0 try { $null = $VmMgmt.RemoveKvpItems($Vm, $kvpDataItem.PSBase.GetText(1)) Write-Verbose "KVP item removed successfully" } catch { Write-Warning $_.Exception.Message } } else { Write-Warning "KVP item found, use -Force to actually remove" } } else { # update the kvp if ($Force) { $VmMgmt = Get-WmiObject -Namespace "root\virtualization\v2" -Class "Msvm_VirtualSystemManagementService" $kvpDataItem = ([WMIClass][String]::Format("\\{0}\{1}:{2}", $VmMgmt.ClassPath.Server, $VmMgmt.ClassPath.NamespacePath, "Msvm_KvpExchangeDataItem")).CreateInstance() $kvpDataItem.Name = $Name $kvpDataItem.Data = $Value $kvpDataItem.Source = 0 try { $null = $VmMgmt.ModifyKvpItems($Vm, $kvpDataItem.PSBase.GetText(1)) Write-Verbose "KVP item updated successfully" } catch { Write-Warning $_.Exception.Message } } else { Write-Warning "KVP item found, use -Force to actually update" } } } } } <# Read-GuestKVP insert, part of HVDX.psm1 2022-05-27 https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn798287(v=ws.11) #> function Read-GuestKVP { <# .SYNOPSIS Reads Hyper-V Data Exchange KVPs from within the guest. .DESCRIPTION Reads Hyper-V Data Exchange KVPs from within the guest. Works on Windows and other OSes with Integration Services installed and operational. .PARAMETER Name Specifies the name of the KVP. If omitted, all KVPs are returned. .PARAMETER GuestKVP If specified, the function will read the KVP written by the guest instead of those written from the hypervisor. .PARAMETER SystemKVP If specified, the function will read the KVP written by hypervisor automatically (like hostname, VM name etc.). .INPUTS None. You cannot pipe objects to this function. .OUTPUTS Array of custom objects having two properties: Key and Value. $null if no KVPs (or no matching KVP) have been found. .EXAMPLE PS> Read-GuestKVP -Name FileToProcess Key Value --- ----- FileToProcess file01.txt .EXAMPLE PS> Read-HostKVP -SystemKVP Key Value --- ----- HostName <HostName> HostingSystemEditionId 8 HostingSystemNestedLevel 0 HostingSystemOsMajor 10 etc.. VirtualMachineId 7FAE99CA-0B48-445C-A2B9-E9065ABB1E4A VirtualMachineName <VMName> #> [CmdletBinding(DefaultParameterSetName = 'Host')] Param( [Parameter(Mandatory = $false)] [string]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'Guest')] [switch]$GuestKVP, [Parameter(Mandatory = $false, ParameterSetName = 'System')] [switch]$SystemKVP ) if (Test-IsWindows) { Write-Verbose "Running on Windows OS" Write-Verbose "Mode: $($PSCmdlet.ParameterSetName)" if ($GuestKVP) { $regKey = "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest" } elseif ($SystemKVP) { $regKey = "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters" } else { $regKey = "HKLM:\SOFTWARE\Microsoft\Virtual Machine\External" } Write-Verbose "Reading registry: $regKey" if (Test-Path $regKey) { try { $regItem = Get-Item -Path $regKey -EA Stop } catch { Write-Warning $_.Exception.Message return } Write-Verbose "Key has $($regItem.Property.Count) entries" foreach ($kvpname in $regItem.Property) { Write-Verbose "Processing KVP [$kvpname]" try { $regProp = Get-ItemProperty -Path $regKey -Name $kvpname -EA Stop } catch { Write-Warning $_.Exception.Message continue } if ([string]::IsNullOrEmpty($Name) -or ($Name -eq $kvpname)) { [PSCustomObject]@{ 'Key' = $kvpname 'Value' = $regProp."$kvpname" } } } } else { Write-Warning "Registry key not found: $regKey" } } else { Write-Verbose "Running on a Non-Windows OS" Write-Verbose "Mode: $($PSCmdlet.ParameterSetName)" if ($GuestKVP) { $regPool = "1" } elseif ($SystemKVP) { $regPool = "3" } else { $regPool = "0" } $regPoolFile = ".kvp_pool_$regPool" Write-Verbose "Reading file: $regPoolFile" $poolFilePath = Join-Path -Path "/var/lib/hyperv" -ChildPath $regPoolFile if ([System.IO.File]::Exists($poolFilePath)) { Write-Verbose "Found pool file: $poolFilePath" $bytes = [System.IO.File]::ReadAllBytes($poolFilePath) Write-Verbose "Read $($bytes.Count) bytes from pool file" $kvps = Split-KVPPool -KVPPool $bytes if ([string]::IsNullOrEmpty($Name)) { $kvps } else { $kvps | Where-Object Key -eq $Name } } else { Write-Warning "Pool file not found: $poolFilePath" } } } <# Write-GuestKVP insert, part of HVDX.psm1 2022-05-27 https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn798287(v=ws.11) #> function Write-GuestKVP { <# .SYNOPSIS Writes Hyper-V Data Exchange KVPs within the guest OS. .DESCRIPTION Writes Hyper-V Data Exchange KVPs within the guest OS. Works on Windows and other OSes with Integration Services installed and operational. Add, Update or Remove. .PARAMETER Name Specifies the name of the KVP. If omitted, all KVPs are returned. The system KVPs such as "SessionMonitor" cannot be edited. .PARAMETER Value Specifies the value for the KVP. .PARAMETER Remove If specified, the function will remove the KVP if it exists. .PARAMETER Force If specified, the function will remove or update an existing KVP. .INPUTS None. You cannot pipe objects to this function. .OUTPUTS None. #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'Upsert')] [string]$Value, [Parameter(Mandatory = $false, ParameterSetName = 'Remove')] [switch]$Remove, [Parameter(Mandatory = $false)] [switch]$Force ) $doNotModify = @("SessionMonitor") if ($doNotModify -contains $Name) { Write-Warning "This KVP cannot be edited or removed!" return } if (Test-IsWindows) { Write-Verbose "Running on Windows OS" $regKey = "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest" Write-Verbose "Reading registry: $regKey" $kvpExists = $false if (Test-Path $regKey) { try { $regItem = Get-Item -Path $regKey -EA Stop } catch { Write-Warning $_.Exception.Message return } Write-Verbose "Key has $($regItem.Property.Count) entries" if ($regItem.Property -contains $Name) { $kvpExists = $true } if (-not $kvpExists -and -not $Remove) { # add new kvp try { $null = New-ItemProperty -Path $regKey -Name $Name -PropertyType String -Value $Value -EA Stop Write-Verbose "KVP added successfully" } catch { Write-Warning $_.Exception.Message } } elseif ($kvpExists) { if ($Remove) { # remove the kvp if ($Force) { try { $null = Remove-ItemProperty -Path $regKey -Name $Name -EA Stop Write-Verbose "KVP removed successfully" } catch { Write-Warning $_.Exception.Message } } else { Write-Warning "KVP item found, use -Force to actually remove" } } else { # update the kvp if ($Force) { try { $null = Set-ItemProperty -Path $regKey -Name $Name -Value $Value -EA Stop Write-Verbose "KVP updated successfully" } catch { Write-Warning $_.Exception.Message } } else { Write-Warning "KVP item found, use -Force to actually update" } } } } else { Write-Warning "Registry key not found: $regKey" } } else { Write-Verbose "Running on a Non-Windows OS" $regPoolFile = ".kvp_pool_1" Write-Verbose "Reading file: $regPoolFile" $poolFilePath = Join-Path -Path "/var/lib/hyperv" -ChildPath $regPoolFile if ([System.IO.File]::Exists($poolFilePath)) { Write-Verbose "Found pool file: $poolFilePath" $bytes = [System.IO.File]::ReadAllBytes($poolFilePath) Write-Verbose "Read $($bytes.Count) bytes from pool file" $kvps = Split-KVPPool -KVPPool $bytes Write-Verbose "Read $($kvps.Count) KVPs from pool file" $kvpExists = $false $kvpChanged = $false if ($kvps.Key -contains $Name) { $kvpExists = $true } if (-not $kvpExists -and -not $Remove) { # add new kvp $kvps += [PSCustomObject]@{ 'Key' = $Name 'Value' = $Value } $kvpChanged = $true } elseif ($kvpExists) { if ($Remove) { # remove the kvp if ($Force) { $kvps = $kvps | Where-Object {$_.Key -ne $Name} $kvpChanged = $true } else { Write-Warning "KVP item found, use -Force to actually remove" } } else { # update the kvp if ($Force) { $kvps[$kvps.Key.IndexOf($Name)].Value = $Value $kvpChanged = $true } else { Write-Warning "KVP item found, use -Force to actually update" } } } if ($kvpChanged) { Write-Verbose "Writing changed KVPs to pool file" $bytes = Join-KVPPool -KVPPool $kvps Write-Verbose "$($bytes.Count) bytes to write" try { $null = [System.IO.File]::WriteAllBytes($poolFilePath, $bytes) Write-Verbose "Pool file written successfully" } catch { Write-Warning $_.Exception.Message } } } else { Write-Warning "Pool file not found: $poolFilePath" } } } #region module init $moduleFunctions = @( 'Read-HostKVP' 'Read-GuestKVP' 'Write-HostKVP' 'Write-GuestKVP' ) Export-ModuleMember -Function $moduleFunctions #endregion |