diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 index 512749b..e220551 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 @@ -34,7 +34,7 @@ function Get-TargetResource SwitchName = $vmObj.NetworkAdapters[0].SwitchName State = $vmobj.State Path = $vmobj.Path - Generation = if($vmobj.Generation -eq 1){"Vhd"}else{"Vhdx"} + Generation = $vmobj.Generation StartupMemory = $vmobj.MemoryStartup MinimumMemory = $vmobj.MemoryMinimum MaximumMemory = $vmobj.MemoryMaximum @@ -76,9 +76,9 @@ function Set-TargetResource # Folder where the VM data will be stored [String]$Path, - # Associated Virtual disk format - Vhd or Vhdx - [ValidateSet("Vhd","Vhdx")] - [String]$Generation = "Vhd", + # Virtual machine generation + [ValidateRange(1,2)] + [UInt32]$Generation = 1, # Startup RAM for the VM [ValidateRange(32MB,17342MB)] @@ -222,11 +222,11 @@ function Set-TargetResource $parameters = @{} $parameters["Name"] = $Name $parameters["VHDPath"] = $VhdPath + $parameters["Generation"] = $Generation # Optional parameters if($SwitchName){$parameters["SwitchName"]=$SwitchName} if($Path){$parameters["Path"]=$Path} - if($Generation){$parameters["Generation"]=if($Generation -eq "Vhd"){1}else{2}} $defaultStartupMemory = 512MB if($StartupMemory){$parameters["MemoryStartupBytes"]=$StartupMemory} elseif($MinimumMemory -and $defaultStartupMemory -lt $MinimumMemory){$parameters["MemoryStartupBytes"]=$MinimumMemory} @@ -296,9 +296,9 @@ function Test-TargetResource # Folder where the VM data will be stored [String]$Path, - # Associated Virtual disk format - Vhd or Vhdx - [ValidateSet("Vhd","Vhdx")] - [String]$Generation = "Vhd", + # Virtual machine generation + [ValidateRange(1,2)] + [UInt32]$Generation = 1, # Startup RAM for the VM [ValidateRange(32MB,17342MB)] @@ -369,12 +369,14 @@ function Test-TargetResource Throw "StartupMemory($StartupMemory) should not be greater than MaximumMemory($MaximumMemory)" } - # Check if the generation matches the VhdPath extenstion - if($Generation -and ($VhdPath.Split('.')[-1] -ne $Generation)) + <# VM Generation has no direct relation to the virtual hard disk format and cannot be changed + after the virtual machine has been created. Generation 2 VMs do not support .VHD files. #> + if(($Generation -eq 2) -and ($VhdPath.Split('.')[-1] -eq 'vhd')) { - Throw "Generation $geneartion should match virtual disk extension $($VhdPath.Split('.')[-1])" + Throw "Generation 2 virtual machines do not support the .VHD virtual disk extension." } + # Check if $Path exist if($Path -and !(Test-Path -Path $Path)) { @@ -396,6 +398,7 @@ function Test-TargetResource if($state -and ($vmObj.State -ne $State)){return $false} if($StartupMemory -and ($vmObj.MemoryStartup -ne $StartupMemory)){return $false} if($MACAddress -and ($vmObj.NetWorkAdapters.MacAddress -notcontains $MACAddress)){return $false} + if($Generation -ne $vmObj.Generation){return $false} if($ProcessorCount -and ($vmObj.ProcessorCount -ne $ProcessorCount)){return $false} if($MaximumMemory -and ($vmObj.MemoryMaximum -ne $MaximumMemory)){return $false} if($MinimumMemory -and ($vmObj.MemoryMinimum -ne $MinimumMemory)){return $false} @@ -510,4 +513,3 @@ function Get-VMIPAddress #endregion Export-ModuleMember -Function *-TargetResource - diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof index 97c5ba2..4da030a 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof @@ -1,4 +1,4 @@ -[ClassVersion("1.0.0"), FriendlyName("xVMHyperV")] +[ClassVersion("1.0.0"), FriendlyName("xVMHyperV")] class MSFT_xVMHyperV : OMI_BaseResource { [Key, Description("Name of the VM")] String Name; @@ -6,7 +6,7 @@ class MSFT_xVMHyperV : OMI_BaseResource [Write, Description("Virtual switch associated with the VM")] String SwitchName; [Write, Description("State of the VM."), ValueMap{"Running","Paused","Off"}, Values{"Running","Paused","Off"}] String State; [Write, Description("Folder where the VM data will be stored")] String Path; - [Write, Description("Associated Virtual disk format - Vhd or Vhdx"), ValueMap{"Vhd","Vhdx"}, Values{"Vhd","Vhdx"}] String Generation; + [Write, Description("Virtual machine generation")] Uint32 String Generation; [Write, Description("Startup RAM for the VM.")] Uint64 StartupMemory; [Write, Description("Minimum RAM for the VM. This enables dynamic memory.")] Uint64 MinimumMemory; [Write, Description("Maximum RAM for the VM. This enable dynamic memory.")] Uint64 MaximumMemory; diff --git a/README.md b/README.md index 920ae7a..bf0cfa3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ This resource is particularly useful when bootstrapping DSC Configurations into * **SwitchName**: Virtual switch associated with the VM * **State**: State of the VM: { Running | Paused | Off } * **Path**: Folder where the VM data will be stored -* **Generation**: Associated Virtual disk format { Vhd | Vhdx } +* **Generation**: Virtual machine generation { 1 | 2 }. +Generation 2 virtual machines __only__ support VHDX files. * **StartupMemory**: Startup RAM for the VM * **MinimumMemory**: Minimum RAM for the VM. Setting this property enables dynamic memory. @@ -45,6 +46,12 @@ Setting this property enables dynamic memory. * **RestartIfNeeded**: If specified, will shutdown and restart the VM as needed for property changes * **Ensure**: Ensures that the VM is Present or Absent +The following xVMHyper-V properties **cannot** be changed after VM creation: + +* VhdPath +* Path +* Generation + ### xVMSwitch * **Name**: The desired VM Switch name @@ -61,6 +68,12 @@ Please see the Examples section for more details. ## Versions +### Unreleased +* Decoupled VM generation from underlying VHD format in xVMHyperV resource. + * The initial generation property was tied to the virtual disk format which was incorrect and has been rectified. + * __This is a breaking change__ due to the xVMHyperV.Generation property has changing from a String to an Integer. + * However, this change will only impact configurations that have previously explicitly specified the VM generation is either "vhd" or "vhdx". + ### 2.4.0.0 * Fixed VM power state issue in xVMHyperV resource @@ -70,7 +83,6 @@ Please see the Examples section for more details. ### 2.2.1 - ### 2.1 * Added logic to automatically adjust VM’s startup memory when only minimum and maximum memory is specified in configuration @@ -88,7 +100,6 @@ Please see the Examples section for more details. - xVMHyperV - xVMSwitch - ## Examples ### End-to-End Example @@ -147,7 +158,7 @@ Configuration Sample_EndToEndXHyperV_RunningVM } - # create the testVM out of the vhd. + # create the generation 1 testVM out of the vhd. xVMHyperV testvm { Name = "$($name)_vm" @@ -247,9 +258,9 @@ Configuration Sample_xVhd_DiffVHD } ``` -### Create a VM for a given VHD +### Create a generation 2 VM for a given VHD -This configuration will create a VM, given a VHD, on Hyper-V host. +This configuration will create a VM, given a VHDX, on Hyper-V host. ```powershell Configuration Sample_xVMHyperV_Simple @@ -262,7 +273,7 @@ Configuration Sample_xVMHyperV_Simple [string]$VMName, [Parameter(Mandatory)] - [string]$VhdPath + [string]$VhdxPath ) Import-DscResource -module xHyper-V @@ -281,8 +292,8 @@ Configuration Sample_xVMHyperV_Simple { Ensure = 'Present' Name = $VMName - VhdPath = $VhdPath - Generation = $VhdPath.Split('.')[-1] + VhdPath = $VhdxPath + Generation = 2 DependsOn = '[WindowsFeature]HyperV' } } @@ -306,6 +317,10 @@ Configuration Sample_xVMHyperV_DynamicMemory [Parameter(Mandatory)] [string]$VhdPath, + [Parameter(Mandatory)] + [ValidateSet(1,2)] + [unit32]$Generation, + [Parameter(Mandatory)] [Uint64]$StartupMemory, @@ -333,7 +348,7 @@ Configuration Sample_xVMHyperV_DynamicMemory Ensure = 'Present' Name = $VMName VhdPath = $VhdPath - Generation = $VhdPath.Split('.')[-1] + Generation = $Generation StartupMemory = $StartupMemory MinimumMemory = $MinimumMemory MaximumMemory = $MaximumMemory @@ -360,6 +375,10 @@ Configuration Sample_xVMHyperV_Complete [Parameter(Mandatory)] [string]$VhdPath, + [Parameter(Mandatory)] + [ValidateSet(1,2)] + [unit32]$Generation, + [Parameter(Mandatory)] [Uint64]$StartupMemory, @@ -404,7 +423,7 @@ Configuration Sample_xVMHyperV_Complete SwitchName = $SwitchName State = $State Path = $Path - Generation = $VhdPath.Split('.')[-1] + Generation = $Generation StartupMemory = $StartupMemory MinimumMemory = $MinimumMemory MaximumMemory = $MaximumMemory diff --git a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 index 86feb1b..9c3ede5 100644 --- a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 +++ b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 @@ -20,22 +20,25 @@ Describe 'xVMHyper-V' { ## Create empty functions to be able to mock the missing Hyper-V cmdlets ## CmdletBinding required on Get-VM to support $ErrorActionPreference function Get-VM { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)] $Name) } - function New-VM { } + ## Generation parameter is required for the mocking -ParameterFilter to work + function New-VM { param ( $Generation) } function Set-VM { } function Stop-VM { } function Remove-VM { } function Get-VMNetworkAdapter { } function Set-VMNetworkAdapter { } - $stubVMDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; + $stubVhdxDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; + $stubVhdDisk = New-Item -Path 'TestDrive:\TestVM.vhd' -ItemType File; $StubVMConfig = New-Item -Path 'TestDrive:\TestVM.xml' -ItemType File; $stubVM = @{ HardDrives = @( - @{ Path = $stubVMDisk.FullName; } + @{ Path = $stubVhdxDisk.FullName; } + @{ Path = $stubVhdDisk.FullName; } ); - State = 'Running'; + #State = 'Running'; Path = $StubVMConfig.FullPath; - Generation = 2; + Generation = 1; MemoryStartup = 512MB; MinimumMemory = 128MB; MaximumMemory = 4096MB; @@ -62,18 +65,17 @@ Describe 'xVMHyper-V' { Context 'Validates Get-TargetResource Method' { It 'Returns a hashtable' { - $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVMDisk.FullName; + $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.FullName; $targetResource -is [System.Collections.Hashtable] | Should Be $true; } It 'Throws when multiple VMs are present' { - { Get-TargetResource -Name 'DuplicateVM' -VhdPath $stubVMDisk.FullName } | Should Throw; + { Get-TargetResource -Name 'DuplicateVM' -VhdPath $stubVhdxDisk.FullName } | Should Throw; } } #end context Validates Get-TargetResource Method Context 'Validates Test-TargetResource Method' { $testParams = @{ - VhdPath = $stubVMDisk.FullName; - Generation = 'Vhdx'; + VhdPath = $stubVhdxDisk.FullName; } It 'Returns a boolean' { @@ -129,20 +131,47 @@ Describe 'xVMHyper-V' { Test-TargetResource -Name 'StoppedVM' -State Running @testParams | Should Be $false; } + It 'Returns $true when VM .vhd file is specified with a generation 1 VM' { + Test-TargetResource -Name 'StoppedVM' -VhdPath $stubVhdDisk -Generation 1 | Should Be $true; + } + + It 'Returns $true when VM .vhdx file is specified with a generation 1 VM' { + Test-TargetResource -Name 'StoppedVM' -VhdPath $stubVhdxDisk -Generation 1 | Should Be $true; + } + + It 'Returns $true when VM .vhdx file is specified with a generation 2 VM' { + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'Generation2VM' } -MockWith { + $generation2VM = $stubVM.Clone(); + $generation2VM['Generation'] = 2; + return [PSCustomObject] $generation2VM; + } + Test-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk -Generation 2 | Should Be $true; + } + + It 'Throws when a VM .vhd file is specified with a generation 2 VM' { + { Test-TargetResource -Name 'Gen2VM' -VhdPath $stubVhdDisk -Generation 2 } | Should Throw; + } + It 'Throws when Hyper-V Tools are not installed' { + ## This test needs to be the last in the Context otherwise all subsequent Get-Module checks will fail Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { } { Test-TargetResource -Name 'RunningVM' @testParams } | Should Throw; } + } #end context Validates Test-TargetResource Method Context 'Validates Set-TargetResource Method' { $testParams = @{ - VhdPath = $stubVMDisk.FullName; - Generation = 'Vhdx'; + VhdPath = $stubVhdxDisk.FullName; } Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NewVM' } -MockWith { } - Mock -CommandName New-VM -MockWith { $newVM = $stubVM.Clone(); $newVM['State'] = 'Off'; return $newVM; } + Mock -CommandName New-VM -MockWith { + $newVM = $stubVM.Clone(); + $newVM['State'] = 'Off'; + $newVM['Generation'] = $Generation; + return $newVM; + } Mock -CommandName Set-VM -MockWith { return $true; } Mock -CommandName Stop-VM -MockWith { return $true; } # requires output to be able to pipe something into Remove-VM Mock -CommandName Remove-VM -MockWith { return $true; } @@ -189,6 +218,21 @@ Describe 'xVMHyper-V' { Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; } + It 'Creates a generation 1 VM by default/when not explicitly specified' { + Set-TargetResource -Name 'NewVM' @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 1 } -Scope It; + } + + It 'Creates a generation 1 VM when explicitly specified' { + Set-TargetResource -Name 'NewVM' -Generation 1 @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 1 } -Scope It; + } + + It 'Creates a generation 2 VM when explicitly specified' { + Set-TargetResource -Name 'NewVM' -Generation 2 @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 2 } -Scope It; + } + It 'Throws when Hyper-V Tools are not installed' { Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { } { Set-TargetResource -Name 'RunningVM' @testParams } | Should Throw;