Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md)
- Switch to use VM image `windows-latest` to build phase.
- Use latest DscCommunity scripts and files
- Changed `HyperVDsc.Common` to a buildable module.
- Added support to enable or disable the TPM on a virtual machine - Fixes [Issue #214](https://github.com/dsccommunity/HyperVDsc/issues/214).

## [3.18.0] - 2022-06-04

Expand Down
103 changes: 103 additions & 0 deletions source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,17 @@ function Get-TargetResource
}

$vmSecureBootState = $false
$vmTPMState = $false
if ($vmobj.Generation -eq 2)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
# Retrieve secure boot status (can only be enabled on Generation 2 VMs) and convert to a boolean.
$vmSecureBootState = ($vmobj | Get-VMFirmware).SecureBoot -eq 'On'

# Retrieve TPM status (can only be enabled on Generation 2 VMs) and return boolean.
$vmTPMState = ($vmobj | Get-VMSecurity).TpmEnabled
}


$guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id

$macAddress = @()
Expand Down Expand Up @@ -90,6 +95,7 @@ function Get-TargetResource
Path = $vmobj.Path
Generation = $vmobj.Generation
SecureBoot = $vmSecureBootState
EnableTPM = $vmTPMState
StartupMemory = $vmobj.MemoryStartup
MinimumMemory = $vmobj.MemoryMinimum
MaximumMemory = $vmobj.MemoryMaximum
Expand Down Expand Up @@ -206,6 +212,11 @@ function Set-TargetResource
[System.Boolean]
$SecureBoot = $true,

# Enable Trusted Platform Module for Generation 2 VMs
[Parameter()]
[System.Boolean]
$EnableTPM = $false,
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Enable Guest Services
[Parameter()]
[System.Boolean]
Expand Down Expand Up @@ -427,6 +438,54 @@ function Set-TargetResource
}
}

if ($vmObj.Generation -eq 2)
{
# Retrive the current TPM state
$vmTPMEnabled = Test-VMTpmEnabled -Name $Name
if ($EnableTPM -ne $vmTPMEnabled)
{
Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'TPMEnabled', $EnableTPM, $vmTPMEnabled)

# Bring the TPM state in line with the desired state (restarts the VM if needed).
if ($EnableTPM)
{
# The default value for the key protector is 0,0,0,4
$keyProtectorDefaultValue = @(0,0,0,4)
# compare the default key protector value and the VM's key protector value
$isVMKeyProtectorDefault = -not(Compare-Object -ReferenceObject (Get-VMKeyProtector -VMName $Name) -DifferenceObject $keyProtectorDefaultValue)

# If the VM has a default key protector, we need to create a new one before enabling the TPM
if ($isVMKeyProtectorDefault)
{
Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector
}

$setVMPropertyParams = @{
VMName = $Name
VMCommand = 'Enable-VMTPM'
ChangeProperty = @{
Confirm = $false
}
RestartIfNeeded = $RestartIfNeeded
}
}
else
{
$setVMPropertyParams = @{
VMName = $Name
VMCommand = 'Disable-VMTPM'
ChangeProperty = @{
Confirm = $false
}
RestartIfNeeded = $RestartIfNeeded
}
}

Set-VMProperty @setVMPropertyParams
Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'TPMEnabled', $EnableTPM)
}
}

if ($Notes -ne $null)
{
# If the VM notes do not match the desire notes, update them. This can be done while the VM is running.
Expand Down Expand Up @@ -576,6 +635,26 @@ function Set-TargetResource
{
Set-VMFirmware -VMName $Name -EnableSecureBoot Off
}

<#
TPM is only applicable to Generation 2 VMs and it defaults to disabled.
Therefore, we only need to explicitly set it to enabled if specified.
#>
if ($EnableTPM -eq $true)
{
# The default value for the key protector is 0,0,0,4
$keyProtectorDefaultValue = @(0,0,0,4)
# compare the default key protector value and the VM's key protector value
$isVMKeyProtectorDefault = -not(Compare-Object -ReferenceObject (Get-VMKeyProtector -VMName $Name) -DifferenceObject $keyProtectorDefaultValue)

# If the VM has a default key protector, we need to create a new one before enabling the TPM
if ($isVMKeyProtectorDefault)
{
Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector
}

Enable-VMTPM -VMName $Name
}
}

