diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 index 39a4c48..93f62df 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 @@ -1,3 +1,34 @@ +# Fallback message strings in en-US +DATA localizedData +{ + # culture = "en-US" + ConvertFrom-StringData @' + RoleMissingError = Please ensure that '{0}' role is installed with its PowerShell module. + MoreThanOneVMExistsError = More than one VM with the name '{0}' exists. + PathDoesNotExistError = Path '{0}' does not exist. + VhdPathDoesNotExistError = Vhd '{0}' does not exist. + MinMemGreaterThanStartupMemError = MinimumMemory '{0}' should not be greater than StartupMemory '{1}' + MinMemGreaterThanMaxMemError = MinimumMemory '{0}' should not be greater than MaximumMemory '{1}' + StartUpMemGreaterThanMaxMemError = StartupMemory '{0}' should not be greater than MaximumMemory '{1}'. + VhdUnsupportedOnGen2VMError = Generation 2 virtual machines do not support the .VHD virtual disk extension. + CannotUpdatePropertiesOnlineError = Can not change properties for VM '{0}' in '{1}' state unless 'RestartIfNeeded' is set to true. + + AdjustingGreaterThanMemoryWarning = VM {0} '{1}' is greater than {2} '{3}'. Adjusting {0} to be '{3}'. + AdjustingLessThanMemoryWarning = VM {0} '{1}' is less than {2} '{3}'. Adjusting {0} to be '{3}'. + VMStateWillBeOffWarning = VM '{0}' state will be 'OFF' and not 'Paused'. + + CheckingVMExists = Checking if VM '{0}' exists ... + VMExists = VM '{0}' exists. + VMDoesNotExist = VM '{0}' does not exist. + CreatingVM = Creating VM '{0}' ... + VMCreated = VM '{0}' created. + VMPropertyShouldBe = VM property '{0}' should be '{1}', actual '{2}'. + VMPropertySet = VM property '{0}' is '{1}'. + VMPropertiesUpdated = VM '{0}' properties have been updated. + WaitingForVMIPAddress = Waiting for IP Address for VM '{0}' ... +'@ +} + function Get-TargetResource { [CmdletBinding()] @@ -16,7 +47,7 @@ function Get-TargetResource # Check if Hyper-V module is present for Hyper-V cmdlets if(!(Get-Module -ListAvailable -Name Hyper-V)) { - Throw "Please ensure that Hyper-V role is installed with its PowerShell module" + Throw ($localizedData.RoleMissingError -f 'Hyper-V') } $vmobj = Get-VM -Name $Name -ErrorAction SilentlyContinue @@ -24,9 +55,12 @@ function Get-TargetResource # Check if 1 or 0 VM with name = $name exist if($vmobj.count -gt 1) { - Throw "More than one VM with the name $Name exist." + Throw ($localizedData.MoreThanOneVMExistsError -f $Name) } + ## Retrieve the Vhd hierarchy to ensure we enumerate snapshots/differencing disks + ## Fixes #28 + $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path)) $vmSecureBootState = $false; if ($vmobj.Generation -eq 2) { @@ -36,7 +70,8 @@ function Get-TargetResource @{ Name = $Name - VhdPath = $vmObj.HardDrives[0].Path + ## Return the Vhd specified if it exists in the Vhd chain + VhdPath = if ($vhdChain -contains $VhdPath) { $VhdPath }; SwitchName = $vmObj.NetworkAdapters.SwitchName State = $vmobj.State Path = $vmobj.Path @@ -128,23 +163,23 @@ function Set-TargetResource # Check if Hyper-V module is present for Hyper-V cmdlets if(!(Get-Module -ListAvailable -Name Hyper-V)) { - Throw "Please ensure that Hyper-V role is installed with its PowerShell module" + Throw ($localizedData.RoleMissingError -f 'Hyper-V') } - Write-Verbose -Message "Checking if VM $Name exists ..." + Write-Verbose -Message ($localizedData.CheckingVMExists -f $Name) $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue # VM already exists if($vmObj) { - Write-Verbose -Message "VM $Name exists" + Write-Verbose -Message ($localizedData.VMExists -f $Name) # If VM shouldn't be there, stop it and remove it if($Ensure -eq "Absent") { - Write-Verbose -Message "VM $Name should be $Ensure" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'Ensure', $Ensure, 'Present') Get-VM $Name | Stop-VM -Force -Passthru -WarningAction SilentlyContinue | Remove-VM -Force - Write-Verbose -Message "VM $Name is $Ensure" + Write-Verbose -Message ($localizedData.VMPropertySet -f 'Ensure', $Ensure) } # If VM is present, check its state, startup memory, minimum memory, maximum memory,processor countand mac address @@ -154,26 +189,26 @@ function Set-TargetResource # 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)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'State', $State, $vmObj.State) Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP - Write-Verbose -Message "VM $Name is now $State" + Write-Verbose -Message ($localizedData.VMPropertySet -f 'State', $State) } $changeProperty = @{} # If the VM does not have the right startup memory if($StartupMemory -and ($vmObj.MemoryStartup -ne $StartupMemory)) { - Write-Verbose -Message "VM $Name does not have correct startup memory. Expected $StartupMemory, actual $($vmObj.MemoryStartup)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MemoryStartup', $StartupMemory, $vmObj.MemoryStartup) $changeProperty["MemoryStartup"]=$StartupMemory } elseif($MinimumMemory -and ($vmObj.MemoryStartup -lt $MinimumMemory)) { - Write-Verbose -Message "VM $Name has a startup memory $($vmObj.MemoryStartup) lesser than minimum memory $MinimumMemory. Setting startup memory to be equal to $MinimumMemory" + Write-Verbose -Message ($localizedData.AdjustingLessThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MinimumMemory', $MinimumMemory) $changeProperty["MemoryStartup"]=$MinimumMemory } elseif($MaximumMemory -and ($vmObj.MemoryStartup -gt $MaximumMemory)) { - Write-Verbose -Message "VM $Name has a startup memory $($vmObj.MemoryStartup) greater than maximum memory $MaximumMemory. Setting startup memory to be equal to $MaximumMemory" + Write-Verbose -Message ($localizedData.AdjustingGreaterThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MaximumMemory', $MaximumMemory) $changeProperty["MemoryStartup"]=$MaximumMemory } @@ -184,12 +219,12 @@ function Set-TargetResource if($MinimumMemory -and ($vmObj.Memoryminimum -ne $MinimumMemory)) { - Write-Verbose -Message "VM $Name does not have correct minimum memory. Expected $MinimumMemory, actual $($vmObj.MemoryMinimum)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MinimumMemory', $MinimumMemory, $vmObj.MemoryMinimum) $changeProperty["MemoryMinimum"]=$MinimumMemory } if($MaximumMemory -and ($vmObj.Memorymaximum -ne $MaximumMemory)) { - Write-Verbose -Message "VM $Name does not have correct maximum memory. Expected $MaximumMemory, actual $($vmObj.MemoryMaximum)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MaximumMemory', $MaximumMemory, $vmObj.MemoryMaximum) $changeProperty["MemoryMaximum"]=$MaximumMemory } } @@ -197,40 +232,41 @@ function Set-TargetResource # If the VM does not have the right processor count, stop the VM, set the right memory, start the VM if($ProcessorCount -and ($vmObj.ProcessorCount -ne $ProcessorCount)) { - Write-Verbose -Message "VM $Name does not have correct processor count. Expected $ProcessorCount, actual $($vmObj.ProcessorCount)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'ProcessorCount', $ProcessorCount, $vmObj.ProcessorCount) $changeProperty["ProcessorCount"]=$ProcessorCount } # 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 - Write-Verbose -Message "VM $Name updated" + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) } ## Set VM network switches. This can be done while the VM is running. for ($i = 0; $i -lt $SwitchName.Count; $i++) { + ### Change-VMProperty -Name $Name -VMCommand "Set-VMNetworkAdapter" -ChangeProperty @{StaticMacAddress=$MACAddress} -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded $switch = $SwitchName[$i] $nic = $vmObj.NetworkAdapters[$i] if ($nic) { - ## We cannot change the MAC address whilst the VM is running.. + ## We cannot change the MAC address whilst the VM is running.. This is changed later if ($nic.SwitchName -ne $switch) { - Write-Verbose -Message "VM $Name NIC $i is not connected to the correct switch. Expected $switch, actual $($nic.SwitchName)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'NIC', $switch, $nic.SwitchName) $nic | Connect-VMNetworkAdapter -SwitchName $switch - Write-Verbose -Message "VM $Name NIC $i connected to the $switch switch." + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) } } else { - Write-Verbose -Message "VM $Name NIC $i is not present. Expected $switch, actual " + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'NIC', $switch, '') if ($MACAddress -and (-not [System.String]::IsNullOrEmpty($MACAddress[$i]))) { Add-VMNetworkAdapter -VMName $Name -SwitchName $switch -StaticMacAddress $MACAddress[$i] - Write-Verbose -Message "VM $Name NIC $i added to the correct switch $switch with MAC address $($MACAddress[$i])." + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) } else { Add-VMNetworkAdapter -VMName $Name -SwitchName $switch - Write-Verbose -Message "VM $Name NIC $i added to the correct switch $switch." + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) } ## Refresh the NICs after we've added one $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue @@ -244,7 +280,7 @@ function Set-TargetResource $nic = $vmObj.NetworkAdapters[$i] if ($nic.MacAddress -ne $address) { - Write-Verbose -Message "VM $Name NIC $i does not have correct MACAddress. Expected $address, actual $($nic.MacAddress)" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MACAddress', $address, $nic.MacAddress) Change-VMMACAddress -Name $Name -NICIndex $i -MACAddress $address -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded } } @@ -255,10 +291,10 @@ function Set-TargetResource $vmSecureBoot = Test-VMSecureBoot -Name $Name if ($SecureBoot -ne $vmSecureBoot) { - Write-Verbose -Message "VM $Name secure boot is incorrect. Expected $SecureBoot, actual $vmSecureBoot" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot) ## Cannot change the secure boot state whilst the VM is powered on. Change-VMSecureBoot -Name $Name -SecureBoot $SecureBoot -RestartIfNeeded $RestartIfNeeded - Write-Verbose -Message "VM $Name secure boot is now correct." + Write-Verbose -Message ($localizedData.VMPropertySet -f 'SecureBoot', $SecureBoot) } } @@ -273,16 +309,17 @@ function Set-TargetResource #If the VM doesn't have Guest Service Interface correctly configured, update it. $GuestServiceStatus = $vmObj | Get-VMIntegrationService -Name 'Guest Service Interface' - if ($GuestServiceStatus.Enabled -eq $false -and $EnableGuestService) { - Write-Verbose -Message "VM $Name has Guest Service Interface disabled and should be enabled" + if ($GuestServiceStatus.Enabled -eq $false -and $EnableGuestService) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $GuestServiceStatus.Enabled) $vmObj | Enable-VMIntegrationService -Name 'Guest Service Interface' - Write-Verbose -Message "VM $Name Guest Service Interface configured correctly" + Write-Verbose -Message ($localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService) } elseif ($GuestServiceStatus.Enabled -and -not $EnableGuestService) { - Write-Verbose -Message "VM $Name has Guest Service Interface enabled and should be disabled" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $GuestServiceStatus.Enabled) $vmObj | Disable-VMIntegrationService -Name 'Guest Service Interface' - Write-Verbose -Message "VM $Name Guest Service Interface configured correctly" + Write-Verbose -Message ($localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService) } } } @@ -290,10 +327,10 @@ function Set-TargetResource # VM is not present, create one else { - Write-Verbose -Message "VM $Name does not exists" + Write-Verbose -Message ($localizedData.VMDoesNotExist -f $Name) if($Ensure -eq "Present") { - Write-Verbose -Message "Creating VM $Name ..." + Write-Verbose -Message ($localizedData.CreatingVM -f $Name) $parameters = @{} $parameters["Name"] = $Name @@ -350,8 +387,8 @@ function Set-TargetResource { $addVMNetworkAdapterParams['StaticMacAddress'] = $MACAddress[$i]; } - Write-Verbose -Message "Adding additional NIC to '$($SwitchName[$i])' switch" Add-VMNetworkAdapter @addVMNetworkAdapterParams + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $SwitchName[$i]) } if ($Generation -eq 2) { @@ -368,12 +405,12 @@ function Set-TargetResource Enable-VMIntegrationService -VMName $Name -Name 'Guest Service Interface' } - Write-Verbose -Message "VM $Name created" + Write-Verbose -Message ($localizedData.VMCreated -f $Name) if ($State) { Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP - Write-Verbose -Message "VM $Name is $State" + Write-Verbose -Message ($localizedData.VMPropertySet -f 'State', $State) } } @@ -450,68 +487,73 @@ function Test-TargetResource # Check if Hyper-V module is present for Hyper-V cmdlets if(!(Get-Module -ListAvailable -Name Hyper-V)) { - Throw "Please ensure that Hyper-V role is installed with its PowerShell module" + Throw ($localizedData.RoleMissingError -f 'Hyper-V') } # Check if 1 or 0 VM with name = $name exist if((Get-VM -Name $Name -ErrorAction SilentlyContinue).count -gt 1) { - Throw "More than one VM with the name $Name exist." + Throw ($localizedData.MoreThanOneVMExistsError -f $Name) } # Check if $VhdPath exist if(!(Test-Path $VhdPath)) { - #Throw "$VhdPath does not exists" + Throw ($localizedData.VhdPathDoesNotExistError -f $VhdPath) } # Check if Minimum memory is less than StartUpmemory if($StartupMemory -and $MinimumMemory -and ($MinimumMemory -gt $StartupMemory)) { - Throw "MinimumMemory($MinimumMemory) should not be greater than StartupMemory($StartupMemory)" + Throw ($localizedData.MinMemGreaterThanStartupMemError -f $MinimumMemory, $StartupMemory) } # Check if Minimum memory is greater than Maximummemory if($MaximumMemory -and $MinimumMemory -and ($MinimumMemory -gt $MaximumMemory)) { - Throw "MinimumMemory($MinimumMemory) should not be greater than MaximumMemory($MaximumMemory)" + Throw ($localizedData.MinMemGreaterThanMaxMemError -f $MinimumMemory, $MaximumMemory) } # Check if Startup memory is greater than Maximummemory if($MaximumMemory -and $StartupMemory -and ($StartupMemory -gt $MaximumMemory)) { - Throw "StartupMemory($StartupMemory) should not be greater than MaximumMemory($MaximumMemory)" + Throw ($localizedData.StartUpMemGreaterThanMaxMemError -f $StartupMemory, $MaximumMemory) } <# 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 2 virtual machines do not support the .VHD virtual disk extension." + Throw ($localizedData.VhdUnsupportedOnGen2VMError) } - # Check if $Path exist if($Path -and !(Test-Path -Path $Path)) { - Throw "$Path does not exists" + Throw ($localizedData.PathDoesNotExistError -f $Path) } #endregion - $result = $false - try { $vmObj = Get-VM -Name $Name -ErrorAction Stop if($Ensure -eq "Present") { + $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path)) + if($vhdChain -notcontains $VhdPath) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'VhdPath', $VhdPath, ($vhdChain -join ',')) + return $false + } + if($state -and ($vmObj.State -ne $State)){return $false} + if($StartupMemory -and ($vmObj.MemoryStartup -ne $StartupMemory)){return $false} if($vmObj.HardDrives.Path -notcontains $VhdPath){return $false} for ($i = 0; $i -lt $SwitchName.Count; $i++) { if ($vmObj.NetworkAdapters[$i].SwitchName -ne $SwitchName[$i]) { - Write-Verbose -Message "Network Adapter '$i' is not connected to the correct switch. Expected '$($SwitchName[$i])', actual '$($vmObj.NetworkAdapters[$i].SwitchName)'" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SwitchName', $SwitchName[$i], $vmObj.NetworkAdapters[$i].SwitchName) return $false } } @@ -521,17 +563,25 @@ function Test-TargetResource { if ($vmObj.NetworkAdapters[$i].MACAddress -ne $MACAddress[$i]) { - Write-Verbose -Message "Network Adapter '$i' MAC address is incorrect. Expected '$($MACAddress[$i])', actual '$($vmObj.NetworkAdapters[$i].MACAddress)'" + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MACAddress', $MACAddress[$i], $vmObj.NetworkAdapters[$i].MACAddress) return $false } } - if($Generation -ne $vmObj.Generation){return $false} + + ## $Generation always exists, only check if parameter has been explicitly specified + if($PSBoundParameters.ContainsKey('Generation') -and ($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} if($vmObj.Generation -eq 2) { - if ($SecureBoot -ne (Test-VMSecureBoot -Name $Name)){return $false} + $vmSecureBoot = Test-VMSecureBoot -Name $Name + if ($SecureBoot -ne $vmSecureBoot) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot) + return $false + } } if (($vmObj | Get-VMIntegrationService -Name 'Guest Service Interface').Enabled -ne $EnableGuestService) {return $false} return $true @@ -549,6 +599,23 @@ function Test-TargetResource #region Helper function +<# Returns VM VHDs, including snapshots and differencing disks #> +function Get-VhdHierarchy +{ + param( + [Parameter(Mandatory)] + [System.String] $VhdPath + ) + + $vmVhdPath = Get-VHD -Path $VhdPath + Write-Output -InputObject $vmVhdPath.Path + while($vmVhdPath.ParentPath -ne [String]::Empty) + { + $vmVhdPath.ParentPath + $vmVhdPath = (Get-VHD -Path $vmVhdPath.ParentPath) + } +} + function Set-VMState { param @@ -608,23 +675,21 @@ function Change-VMMACAddress { Set-VMState -Name $Name -State Running -WaitForIP $WaitForIP } - - Write-Verbose -Message "VM $Name NIC $NICIndex now has $MACAddress MACAddress." - + # Cannot make a paused VM to go back to Paused state after turning Off if($originalState -eq "Paused") { - Write-Warning -Message "VM $Name state will be OFF and not Paused" + Write-Warning -Message ($localizedData.VMStateWillBeOffWarning -f $Name) } } elseif($originalState -eq "Off") { $vmObj.NetworkAdapters[$NICIndex] | Set-VMNetworkAdapter -StaticMacAddress $MACAddress - Write-Verbose -Message "VM $Name NIC $NICIndex now has $MACAddress MACAddress." + Write-Verbose -Message ($localizedData.VMPropertySet -f 'MACAddress', $MACAddress) } else { - Write-Error -Message "Can not change properties for VM $Name in $($vmObj.State) state unless RestartIfNeeded is set to true" + Write-Error -Message ($localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmObj.State) } } @@ -659,22 +724,22 @@ function Change-VMProperty Set-VMState -Name $Name -State Running -WaitForIP $WaitForIP } - Write-Verbose -Message "VM $Name now has correct properties." + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) # Cannot make a paused VM to go back to Paused state after turning Off if($originalState -eq "Paused") { - Write-Warning -Message "VM $Name state will be OFF and not Paused" + Write-Warning -Message ($localizedData.VMStateWillBeOffWarning -f $Name) } } elseif($originalState -eq "Off") { &$VMCommand -Name $Name @ChangeProperty - Write-Verbose -Message "VM $Name now has correct properties." + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) } else { - Write-Error -Message "Can not change properties for VM $Name in $($vmObj.State) state unless RestartIfNeeded is set to true" + Write-Error -Message ($localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmObj.State) } } @@ -711,12 +776,12 @@ function Change-VMSecureBoot Set-VMState -Name $Name -State Running -WaitForIP $true } - Write-Verbose -Message "VM $Name now has correct properties." + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) # Cannot make a paused VM to go back to Paused state after turning Off if($originalState -eq "Paused") { - Write-Warning -Message "VM $Name state will be OFF and not Paused" + Write-Warning -Message ($localizedData.VMStateWillBeOffWarning -f $Name) } } elseif($originalState -eq "Off") @@ -731,7 +796,7 @@ function Change-VMSecureBoot } else { - Write-Error -Message "Can not change properties for VM $Name in $($vmObj.State) state unless RestartIfNeeded is set to true" + Write-Error -Message ($localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmObjState) } } @@ -755,7 +820,7 @@ function Get-VMIPAddress while((Get-VMNetworkAdapter -VMName $Name).ipaddresses.count -lt 2) { - Write-Verbose -Message "Waiting for IP Address for VM $Name ..." -Verbose + Write-Verbose -Message ($localizedData.WaitingForVMIPAddress -f $Name) Start-Sleep -Seconds 3; } } diff --git a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof index f7005dc..d76bd19 100644 --- a/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof +++ b/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof @@ -1,29 +1,29 @@ -[ClassVersion("1.0.0"), FriendlyName("xVMHyperV")] -class MSFT_xVMHyperV : OMI_BaseResource -{ - [Key, Description("Name of the VM")] String Name; - [Required, Description("VHD associated with the VM")] String VhdPath; - [Write, Description("Virtual switch(es) 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("Virtual machine generation")] Uint32 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; - [Write, Description("MAC address(es) of the VM NICs.")] String MACAddress[]; - [Write, Description("Processor count for the VM")] Uint32 ProcessorCount; - [Write, Description("Waits for VM to get valid IP address.")] Boolean WaitForIP; - [Write, Description("If specified, shutdowns and restarts the VM as needed for property changes")] Boolean RestartIfNeeded; - [Write, Description("Should the VM be created or deleted"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; - [Write, Description("Notes about the VM.")] String Notes; - [Write, Description("Enable secure boot for Generation 2 VMs.")] Boolean SecureBoot; - [Write, Description("Enable Guest Service Interface for the VM.")] Boolean EnableGuestService; - [Read, Description("VM unique ID")] String ID; - [Read, Description("Status of the VM")] String Status; - [Read, Description("CPU Usage of the VM")] Uint32 CPUUsage; - [Read, Description("Memory assigned to the VM")] Uint64 MemoryAssigned; - [Read, Description("Uptime of the VM")] String Uptime; - [Read, Description("Creation time of the VM")] DateTime CreationTime; - [Read, Description("Does VM has dynamic memory enabled")] Boolean HasDynamicMemory; - [Read, Description("Network adapters' IP addresses of the VM")] String NetworkAdapters[]; -}; +[ClassVersion("1.0.0"), FriendlyName("xVMHyperV")] +class MSFT_xVMHyperV : OMI_BaseResource +{ + [Key, Description("Name of the VM")] String Name; + [Required, Description("VHD associated with the VM")] String VhdPath; + [Write, Description("Virtual switch(es) 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("Virtual machine generation")] Uint32 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; + [Write, Description("MAC address(es) of the VM NICs.")] String MACAddress[]; + [Write, Description("Processor count for the VM")] Uint32 ProcessorCount; + [Write, Description("Waits for VM to get valid IP address.")] Boolean WaitForIP; + [Write, Description("If specified, shutdowns and restarts the VM as needed for property changes")] Boolean RestartIfNeeded; + [Write, Description("Should the VM be created or deleted"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Notes about the VM.")] String Notes; + [Write, Description("Enable secure boot for Generation 2 VMs.")] Boolean SecureBoot; + [Write, Description("Enable Guest Service Interface for the VM.")] Boolean EnableGuestService; + [Read, Description("VM unique ID")] String ID; + [Read, Description("Status of the VM")] String Status; + [Read, Description("CPU Usage of the VM")] Uint32 CPUUsage; + [Read, Description("Memory assigned to the VM")] Uint64 MemoryAssigned; + [Read, Description("Uptime of the VM")] String Uptime; + [Read, Description("Creation time of the VM")] DateTime CreationTime; + [Read, Description("Does VM has dynamic memory enabled")] Boolean HasDynamicMemory; + [Read, Description("Network adapters' IP addresses of the VM")] String NetworkAdapters[]; +}; diff --git a/README.md b/README.md index f22ff87..5f587e7 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ Please see the Examples section for more details. ### Unreleased +* MSFT_xVMHyperV: Fixed bug causing Test-TargetResource to fail when VM had snapshots. +* MSFT_xVMHyperV: Adds localization support. + ### 3.3.0.0 * xHyperV: Added SecureBoot parameter to enable control of the secure boot BIOS setting on generation 2 VMs. @@ -113,7 +116,7 @@ Please see the Examples section for more details. ### 2.1 -* Added logic to automatically adjust VM’s startup memory when only minimum and maximum memory is specified in configuration +* Added logic to automatically adjust VM's startup memory when only minimum and maximum memory is specified in configuration * Fixed the issue that a manually stopped VM cannot be brought back to running state with DSC ### 2.0 diff --git a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 index 78c24f5..8a75d6c 100644 --- a/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 +++ b/Tests/MSFT_xVMHyper-V/xVMHyper-V.Tests.ps1 @@ -36,41 +36,75 @@ Describe 'xVMHyper-V' { function Disable-VMIntegrationService { param ([Parameter(ValueFromPipeline)] $VM, $name)} $stubVhdxDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; + $studVhdxDiskSnapshot = New-Item -Path "TestDrive:\TestVM_D0145678-1576-4435-AB18-9F000C1C17D0.avhdx" -ItemType File; $stubVhdDisk = New-Item -Path 'TestDrive:\TestVM.vhd' -ItemType File; $StubVMConfig = New-Item -Path 'TestDrive:\TestVM.xml' -ItemType File; $stubNIC1 = @{ SwitchName = 'Test Switch 1'; MacAddress = 'AA-BB-CC-DD-EE-FF'; IpAddresses = @('192.168.0.1','10.0.0.1'); }; $stubNIC2 = @{ SwitchName = 'Test Switch 2'; MacAddress = 'AA-BB-CC-DD-EE-FE'; IpAddresses = @('192.168.1.1'); }; $stubVM = @{ - HardDrives = @( - @{ Path = $stubVhdxDisk.FullName; } - @{ Path = $stubVhdDisk.FullName; } - ); - #State = 'Running'; - Path = $StubVMConfig.FullPath; - Generation = 1; - SecureBoot = $true; - MemoryStartup = 512MB; - MinimumMemory = 128MB; - MaximumMemory = 4096MB; - ProcessorCount = 1; - ID = [System.Guid]::NewGuid().ToString(); - CPUUsage = 10; - MemoryAssigned = 512MB; - Uptime = New-TimeSpan -Hours 12; - CreationTime = (Get-Date).AddHours(-12); - DynamicMemoryEnabled = $true; + HardDrives = @( + @{ Path = $stubVhdxDisk.FullName; } + @{ Path = $stubVhdDisk.FullName; } + ); + #State = 'Running'; + Path = $StubVMConfig.FullPath; + Generation = 1; + SecureBoot = $true; + MemoryStartup = 512MB; + MinimumMemory = 128MB; + MaximumMemory = 4096MB; + ProcessorCount = 1; + ID = [System.Guid]::NewGuid().ToString(); + CPUUsage = 10; + MemoryAssigned = 512MB; + Uptime = New-TimeSpan -Hours 12; + CreationTime = (Get-Date).AddHours(-12); + DynamicMemoryEnabled = $true; NetworkAdapters = @($stubNIC1,$stubNIC2); 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-VM -ParameterFilter { $Name -eq 'Generation2VM' } -MockWith { $gen2VM = $stubVM.Clone(); $gen2VM['Generation'] = 2; return [PSCustomObject] $gen2VM; } + 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-VM -ParameterFilter { $Name -eq 'Generation1Vhd' } -MockWith { + $vhdVM = $stubVM.Clone(); + $vhdVM['HardDrives'] = @( @{ Path = $stubVhdDisk.FullName } ); + return [PSCustomObject] $vhdVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'Generation2VM' } -MockWith { + $gen2VM = $stubVM.Clone(); + $gen2VM['Generation'] = 2; + return [PSCustomObject] $gen2VM; + } Mock -CommandName Get-VMIntegrationService -MockWith {return [pscustomobject]@{Enabled=$false}} Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { return $true; } + Mock -CommandName Get-VhdHierarchy -ParameterFilter { $VhdPath.EndsWith('.vhd') } -MockWith { + ## Return single Vhd chain for .vhds + return @($stubVhdDisk.FullName); + } + Mock -CommandName Get-VhdHierarchy -ParameterFilter { $VhdPath.EndsWith('.vhdx') } -MockWith { + ## Return snapshot hierarchy for .vhdxs + return @($stubVhdxDiskSnapshot.FullName, $stubVhdxDisk.FullName); + } Context 'Validates Get-TargetResource Method' { @@ -99,7 +133,7 @@ Describe 'xVMHyper-V' { Context 'Validates Test-TargetResource Method' { $testParams = @{ - VhdPath = $stubVhdxDisk.FullName; + VhdPath = $stubVhdxDisk.FullName; } It 'Returns a boolean' { @@ -156,7 +190,7 @@ Describe 'xVMHyper-V' { } 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; + Test-TargetResource -Name 'Generation1Vhd' -VhdPath $stubVhdDisk -Generation 1 -Verbose | Should Be $true; } It 'Returns $true when VM .vhdx file is specified with a generation 1 VM' { @@ -184,7 +218,6 @@ Describe 'xVMHyper-V' { Test-TargetResource -Name 'RunningVM' @testParams -MACAddress @($stubNIC1.MACAddress,$stubNIC2.MACAddress) | Should Be $true; } - It 'Returns $false when multiple MAC addresses not assigned/assigned in the wrong order' { Test-TargetResource -Name 'RunningVM' @testParams -MACAddress @($stubNIC1.MACAddress,$stubNIC2.MACAddress) | Should Be $true; } @@ -200,8 +233,14 @@ Describe 'xVMHyper-V' { } It 'Returns $false when SecureBoot is On and requested "SecureBoot" = "$false"' { - Mock -CommandName Test-VMSecureBoot -MockWith { return $true ; } - Test-TargetResource -Name 'Generation2MV' -SecureBoot $false -Generation 2 @testParams | Should be $false; + Mock -CommandName Test-VMSecureBoot -MockWith { return $true; } + Test-TargetResource -Name 'Generation2VM' -SecureBoot $false -Generation 2 @testParams | Should be $false; + } + + It 'Returns $true when VM has snapshot chain' { + Mock -CommandName Get-VhdHierarchy -MockWith { Write-Host $VhdPath; return @($studVhdxDiskSnapshot, $stubVhdxDisk); } + + Test-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk -Verbose | Should Be $true; } It 'Returns $false when EnableGuestService is off and requested "EnableGuestService" = "$true"' { @@ -227,7 +266,7 @@ Describe 'xVMHyper-V' { Context 'Validates Set-TargetResource Method' { $testParams = @{ - VhdPath = $stubVhdxDisk.FullName; + VhdPath = $stubVhdxDisk.FullName; } Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NewVM' } -MockWith { }