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