if ($EnableGuestService)
Expand Down Expand Up @@ -687,6 +766,11 @@ function Test-TargetResource
[System.Boolean]
$SecureBoot = $true,

# Enable Trusted Platform Module for Generation 2 VMs
[Parameter()]
[System.Boolean]
$EnableTPM = $false,

[Parameter()]
[System.Boolean]
$EnableGuestService = $false,
Expand Down Expand Up @@ -864,6 +948,13 @@ function Test-TargetResource
Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot)
return $false
}

$vmTPMEnabled = Test-VMTpmEnabled -Name $Name
if ($EnableTPM -ne $vmTPMEnabled)
{
Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'TPMEnabled', $EnableTPM, $vmTPMEnabled)
return $false
}
}

$guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id
Expand Down Expand Up @@ -988,6 +1079,18 @@ function Test-VMSecureBoot
return (Get-VMFirmware -VM $vm).SecureBoot -eq 'On'
}

function Test-VMTpmEnabled
{
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name
)
$vm = Get-VM -Name $Name
return (Get-VMSecurity -VM $vm).TpmEnabled
}

#endregion

Export-ModuleMember -Function *-TargetResource
1 change: 1 addition & 0 deletions source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class DSC_VMHyperV : OMI_BaseResource
[Write, Description("Specifies if the VM should be Present (created) or Absent (removed). The default value is `Present`."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure;
[Write, Description("Notes about the VM.")] String Notes;
[Write, Description("Specifies if Secure Boot should be enabled for Generation 2 virtual machines. **Only supports generation 2 virtual machines**. Default value is `$true`.")] Boolean SecureBoot;
[Write, Description("Specifies if Trusted Platform Module (TPM) should be enabled for Generation 2 virtual machines. **Only supports generation 2 virtual machines**. Default value is `$false`.")] Boolean EnableTPM;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align EnableTPM schema property with Get-TargetResource output

You’ve added a writable EnableTPM property in the schema, but Get-TargetResource currently returns a key named TpmEnabled rather than EnableTPM. For MOF resources, all [Write] properties should be present in the hashtable returned from Get-TargetResource; otherwise Get-DscConfiguration won’t surface EnableTPM as a configured property.

Consider either:

  • Returning EnableTPM = $vmTPMState from Get-TargetResource (replacing TpmEnabled), or
  • Declaring an additional [Read] Boolean TpmEnabled; in the schema if you want to expose both desired (EnableTPM) and actual (TpmEnabled) TPM state.

Based on learnings, …

🤖 Prompt for AI Agents
In source/DSCResources/DSC_VMHyperV/DSC_VMHyperV.schema.mof around line 20, the
writable schema property EnableTPM does not match the key returned by
Get-TargetResource (which currently returns TpmEnabled), so Get-DscConfiguration
won't report EnableTPM; fix by either updating Get-TargetResource to return
EnableTPM = $vmTPMState in the hashtable (replacing TpmEnabled) so the writeable
property is present, or alternatively add a Read-only Boolean TpmEnabled;
declaration to the MOF schema to explicitly expose the actual TPM state while
keeping EnableTPM as the desired state.

[Write, Description("Enable Guest Service Interface for the VM. The default value is `$false`.")] Boolean EnableGuestService;
[Write, Description("Enable AutomaticCheckpoints for the VM.")] Boolean AutomaticCheckpointsEnabled;
[Read, Description("Returns the unique ID for the VM.")] String ID;
Expand Down
69 changes: 69 additions & 0 deletions source/Examples/Resources/VMHyperV/7-TPMEnabled.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<#
.SYNOPSIS
Creates a Generation 2 VM with the Trusted Platform Module (TPM) enabled.

.DESCRIPTION
Creates a new Generation 2 virtual machine and enables the Trusted
Platform Module (TPM) on it.

.PARAMETER NodeName
The names of one or more nodes to compile a configuration for.
Defaults to 'localhost'.

.PARAMETER VMName
The name of the virtual machine to create.

.PARAMETER VhdPath
The path to the VHDX file to associate with the virtual machine.

.INPUTS
None.

.OUTPUTS
None.

.EXAMPLE
Example -VMName 'TPMVM' -VhdPath 'C:\VMs\TPMVM.vhdx'

Compiles a configuration that creates a Generation 2 VM named 'TPMVM'
with TPM enabled.
#>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
configuration Example
{
param
(
[System.String[]]
$NodeName = 'localhost',

[Parameter(Mandatory = $true)]
[System.String]
$VMName,

[Parameter(Mandatory = $true)]
[System.String]
$VhdPath
)

Import-DscResource -ModuleName 'HyperVDsc'

Node $NodeName
{
# Install HyperV feature, if not installed - Server SKU only
WindowsFeature HyperV
{
Ensure = 'Present'
Name = 'Hyper-V'
}

# Ensures a VM with default settings
VMHyperV NewVM
{
Ensure = 'Present'
Name = $VMName
VhdPath = $VhdPath
Generation = 2
EnableTPM = $true
DependsOn = '[WindowsFeature]HyperV'
}
}
}
94 changes: 94 additions & 0 deletions tests/Unit/DSC_VMHyperV.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ try
return $stubVM
}

Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'StoppedGeneration2VM' } -MockWith {
$stubVM = [Microsoft.HyperV.PowerShell.VirtualMachine]::CreateTypeInstance()
$stubVM.Name = 'StoppedGeneration2VM'
$stubVM.HardDrives = @(
$stubVhdxDisk,
$stubVhdDisk
)
$stubVM.Path = $StubVMConfig.FullPath
$stubVM.Generation = 2
$stubVM.MemoryStartup = 512MB
$stubVM.MemoryMinimum = 128MB
$stubVM.MemoryMaximum = 4096MB
$stubVM.ProcessorCount = 1
$stubVM.ID = $mockVmGuid
$stubVM.CPUUsage = 10
$stubVM.MemoryAssigned = 512MB
$stubVM.Uptime = New-TimeSpan -Hours 12
$stubVM.CreationTime = (Get-Date).AddHours(-12)
$stubVM.DynamicMemoryEnabled = $true
$stubVM.Notes = ''
$stubVM.State = 'Off'
$stubVM.NetworkAdapters = @(
$stubNIC1,
$stubNIC2
)

return $stubVM
}

Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'PausedVM' } -MockWith {
$stubVM = [Microsoft.HyperV.PowerShell.VirtualMachine]::CreateTypeInstance()
$stubVM.Name = 'PausedVM'
Expand Down Expand Up @@ -387,10 +416,17 @@ try

It 'Calls Get-VMFirmware if a generation 2 VM' {
Mock -CommandName Get-VMFirmware -MockWith { return $true }
Mock -CommandName Get-VMSecurity -MockWith { return @{ TpmEnabled = $false } }
$null = Get-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk.Path
Assert-MockCalled -CommandName Get-VMFirmware -Scope It -Exactly 1
}

It 'Calls Get-VMSecurity if a generation 2 VM' {
Mock -CommandName Get-VMSecurity -MockWith { return @{ TpmEnabled = $false } }
$null = Get-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk.Path
Assert-MockCalled -CommandName Get-VMSecurity -Scope It -Exactly 1
}

It 'Hash table contains key EnableGuestService' {
$targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.Path
$targetResource.ContainsKey('EnableGuestService') | Should -Be $true
Expand Down Expand Up @@ -474,6 +510,7 @@ try

It 'Returns $true when VM .vhdx file is specified with a generation 2 VM' {
Mock -CommandName Test-VMSecureBoot -MockWith { return $true }
Mock -CommandName Test-VMTpmEnabled -MockWith { return $false }
Test-TargetResource -Name 'Generation2VM' -Generation 2 @testParams | Should -Be $true
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -512,6 +549,20 @@ try
Test-TargetResource -Name 'Generation2VM' -SecureBoot $false -Generation 2 @testParams | Should -Be $false
}

It 'Returns $true when TPM is enabled and requested "EnableTPM" = "$true"' {
Mock -CommandName Test-VMTpmEnabled -MockWith { return $true }

Test-TargetResource -Name 'Generation2VM' -EnableTPM $true -Generation 2 @testParams |
Should -BeTrue
}

It 'Returns $false when TPM is disabled and requested "EnableTPM" = "$true"' {
Mock -CommandName Test-VMTpmEnabled -MockWith { return $false }

Test-TargetResource -Name 'Generation2VM' -EnableTPM $true -Generation 2 @testParams |
Should -BeFalse
}

It 'Returns $true when VM has snapshot chain' {
Mock -CommandName Get-VhdHierarchy -MockWith {
return @($studVhdxDiskSnapshot, $stubVhdxDisk)
Expand Down Expand Up @@ -605,6 +656,8 @@ try
Mock -CommandName Get-VMNetworkAdapter -MockWith { return $stubVM.NetworkAdapters.IpAddresses }
Mock -CommandName Set-VMState -MockWith { return $true }
Mock -CommandName Set-VMMemory
# Default TPM state for generation 2 VMs so the new TPM code path does not hit the Hyper-V stub.
Mock -CommandName Test-VMTpmEnabled -MockWith { return $false }

It 'Removes an existing VM when "Ensure" = "Absent"' {
Set-TargetResource -Name 'RunningVM' -Ensure Absent @testParams
Expand Down Expand Up @@ -778,6 +831,47 @@ try
Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 1 -Scope It
}

It 'Enables TPM without prompting when an existing generation 2 VM has TPM disabled' {
Mock -CommandName Test-VMSecureBoot -MockWith { return $true }
Mock -CommandName Test-VMTpmEnabled -MockWith { return $false }
Mock -CommandName Get-VMKeyProtector -MockWith { return @(0, 0, 0, 4) }
Mock -CommandName Set-VMKeyProtector
Mock -CommandName Set-VMProperty

Set-TargetResource -Name 'StoppedGeneration2VM' -Generation 2 -EnableTPM $true @testParams

Assert-MockCalled -CommandName Set-VMKeyProtector -Exactly 1 -Scope It
Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter {
$VMCommand -eq 'Enable-VMTPM' -and
$ChangeProperty.Confirm -eq $false
} -Exactly 1 -Scope It
}

It 'Disables TPM without prompting when an existing generation 2 VM has TPM enabled' {
Mock -CommandName Test-VMSecureBoot -MockWith { return $true }
Mock -CommandName Test-VMTpmEnabled -MockWith { return $true }
Mock -CommandName Set-VMProperty

Set-TargetResource -Name 'StoppedGeneration2VM' -Generation 2 -EnableTPM $false @testParams

Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter {
$VMCommand -eq 'Disable-VMTPM' -and
$ChangeProperty.Confirm -eq $false
} -Exactly 1 -Scope It
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
It 'Does not configure TPM on an existing generation 1 VM when generation 2 is requested' {
Mock -CommandName Test-VMSecureBoot -MockWith { return $true }
Mock -CommandName Test-VMTpmEnabled -MockWith { return $false }
Mock -CommandName Set-VMProperty

Set-TargetResource -Name 'StoppedVM' -Generation 2 -EnableTPM $true @testParams

Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter {
$VMCommand -in @('Enable-VMTPM', 'Disable-VMTPM')
} -Exactly 0 -Scope It
}

It 'Does call "Enable-VMIntegrationService" when "EnableGuestService" = "$true"' {
Mock -CommandName Enable-VMIntegrationService
Set-TargetResource -Name 'RunningVM' -EnableGuestService $true @testParams
Expand Down