From 87626f0dd5bbb77bd439c6ae55f92ee7ab87699d Mon Sep 17 00:00:00 2001 From: Iain Brighton Date: Thu, 14 May 2015 17:52:24 +0100 Subject: [PATCH 1/4] Fixes VM Power State Issue --- DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 index c0190ce..19988c7 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 @@ -69,8 +69,9 @@ function Set-TargetResource [String]$SwitchName, # State of the VM + [AllowNull()] [ValidateSet("Running","Paused","Off")] - [String]$State = "Off", + [String]$State, # Folder where the VM data will be stored [String]$Path, @@ -136,8 +137,8 @@ function Set-TargetResource # One cannot set the VM's vhdpath, path, generation and switchName after creation else { - # If the VM is not in right state, set it to right state - if($vmObj.State -ne $State) + # If state has been specified and the VM is not in right state, set it to right state + if($State -and $vmObj.State -ne $State) { Write-Verbose -Message "VM $Name is not $State. Expected $State, actual $($vmObj.State)" Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP @@ -280,8 +281,9 @@ function Test-TargetResource [String]$SwitchName, # State of the VM + [AllowNull()] [ValidateSet("Running","Paused","Off")] - [String]$State = "Off", + [String]$State, # Folder where the VM data will be stored [String]$Path, From d551fd9edb3891c24f84b4f28945c69ed7905c13 Mon Sep 17 00:00:00 2001 From: Iain Brighton Date: Tue, 19 May 2015 17:41:25 +0100 Subject: [PATCH 2/4] Added check for change in VM properties Added Pester tests --- .../MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 | 20 +- Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 | 189 ++++++++++++++++++ 2 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 index 19988c7..512749b 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 @@ -138,7 +138,7 @@ function Set-TargetResource else { # If state has been specified and the VM is not in right state, set it to right state - if($State -and $vmObj.State -ne $State) + if($State -and ($vmObj.State -ne $State)) { Write-Verbose -Message "VM $Name is not $State. Expected $State, actual $($vmObj.State)" Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP @@ -187,8 +187,10 @@ function Set-TargetResource $changeProperty["ProcessorCount"]=$ProcessorCount } - # Stop the VM, set the right properties, start the VM - Change-VMProperty -Name $Name -VMCommand "Set-VM" -ChangeProperty $changeProperty -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded + # Stop the VM, set the right properties, start the VM only if there are properties to change + if ($changeProperty.Count -gt 0) { + Change-VMProperty -Name $Name -VMCommand "Set-VM" -ChangeProperty $changeProperty -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded + } # If the VM does not have the right MACAddress, stop the VM, set the right MACAddress, start the VM if($MACAddress -and ($vmObj.NetWorkAdapters.MacAddress -notcontains $MACAddress)) @@ -256,9 +258,15 @@ function Set-TargetResource { Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $MACAddress } - - Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP - Write-Verbose -Message "VM $Name created and is $State" + + Write-Verbose -Message "VM $Name created" + + if ($State) + { + Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP + Write-Verbose -Message "VM $Name is $State" + } + } } } diff --git a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 new file mode 100644 index 0000000..b46bbd9 --- /dev/null +++ b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 @@ -0,0 +1,189 @@ +[CmdletBinding()] +param() + +if (!$PSScriptRoot) # $PSScriptRoot is not defined in 2.0 +{ + $PSScriptRoot = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +} + +$ErrorActionPreference = 'stop' +Set-StrictMode -Version latest + +$RepoRoot = (Resolve-Path $PSScriptRoot\..\..).Path + +$ModuleName = 'MSFT_xVMHyperV' +Import-Module (Join-Path $RepoRoot "DSCResources\$ModuleName\$ModuleName.psm1") -Force; + +Describe 'xVMHyper-V' { + InModuleScope $ModuleName { + $stubVMDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; + $StubVMConfig = New-Item -Path 'TestDrive:\TestVM.xml' -ItemType File; + $stubVM = @{ + HardDrives = @( + @{ Path = $stubVMDisk.FullName; } + ); + State = 'Running'; + Path = $StubVMConfig.FullPath; + Generation = 2; + MemoryStartup = 512MB; + MinimumMemory = 128MB; + MaximumMemory = 4096MB; + ProcessorCount = 1; + ID = [System.Guid]::NewGuid().ToString(); + #Status = 'Running'; + CPUUsage = 10; + MemoryAssigned = 512MB; + Uptime = New-TimeSpan -Hours 12; + CreationTime = (Get-Date).AddHours(-12); + DynamicMemoryEnabled = $true; + NetworkAdapters = @( + @{ SwitchName = 'TestSwitch'; MacAddress = 'AA-BB-CC-DD-EE-FF'; IpAddresses = @('192.168.0.1','10.0.0.1'); }; + ); + Notes = ''; + } + + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'RunningVM' } -MockWith { $runningVM = $stubVM.Clone(); $runningVM['State'] = 'Running'; return [PSCustomObject] $runningVM; } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'StoppedVM' } -MockWith { $stoppedVM = $stubVM.Clone(); $stoppedVM['State'] = 'Off'; return [PSCustomObject] $stoppedVM; } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'PausedVM' } -MockWith { $pausedVM = $stubVM.Clone(); $pausedVM['State'] = 'Paused'; return [PSCustomObject] $pausedVM; } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NonexistentVM' } -MockWith { Write-Error 'VM not found'; } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'DuplicateVM' } -MockWith { return @([PSCustomObject] $stubVM, [PSCustomObject] $stubVM); } + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { return $true; } + + Context 'Validates Get-TargetResource Method' { + + It 'Returns a hashtable' { + $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVMDisk.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; + } + } #end context Validates Get-TargetResource Method + + Context 'Validates Test-TargetResource Method' { + $testParams = @{ + VhdPath = $stubVMDisk.FullName; + Generation = 'Vhdx'; + } + + It 'Returns a boolean' { + $targetResource = Test-TargetResource -Name 'RunningVM' @testParams; + $targetResource -is [System.Boolean] | Should Be $true; + } + + It 'Returns $true when VM is present and "Ensure" = "Present"' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $false when VM is not present and "Ensure" = "Present"' { + Test-TargetResource -Name 'NonexistentVM' @testParams | Should Be $false; + } + + It 'Returns $true when VM is not present and "Ensure" = "Absent"' { + Test-TargetResource -Name 'NonexistentVM' -Ensure Absent @testParams | Should Be $true; + } + + It 'Returns $false when VM is present and "Ensure" = "Absent"' { + Test-TargetResource -Name 'RunningVM' -Ensure Absent @testParams | Should Be $false; + } + + It 'Returns $true when VM is in the "Running" state and no state is explicitly specified' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Stopped" state and no state is explicitly specified' { + Test-TargetResource -Name 'StoppedVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Paused" state and no state is explicitly specified' { + Test-TargetResource -Name 'PausedVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Running" state and requested "State" = "Running"' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Off" state and requested "State" = "Off"' { + Test-TargetResource -Name 'StoppedVM' -State Off @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Paused" state and requested "State" = Paused"' { + Test-TargetResource -Name 'PausedVM' -State Paused @testParams | Should Be $true; + } + + It 'Returns $false when VM is in the "Running" state and requested "State" = "Off"' { + Test-TargetResource -Name 'RunningVM' -State Off @testParams | Should Be $false; + } + + It 'Returns $false when VM is in the "Off" state and requested "State" = "Runnning"' { + Test-TargetResource -Name 'StoppedVM' -State Running @testParams | Should Be $false; + } + + It 'Throws when Hyper-V Tools are not installed' { + 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'; + } + + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NewVM' } -MockWith { } + Mock -CommandName New-VM -MockWith { $newVM = $stubVM.Clone(); $newVM['State'] = 'Off'; return $newVM; } + Mock -CommandName Set-VM -MockWith { return $true; } + Mock -CommandName Stop-VM -MockWith { return $true; } # requires output to be piped to Remove-VM + Mock -CommandName Remove-VM -MockWith { return $true; } + Mock -CommandName Set-VMNetworkAdapter -MockWith { return $true; } + Mock -CommandName Get-VMNetworkAdapter -MockWith { return $stubVM.NetworkAdapters.IpAddresses; } + Mock -CommandName Set-VMState -MockWith { return $true; } + + It 'Removes an existing VM when "Ensure" = "Absent"' { + Set-TargetResource -Name 'RunningVM' -Ensure Absent @testParams; + Assert-MockCalled -CommandName Remove-VM -Scope It; + } + + It 'Creates and does not start a VM that does not exist when "Ensure" = "Present"' { + Set-TargetResource -Name 'NewVM' @testParams; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Creates and starts a VM that does not exist when "Ensure" = "Present" and "State" = "Running"' { + #Mock -CommandName Change-VMProperty -MockWith { } + Set-TargetResource -Name 'NewVM' -State Running @testParams; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; + } + + It 'Does not change VM state when VM "State" = "Running" and requested "State" = "Running"' { + Set-TargetResource -Name 'RunningVM' -State Running @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Does not change VM state when VM "State" = "Off" and requested "State" = "Off"' { + Set-TargetResource -Name 'StoppedVM' -State Off @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Changes VM state when existing VM "State" = "Off" and requested "State" = "Running"' { + Set-TargetResource -Name 'StoppedVM' -State Running @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; + } + + It 'Changes VM state when existing VM "State" = "Running" and requested "State" = "Off"' { + Set-TargetResource -Name 'RunningVM' -State Off @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -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; + } + } #end context Validates Set-TargetResource Method + } #end inmodulescope +} #end describe xVMHyper-V From ac2a16cdd10f6301975adb26c2240148a5e9d607 Mon Sep 17 00:00:00 2001 From: Iain Brighton Date: Tue, 19 May 2015 18:26:56 +0100 Subject: [PATCH 3/4] Added empty functions to enable mocking of the Hyper-V cmdlets --- Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 index b46bbd9..0c67c1d 100644 --- a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 +++ b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 @@ -14,6 +14,13 @@ $RepoRoot = (Resolve-Path $PSScriptRoot\..\..).Path $ModuleName = 'MSFT_xVMHyperV' Import-Module (Join-Path $RepoRoot "DSCResources\$ModuleName\$ModuleName.psm1") -Force; +## Create empty functions to be able to mock the Hyper-V cmdlets +function Get-VM { } +function New-VM { } +function Set-VM { } +function Stop-VM { } +function Remove-VM { } + Describe 'xVMHyper-V' { InModuleScope $ModuleName { $stubVMDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; @@ -153,7 +160,6 @@ Describe 'xVMHyper-V' { } It 'Creates and starts a VM that does not exist when "Ensure" = "Present" and "State" = "Running"' { - #Mock -CommandName Change-VMProperty -MockWith { } Set-TargetResource -Name 'NewVM' -State Running @testParams; Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It; From cee8167d83c4c05b8f16cb8fc8a0dc1537e6c639 Mon Sep 17 00:00:00 2001 From: iainbrighton Date: Tue, 19 May 2015 20:53:51 +0100 Subject: [PATCH 4/4] Moved empty functions to within the 'InModuleScope' Added [CmdletBinding()] attribute to the empty Get-VM function to enable -ErrorAction functionality --- Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 index 0c67c1d..86feb1b 100644 --- a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 +++ b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 @@ -14,15 +14,19 @@ $RepoRoot = (Resolve-Path $PSScriptRoot\..\..).Path $ModuleName = 'MSFT_xVMHyperV' Import-Module (Join-Path $RepoRoot "DSCResources\$ModuleName\$ModuleName.psm1") -Force; -## Create empty functions to be able to mock the Hyper-V cmdlets -function Get-VM { } -function New-VM { } -function Set-VM { } -function Stop-VM { } -function Remove-VM { } - Describe 'xVMHyper-V' { InModuleScope $ModuleName { + + ## 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 { } + function Set-VM { } + function Stop-VM { } + function Remove-VM { } + function Get-VMNetworkAdapter { } + function Set-VMNetworkAdapter { } + $stubVMDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; $StubVMConfig = New-Item -Path 'TestDrive:\TestVM.xml' -ItemType File; $stubVM = @{ @@ -37,7 +41,6 @@ Describe 'xVMHyper-V' { MaximumMemory = 4096MB; ProcessorCount = 1; ID = [System.Guid]::NewGuid().ToString(); - #Status = 'Running'; CPUUsage = 10; MemoryAssigned = 512MB; Uptime = New-TimeSpan -Hours 12; @@ -52,7 +55,7 @@ Describe 'xVMHyper-V' { Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'RunningVM' } -MockWith { $runningVM = $stubVM.Clone(); $runningVM['State'] = 'Running'; return [PSCustomObject] $runningVM; } Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'StoppedVM' } -MockWith { $stoppedVM = $stubVM.Clone(); $stoppedVM['State'] = 'Off'; return [PSCustomObject] $stoppedVM; } Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'PausedVM' } -MockWith { $pausedVM = $stubVM.Clone(); $pausedVM['State'] = 'Paused'; return [PSCustomObject] $pausedVM; } - Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NonexistentVM' } -MockWith { Write-Error 'VM not found'; } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NonexistentVM' } -MockWith { Write-Error 'VM not found.'; } Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'DuplicateVM' } -MockWith { return @([PSCustomObject] $stubVM, [PSCustomObject] $stubVM); } Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { return $true; } @@ -141,7 +144,7 @@ Describe 'xVMHyper-V' { Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NewVM' } -MockWith { } Mock -CommandName New-VM -MockWith { $newVM = $stubVM.Clone(); $newVM['State'] = 'Off'; return $newVM; } Mock -CommandName Set-VM -MockWith { return $true; } - Mock -CommandName Stop-VM -MockWith { return $true; } # requires output to be piped to Remove-VM + 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; } Mock -CommandName Set-VMNetworkAdapter -MockWith { return $true; } Mock -CommandName Get-VMNetworkAdapter -MockWith { return $stubVM.NetworkAdapters.IpAddresses; }