Src/Public/Public.ps1
function Checkpoint-Lab { <# .SYNOPSIS Snapshots all lab VMs in their current configuration. .DESCRIPTION The Checkpoint-Lab creates a VM checkpoint of all the nodes defined in a PowerShell DSC configuration document. When creating the snapshots, they will be created using the snapshot name specified. All virtual machines should be powered off when the snapshot is taken to ensure that the machine is in a consistent state. If VMs are powered on, an error will be generated. You can override this behaviour by specifying the -Force parameter. WARNING: If the -Force parameter is used, the virtual machine snapshot(s) may be in an inconsistent state. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document. .PARAMETER SnapshotName Specifies the virtual machine snapshot name that applied to each VM in the PowerShell DSC configuration document. This name is used to restore a lab configuration. It can contain spaces, but is not recommended. .PARAMETER Force Forces virtual machine snapshots to be taken - even if there are any running virtual machines. .LINK Restore-Lab Reset-Lab #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Snapshot name [Parameter(Mandatory)] [Alias('Name')] [System.String] $SnapshotName, ## Force snapshots if virtual machines are on [System.Management.Automation.SwitchParameter] $Force ) process { $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData; }; $runningNodes = Hyper-V\Get-VM -Name $nodes.NodeDisplayName | Where-Object { $_.State -ne 'Off' } if ($runningNodes -and $Force) { New-LabVMSnapshot -Name $nodes.NodeDisplayName -SnapshotName $SnapshotName; } elseif ($runningNodes) { foreach ($runningNode in $runningNodes) { Write-Error -Message ($localized.CannotSnapshotNodeError -f $runningNode.Name); } } else { $nodesDisplayString = [System.String]::Join(', ', $nodes.NodeDisplayName); $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Checkpoint-Lab', $node.Name; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVirtualMachineSnapshot -f $nodesDisplayString, $SnapshotName); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { New-LabVMSnapshot -Name $nodes.NodeDisplayName -SnapshotName $SnapshotName; } } } #end process } #end function Checkpoint-Lab function Clear-LabModuleCache { <# .SYNOPSIS Removes all cached modules from the Lability module cache. .DESCRIPTION The Clear-LabModuleCache removes all cached PowerShell module and DSC resource modules stored in Lability's internal cache. .PARAMETER Force Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or read-only aliases or variables. .EXAMPLE Clear-LabModuleCache -Force Removes all previously downloaded/cached PowerShell modules and DSC resources. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param ( [Parameter(ValueFromPipeline)] [System.Management.Automation.SwitchParameter] $Force ) process { $moduleCachePath = (Get-ConfigurationData -Configuration 'Host').ModuleCachePath; $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Clear-LabModuleCache', $moduleCachePath; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingDirectory -f $moduleCachePath); $shouldProcessWarning = $localized.ShouldProcessWarning; if (($Force) -or ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $shouldProcessWarning))) { Remove-Item -Path $moduleCachePath -Recurse -Force:$Force; $moduleCachePathParent = Split-Path -Path $moduleCachePath -Parent; $moduleCachePathName = Split-Path -Path $moduleCachePath -Leaf; $newItemParams = @{ Path = $moduleCachePathParent; Name = $moduleCachePathName; ItemType = 'Directory'; Force = $true; Confirm = $false; } [ref] $null = New-Item @newItemParams; } } #end process } #end function function Clear-ModulePath { <# .SYNOPSIS Removes all PowerShell modules installed in a given scope. .DESCRIPTION The Clear-ModulePath removes all existing PowerShell module and DSC resources from either the current user's or the local machine's module path. .PARAMETER Scope Specifies the scope to install module(s) in to. The default value is 'CurrentUser'. .PARAMETER Force Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or read-only aliases or variables. .EXAMPLE Clear-ModulePath -Scope CurrentUser Removes all PowerShell modules and DSC resources from the current user's module path. .EXAMPLE Clear-ModulePath -Scope AllUsers -Force Removes all PowerShell modules and DSC resources from the local machine's module path. .NOTES USE WITH CAUTION! Not sure thsi should even be in this module? #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('AllUsers','CurrentUser')] [System.String] $Scope = 'CurrentUser', [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) process { if ($Scope -eq 'AllUsers') { $localizedProgramFiles = Resolve-ProgramFilesFolder -Path $env:SystemRoot; $modulePath = Join-Path -Path $localizedProgramFiles -ChildPath 'WindowsPowerShell\Modules'; } elseif ($Scope -eq 'CurrentUser') { $userDocuments = [System.Environment]::GetFolderPath('MyDocuments'); $modulePath = Join-Path -Path $userDocuments -ChildPath 'WindowsPowerShell\Modules'; } if (Test-Path -Path $modulePath) { $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Clear-ModulePath', $moduleCachePath; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingDirectory -f $moduleCachePath); $shouldProcessWarning = $localized.ShouldProcessWarning; if (($Force) -or ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $shouldProcessWarning))) { ## The -Force on Remove-Item supresses the confirmation :() Remove-Item -Path $modulePath -Recurse -Force:$Force; } } else { Write-Verbose -Message ($localized.PathDoesNotExist -f $modulePath); } } #end process } #end function function Export-LabHostConfiguration { <# .SYNOPSIS Backs up the current lab host configuration. .LINK Import-LabHostConfiguration #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')] [OutputType([System.IO.FileInfo])] param ( # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias("PSPath")] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $LiteralPath, ## Do not overwrite an existing file [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoClobber ) process { $now = [System.DateTime]::UtcNow; $configuration = [PSCustomObject] @{ Author = $env:USERNAME; GenerationHost = $env:COMPUTERNAME; GenerationDate = '{0} {1}' -f $now.ToShortDateString(), $now.ToString('hh:mm:ss'); ModuleVersion = (Get-Module -Name $labDefaults.ModuleName).Version.ToString(); HostDefaults = [PSCustomObject] (Get-ConfigurationData -Configuration Host); VMDefaults = [PSCustomObject] (Get-ConfigurationData -Configuration VM); CustomMedia = @([PSCustomObject] (Get-ConfigurationData -Configuration CustomMedia)); } if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } if ($NoClobber -and (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) { $errorMessage = $localized.FileAlreadyExistsError -f $Path; $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage; $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists; $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileExists', $errorCategory, $Path; $PSCmdlet.WriteError($errorRecord); } else { $verboseMessage = Get-FormattedMessage -Message ($localized.ExportingConfiguration -f $labDefaults.ModuleName, $Path); $operationMessage = $localized.ShouldProcessOperation -f 'Export', $Path; $setContentParams = @{ Path = $Path; Value = ConvertTo-Json -InputObject $configuration -Depth 5; Force = $true; Confirm = $false; } if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { try { ## Set-Content won't actually throw a terminating error?! Set-Content @setContentParams -ErrorAction Stop; Write-Output -InputObject (Get-Item -Path $Path); } catch { throw $_; } } } } #end process } #end function function Export-LabImage { <# .SYNOPSIS Exports a lab image (.vhdx file) and creates Lability custom media registration document (.json file). #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms','')] param ( ## Lab media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $Id, # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias("PSPath")] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $LiteralPath, ## Force the re(creation) of the master/parent image [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## Do not calculate media checksum. [Parameter()] [System.Management.Automation.SwitchParameter] $NoChecksum, ## Do not create Lability custom media regstration (json) file. [Parameter()] [System.Management.Automation.SwitchParameter] $NoRegistrationDocument ) begin { try { if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } $pathItem = Get-Item -Path $Path if ($pathItem -isnot [System.IO.DirectoryInfo]) { throw "not a directory path" } } catch { Write-Error -ErrorRecord $_ } } process { foreach ($mediaId in $Id) { try { $media = Resolve-LabMedia -Id $mediaId -ErrorAction Stop $image = Get-LabImage -Id $mediaId -ErrorAction Stop ## Copy vhd/x file to destination path $imageItem = Get-Item -Path $image.ImagePath $destinationVhdxPath = Join-Path -Path $Path -ChildPath $imageItem.Name if (-not (Test-Path -Path $destinationVhdxPath -PathType Leaf) -or $Force) { Write-Verbose -Message ($localized.ExportingImage -f $image.ImagePath, $destinationVhdxPath) Copy-Item -Path $image.ImagePath -Destination $destinationVhdxPath -Force if (-not $NoRegistrationDocument) { ## Create media registration json $registerLabMedia = [ordered] @{ Id = $media.Id Filename = $imageItem.Name Description = $media.Description OperatingSystem = $media.OperatingSystem Architecture = $media.Architecture MediaType = 'VHD' Uri = $destinationVhdxPath -as [System.Uri] CustomData = $media.CustomData } if (-not $NoChecksum) { Write-Verbose ($localized.CalculatingResourceChecksum -f $image.ImagePath) $checksum = (Get-FileHash -Path $image.ImagePath -Algorithm MD5).Hash $registerLabMedia['Checksum'] = $checksum } $registerLabMediaFilename = '{0}.json' -f $media.Id $registerLabMediaPath = Join-Path -Path $Path -ChildPath $registerLabMediaFilename Write-Verbose -Message ($localized.ExportingImageRegistrationFile -f $registerLabMediaPath) ConvertTo-Json -InputObject $registerLabMedia | Out-File -FilePath $registerLabMediaPath -Encoding ASCII -Force } } else { $errorMessage = $localized.FileAlreadyExistsError -f $destinationVhdxPath; $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage; $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists; $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileExists', $errorCategory, $destinationVhdxPath; $PSCmdlet.WriteError($errorRecord); } } catch { Write-Error -ErrorRecord $_ } } } } function Get-LabHostConfiguration { <# .SYNOPSIS Retrieves the current lab host's configuration default values. .LINK Test-LabHostConfiguration Start-LabHostConfiguration #> [CmdletBinding()] [OutputType([System.Management.Automation.PSObject])] param ( ) process { $labHostSetupConfiguation = Get-LabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguation) { $importDscResourceParams = @{ ModuleName = $configuration.ModuleName; ResourceName = $configuration.ResourceName; Prefix = $configuration.Prefix; UseDefault = $configuration.UseDefault; } Import-LabDscResource @importDscResourceParams; $resource = Get-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters; $resource['Resource'] = $configuration.ResourceName; Write-Output -InputObject ([PSCustomObject] $resource); } } #end process } #end function function Get-LabHostDefault { <# .SYNOPSIS Gets the lab host's default settings. .DESCRIPTION The Get-LabHostDefault cmdlet returns the lab host's current settings. .LINK Set-LabHostDefault Reset-LabHostDefault #> [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; ## Create/update Lability environment variables $env:LabilityConfigurationPath = $hostDefaults.ConfigurationPath; $env:LabilityDifferencingVhdPath = $hostDefaults.DifferencingVhdPath; $env:LabilityHotfixPath = $hostDefaults.HotfixPath; $env:LabilityIsoPath = $hostDefaults.IsoPath; $env:LabilityModuleCachePath = $hostDefaults.ModuleCachePath; $env:LabilityResourcePath = $hostDefaults.ResourcePath; $env:LabilityDismPath = $hostDefaults.DismPath; $env:LabilityRepositoryUri = $hostDefaults.RepositoryUri; $env:LabilityParentVhdPath = $hostDefaults.ParentVhdPath; return $hostDefaults; } #end process } #end function function Get-LabImage { <# .SYNOPSIS Gets master/parent disk image. .DESCRIPTION The Get-LabImage cmdlet returns current master/parent disk image properties. .PARAMETER Id Specifies the media Id of the image to return. If this parameter is not specified, all images are returned. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document that contains the required media definition. .EXAMPLE Get-LabImage Returns all current lab images on the host. .EXAMPLE Get-LabImage -Id 2016_x64_Datacenter_EN_Eval Returns the '2016_x64_Datacenter_EN_Eval' lab image properties, if available. #> [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Id, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $parentVhdPath = Resolve-PathEx -Path $hostDefaults.ParentVhdPath; if ($PSBoundParameters.ContainsKey('Id')) { ## We have an Id. so resolve that try { $labMedia = Resolve-LabMedia @PSBoundParameters; } catch { $labMedia = $null; } } else { ## Otherwise return all media $labMedia = Get-LabMedia; } foreach ($media in $labMedia) { $differencingVhdPath = '{0}.vhdx' -f $media.Id; if ($media.MediaType -eq 'VHD') { $differencingVhdPath = $media.Filename; } $imagePath = Join-Path -Path $parentVhdPath -ChildPath $differencingVhdPath; if (Test-Path -Path $imagePath -PathType Leaf) { $imageFileInfo = Get-Item -Path $imagePath; $diskImage = Storage\Get-DiskImage -ImagePath $imageFileInfo.FullName; $labImage = [PSCustomObject] @{ Id = $media.Id; Attached = $diskImage.Attached; ImagePath = $diskImage.ImagePath; LogicalSectorSize = $diskImage.LogicalSectorSize; BlockSize = $diskImage.BlockSize; FileSize = $diskImage.FileSize; Size = $diskImage.Size; Generation = ($imagePath.Split('.')[-1]).ToUpper(); } $labImage.PSObject.TypeNames.Insert(0, 'VirtualEngine.Lability.Image'); Write-Output -InputObject $labImage; } } #end foreach media } #end process } #end function Get-LabImage function Get-LabMedia { <# .SYNOPSIS Gets registered lab media. .DESCRIPTION The Get-LabMedia cmdlet retrieves all built-in and registered custom media. .PARAMETER Id Specifies the specific media Id to return. .PARAMETER CustomOnly Specifies that only registered custom media are returned. .PARAMETER Legacy Specifies that legacy evaluation media definition(s) are returned. These deprecated media definitions can be reregistered using the Register-LabMedia cmdlet with the -Legacy switch. .EXAMPLE Get-LabMedia -Legacy Id Arch Media Description -- ---- ----- ----------- WIN10_x64_Enterprise_1709_EN_Eval x64 ISO Windows 10 64bit Enterprise 1709 English Evaluation WIN10_x64_Enterprise_1803_EN_Eval x64 ISO Windows 10 64bit Enterprise 1804 English Evaluation WIN10_x64_Enterprise_1809_EN_Eval x64 ISO Windows 10 64bit Enterprise 1809 English Evaluation WIN10_x64_Enterprise_1903_EN_Eval x64 ISO Windows 10 64bit Enterprise 1903 English Evaluation WIN10_x64_Enterprise_LTSB_2016_EN_Eval x64 ISO Windows 10 64bit Enterprise LTSB 2016 English Evaluation WIN10_x86_Enterprise_1709_EN_Eval x86 ISO Windows 10 32bit Enterprise 1709 English Evaluation WIN10_x86_Enterprise_1803_EN_Eval x86 ISO Windows 10 32bit Enterprise 1804 English Evaluation WIN10_x86_Enterprise_1809_EN_Eval x86 ISO Windows 10 32bit Enterprise 1809 English Evaluation WIN10_x86_Enterprise_1903_EN_Eval x86 ISO Windows 10 32bit Enterprise 1903 English Evaluation WIN10_x86_Enterprise_LTSB_2016_EN_Eval x86 ISO Windows 10 32bit Enterprise LTSB 2016 English Evaluation Returns deprecated/previous media definitions. #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( ## Media ID or alias to return [Parameter(ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $Id, ## Only return custom media [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Default')] [System.Management.Automation.SwitchParameter] $CustomOnly, ## Only return legacy media [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Legacy')] [System.Management.Automation.SwitchParameter] $Legacy ) process { if ($PSBoundParameters.ContainsKey('Legacy')) { $legacyMediaPath = Resolve-ConfigurationDataPath -Configuration LegacyMedia -IsDefaultPath $media = Get-ChildItem -Path $legacyMediaPath | ForEach-Object { Get-Content -Path $_.FullName | ConvertFrom-Json } if ($Id) { $media = $media | Where-Object { $_.Id -eq $Id }; } } else { ## Retrieve built-in media if (-not $CustomOnly) { $defaultMedia = Get-ConfigurationData -Configuration Media; } ## Retrieve custom media $customMedia = @(Get-ConfigurationData -Configuration CustomMedia); if (-not $customMedia) { $customMedia = @(); } ## Are we looking for a specific media Id if ($Id) { ## Return the custom media definition first (if it exists) $media = $customMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id }; if ((-not $media) -and (-not $CustomOnly)) { ## We didn't find a custom media entry, return a default entry (if it exists) $media = $defaultMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id }; } } else { ## Return all media $media = $customMedia; if (-not $CustomOnly) { foreach ($mediaEntry in $defaultMedia) { ## Determine whether the media is present in the custom media, i.e. make sure ## we don't override a custom entry with the default one. $defaultMediaEntry = $customMedia | Where-Object { $_.Id -eq $mediaEntry.Id } ## If not, add it to the media array to return if (-not $defaultMediaEntry) { $media += $mediaEntry; } } #end foreach default media } #end if not custom only } } foreach ($mediaObject in $media) { $mediaObject.PSObject.TypeNames.Insert(0, 'VirtualEngine.Lability.Media'); Write-Output -InputObject $mediaObject; } } #end process } #end function function Get-LabStatus { <# .SYNOPSIS Queries computers' LCM state to determine whether an existing DSC configuration has applied. .EXAMPLE Get-LabStatus -ComputerName CONTROLLER, XENAPP Queries the CONTROLLER and XENAPP computers' LCM state using the current user credential. .EXAMPLE Get-LabStatus -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential) Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query the LCM state. .EXAMPLE Get-LabStatus -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential) Prompts for credentials to connect to the computers defined in the DSC configuration document (.psd1) and query the LCM state. .EXAMPLE Get-LabStatus -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential) Prompts for credentials to connect to the computers by their IPAddress node property as defined in the DSC configuration document (.psd1) and query the LCM state. #> [CmdletBinding()] param ( ## Connect to the computer name(s) specified. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')] [System.String[]] $ComputerName, ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [System.String] $PreferNodeProperty, ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN. ## The complete identifier for the remote endpoint is in the following format: ## ## <transport>://<server>:<port>/<ApplicationName> ## ## For example: `http://server01:8080/WSMAN` ## ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell. ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ApplicationName, ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are: ## ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy. ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. - ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. - ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. - ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP) ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one ## remote computer but collect data from or run additional commands on other remote computers. ## ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials ## are passed to it, the credentials can be used to control the network session. ## ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote ## computer anonymously, without using authentication. If the request is made anonymously, it returns no ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0). [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')] [System.String] $Authentication = 'Default', ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this ## action. Enter the certificate thumbprint of the certificate. ## ## Certificates are used in client certificate-based authentication. They can be mapped only to local user ## accounts; they do not work with domain accounts. ## ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert: ## drive. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $CertificateThumbprint, ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the ## default port is 80. When the transport is HTTPS, the default port is 443. ## ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's ## certificate common name (CN). [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $Port, ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote ## computer. By default, SSL is not used. ## ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the ## port that is used for the connection, and you specify this parameter, the command fails. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseSSL, ## Credential used to connect to the remote computer. [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential ) begin { ## Authentication might not be explicitly passed, add it so it gets splatted $PSBoundParameters['Authentication'] = $Authentication; } process { if ($PSCmdlet.ParameterSetName -eq 'ConfigurationData') { $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' }; foreach ($node in $nodes) { $nodeName = $node.NodeName; if (($PSBoundParameters.ContainsKey('PreferNodeProperty')) -and (-not [System.String]::IsNullOrEmpty($node[$PreferNodeProperty]))) { $nodeName = $node[$PreferNodeProperty]; } $ComputerName += $nodeName; } } $sessions = Get-PSSession; $activeSessions = @(); $inactiveSessions = @(); ## Remove parameters that aren't supported by Get-PSSession, Test-WSMan and New-PSSession [ref] $null = $PSBoundParameters.Remove('ComputerName'); [ref] $null = $PSBoundParameters.Remove('ConfigurationData'); [ref] $null = $PSBoundParameters.Remove('PreferNodeProperty'); [ref] $null = $PSBoundParameters.Remove('ErrorAction'); foreach ($computer in $ComputerName) { $session = $sessions | Where-Object { $_.ComputerName -eq $computer -and $_.State -eq 'Opened' } | Select-Object -First 1; if (-not $session) { Write-Verbose -Message ($localized.TestingWinRMConnection -f $computer); try { if (Test-WSMan -ComputerName $computer -ErrorAction Stop @PSBoundParameters) { ## WSMan is up so we should be able to connect, if not throw.. Write-Verbose -Message ($localized.ConnectingRemoteSession -f $computer); $activeSessions += New-PSSession -ComputerName $computer -ErrorAction Stop @PSBoundParameters; } } catch { $inactiveSessions += $computer; Write-Error $_; } } else { Write-Verbose -Message ($localized.ReusingExistingRemoteSession -f $computer); $activeSessions += $session } } #end foreach computer if ($activeSessions.Count -gt 0) { Write-Verbose -Message ($localized.QueryingActiveSessions -f ($activeSessions.ComputerName -join "','")); $results = Invoke-Command -Session $activeSessions -ScriptBlock { Get-DscLocalConfigurationManager | Select-Object -Property LCMVersion, LCMState; }; } foreach ($computer in $ComputerName) { if ($computer -in $inactiveSessions) { $labState = [PSCustomObject] @{ ComputerName = $computer; LCMVersion = ''; LCMState = 'Unknown'; Completed = $false; } Write-Output -InputObject $labState; } else { $result = $results | Where-Object { $_.PSComputerName -eq $computer }; $labState = [PSCustomObject] @{ ComputerName = $result.PSComputerName; LCMVersion = $result.LCMVersion; LCMState = $result.LCMState; Completed = $result.LCMState -eq 'Idle'; } Write-Output -InputObject $labState; } } #end foreach computer } #end process } #end function function Get-LabVM { <# .SYNOPSIS Retrieves the current configuration of a VM. .DESCRIPTION Gets a virtual machine's configuration using the xVMHyperV DSC resource. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Specifies the lab virtual machine/node name. [Parameter(ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if (-not $Name) { # Return all nodes defined in the configuration $Name = $ConfigurationData.AllNodes | Where-Object NodeName -ne '*' | ForEach-Object { $_.NodeName; } } $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; foreach ($nodeName in $Name) { $node = Resolve-NodePropertyValue -NodeName $nodeName -ConfigurationData $ConfigurationData; $xVMParams = @{ Name = $node.NodeDisplayName; VhdPath = Resolve-LabVMDiskPath -Name $node.NodeDisplayName -EnvironmentName $environmentName; } try { Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM; $vm = Get-LabDscResource -ResourceName VM -Parameters $xVMParams; Write-Output -InputObject ([PSCustomObject] $vm); } catch { Write-Error -Message ($localized.CannotLocateNodeError -f $nodeName); } } #end foreach node } #end process } #end function Get-LabVM function Get-LabVMDefault { <# .SYNOPSIS Gets the current lab virtual machine default settings. #> [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( ) process { $vmDefaults = Get-ConfigurationData -Configuration VM; ## BootOrder property should not be exposed via the Get-LabVMDefault/Set-LabVMDefault $vmDefaults.PSObject.Properties.Remove('BootOrder'); return $vmDefaults; } } #end function function Import-LabHostConfiguration { <# .SYNOPSIS Restores the lab host configuration from a backup. .LINK Export-LabHostConfiguration #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')] [OutputType([System.Management.Automation.PSCustomObject])] param ( # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias("PSPath")] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $LiteralPath, ## Restores only the lab host default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $HostSetting, ## Restores only the lab VM default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $VM, ## Restores only the lab custom media default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Media ) process { if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } if (-not (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) { $errorMessage = $localized.InvalidPathError -f 'Import', $Path; $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage; $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceUnavailable; $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileNotFound', $errorCategory, $Path; $PSCmdlet.WriteError($errorRecord); return; } Write-Verbose -Message ($localized.ImportingConfiguration -f $labDefaults.ModuleName, $Path); $configurationDocument = Get-Content -Path $Path -Raw -ErrorAction Stop; try { $configuration = ConvertFrom-Json -InputObject $configurationDocument -ErrorAction Stop; } catch { $errorMessage = $localized.InvalidConfigurationError -f $Path; throw $errorMessage; } if ((-not $PSBoundParameters.ContainsKey('HostSetting')) -and (-not $PSBoundParameters.ContainsKey('VM')) -and (-not $PSBoundParameters.ContainsKey('Media'))) { ## Nothing specified to load 'em all! $VM = $true; $HostSetting = $true; $Media = $true; } Write-Verbose -Message ($localized.ImportingConfigurationSettings -f $configuration.GenerationDate, $configuration.GenerationHost); if ($HostSetting) { $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Host'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Host'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabHostDefault -Confirm:$false; $hostSettingDefaultObject = $configuration.HostDefaults; $hostSettingDefaults = Convert-PSObjectToHashtable -InputObject $hostSettingDefaultObject; Set-LabHostDefault @hostSettingDefaults -Confirm:$false; Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'Host'); } } #end if restore host defaults if ($Media) { ## Restore media before VM defaults as VM defaults may reference custom media! $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Media'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Media'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabMedia -Confirm:$false; foreach ($mediaObject in $configuration.CustomMedia) { $customMedia = Convert-PSObjectToHashtable -InputObject $mediaObject -IgnoreNullValues; Write-Output (Register-LabMedia @customMedia -Force); } Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'Media'); } } #end if restore custom media if ($VM) { $verboseMessage = Get-FormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'VM'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'VM'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabVMDefault -Confirm:$false; $vmDefaultObject = $configuration.VMDefaults; $vmDefaults = Convert-PSObjectToHashtable -InputObject $vmDefaultObject; ## Boot order is exposed externally $vmDefaults.Remove('BootOrder'); Set-LabVMDefault @vmDefaults -Confirm:$false; Write-Verbose -Message ($localized.ConfigurationRestoreComplete -f 'VM'); } } #end if restore VM defaults } #end process } #end function function Install-LabModule { <# .SYNOPSIS Installs Lability PowerShell and DSC resource modules. .DESCRIPTION The Install-LabModule cmdlet installs PowerShell modules and/or DSC resource modules from Lability's module cache in to the local system. The DSC resources and/or PowerShell module can be installed into either the current user's or the local machine's module path. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document that contains ability'srequired media definition. .PARAMETER ModuleType Specifies the module type(s) defined in a PowerShell DSC configuration (.psd1) document to install. .PARAMETER NodeName Specifies only modules that target the node(s) specified are installed. .PARAMETER Scope Specifies the scope to install module(s) in to. The default value is 'CurrentUser'. .EXAMPLE Install-LabModule -ConfigurationData .\Config.psd1 -ModuleType -DscResource Installs all DSC resource modules defined in the 'Config.psd1' document into the user's module scope. .EXAMPLE Install-LabModule -ConfigurationData .\Config.psd1 -ModuleType -Module -Scope AllUsers Installs all PowerShell modules defined in the 'Config.psd1' document into the local machine's module scope. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $ConfigurationData, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('Module','DscResource')] [System.String[]] $ModuleType, [Parameter(ValueFromPipelineByPropertyName)] [System.String] $NodeName, [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('AllUsers','CurrentUser')] [System.String] $Scope = 'CurrentUser' ) process { $moduleRelativePath = 'WindowsPowerShell\Modules'; if ($Scope -eq 'AllUsers') { $systemDrive = (Resolve-Path -Path $env:SystemDrive).Drive; $localizedProgramFiles = Resolve-ProgramFilesFolder -Drive $systemDrive; $DestinationPath = Join-Path -Path $localizedProgramFiles.FullName -ChildPath $moduleRelativePath; } elseif ($Scope -eq 'CurrentUser') { $userDocuments = [System.Environment]::GetFolderPath('MyDocuments'); $DestinationPath = Join-Path -Path $userDocuments -ChildPath $moduleRelativePath; } $copyLabModuleParams = @{ ConfigurationData = $ConfigurationData; ModuleType = $ModuleType; DestinationPath = $DestinationPath; } Copy-LabModule @copyLabModuleParams; } #end process } #end function function Invoke-LabResourceDownload { <# .SYNOPSIS Starts a download of all required lab resources. .DESCRIPTION When a lab configuration is started, Lability will attempt to download all the required media and resources. In some scenarios you many need to download lab resources in advance, e.g. where internet access is not readily available or permitted. The `Invoke-LabResourceDownload` cmdlet can be used to manually download all required resources or specific media/resources as needed. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. .PARAMETER All Specifies all media, custom and DSC resources should be downloaded. .PARAMETER MediaId Specifies the specific media IDs to download. .PARAMETER ResourceId Specifies the specific custom resource IDs to download. .PARAMETER Media Specifies all media IDs should be downloaded. .PARAMETER Resources Specifies all custom resource IDs should be downloaded. .PARAMETER DSCResources Specifies all defined DSC resources should be downloaded. .PARAMETER Moduless Specifies all defined PowerShell modules should be downloaded. .PARAMETER Force Forces a download of all resources, overwriting any existing resources. .PARAMETER DestinationPath Specifies the target destination path of downloaded custom resources (not media or DSC resources). .EXAMPLE Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -All Downloads all required lab media, any custom resources and DSC resources defined in the 'MyLab.psd1' configuration. .EXAMPLE Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -MediaId 'WIN10_x64_Enterprise_EN_Eval' Downloads only the 'WIN10_x64_Enterprise_EN_Eval' media. .EXAMPLE Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -ResourceId 'MyCustomResource' Downloads only the 'MyCustomResource' resource defined in the 'MyLab.psd1' configuration. .EXAMPLE Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Media Downloads only the media defined in the 'MyLab.psd1' configuration. .EXAMPLE Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Resources -DSCResources Downloads only the custom file resources and DSC resources defined in the 'MyLab.psd1' configuration. #> [CmdletBinding(DefaultParameterSetName = 'All')] param ( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData = @{ }, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'All')] [System.Management.Automation.SwitchParameter] $All, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'MediaId')] [System.String[]] $MediaId, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ResourceId')] [System.String[]] $ResourceId, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Media')] [System.Management.Automation.SwitchParameter] $Media, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')] [System.Management.Automation.SwitchParameter] $Resources, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'DSCResources')] [System.Management.Automation.SwitchParameter] $DSCResources, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Modules')] [System.Management.Automation.SwitchParameter] $Modules, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')] [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ResourceId')] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { $hostDefaults = Get-ConfigurationData -Configuration Host; if (-not $DestinationPath) { $DestinationPath = $hostDefaults.ResourcePath; } } process { if ($PSCmdlet.ParameterSetName -in 'MediaId','Media','All') { if (-not $MediaId) { Write-Verbose -Message ($Localized.DownloadingAllRequiredMedia); $uniqueMediaIds = @(); $ConfigurationData.AllNodes.Where({ $_.NodeName -ne '*' }) | ForEach-Object { $id = (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData).Media; if ($uniqueMediaIds -notcontains $id) { $uniqueMediaIds += $id; } } $MediaId = $uniqueMediaIds; } if ($MediaId) { foreach ($id in $MediaId) { $labMedia = Resolve-LabMedia -ConfigurationData $ConfigurationData -Id $id; Invoke-LabMediaImageDownload -Media $labMedia -Force:$Force; Write-Verbose -Message $Localized.DownloadingAllRequiredHotfixes; if ($labMedia.Hotfixes.Count -gt 0) { foreach ($hotfix in $labMedia.Hotfixes) { Invoke-LabMediaDownload -Id $hotfix.Id -Uri $hotfix.Uri; } } else { Write-Verbose -Message ($localized.NoHotfixesSpecified); } } } else { Write-Verbose -Message ($localized.NoMediaDefined); } } #end if MediaId or MediaOnly if ($PSCmdlet.ParameterSetName -in 'ResourceId','Resources','All') { if (-not $ResourceId) { Write-Verbose -Message ($Localized.DownloadingAllDefinedResources); $ResourceId = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Resource.Id; } if (($ResourceId.Count -gt 0) -and (-not $MediaOnly)) { foreach ($id in $ResourceId) { $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $id; if (($null -eq $resource.IsLocal) -or ($resource.IsLocal -eq $false)) { $fileName = $resource.Id; if ($resource.Filename) { $fileName = $resource.Filename; } $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $fileName; $invokeResourceDownloadParams = @{ DestinationPath = $resourceDestinationPath; Uri = $resource.Uri; Checksum = $resource.Checksum; Force = $Force; } [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams; Write-Output (Get-Item -Path $resourceDestinationPath); } } } else { Write-Verbose -Message ($localized.NoResourcesDefined); } } #end if ResourceId or ResourceOnly if ($PSCmdlet.ParameterSetName -in 'DSCResources','All') { $dscResourceDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).DSCResource; if (($null -ne $dscResourceDefinitions) -and ($dscResourceDefinitions.Count -gt 0)) { ## Invokes download of DSC resource modules into the module cache Write-Verbose -Message ($Localized.DownloadingAllDSCResources); Invoke-LabModuleCacheDownload -Module $dscResourceDefinitions -Force:$Force; } else { Write-Verbose -Message ($localized.NoDSCResourcesDefined); } } #end if DSC resource if ($PSCmdlet.ParameterSetName -in 'Modules','All') { $moduleDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Module; if (($null -ne $moduleDefinitions) -and ($moduleDefinitions.Count -gt 0)) { ## Invokes download of PowerShell modules into the module cache Write-Verbose -Message ($Localized.DownloadingAllPowerShellModules); Invoke-LabModuleCacheDownload -Module $moduleDefinitions -Force:$Force; } else { Write-Verbose -Message ($localized.NoPowerShellModulesDefined); } } #end PowerShell module } #end process } #end function function New-LabImage { <# .SYNOPSIS Creates a new master/parent lab image. .DESCRIPTION The New-LabImage cmdlet starts the creation of a lab VHD(X) master image from the specified media Id. Lability will automatically create lab images as required. If there is a need to manally recreate an image, then the New-LabImage cmdlet can be used. .PARAMETER Id Specifies the media Id of the image to create. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document that contains the required media definition. .PARAMETER Force Specifies that any existing image should be overwritten. .EXAMPLE New-LabImage -Id 2016_x64_Datacenter_EN_Eval Creates the VHD(X) image from the '2016_x64_Datacenter_EN_Eval' media Id. .EXAMPLE New-LabImage -Id 2016_x64_Datacenter_EN_Eval -Force Creates the VHD(X) image from the '2016_x64_Datacenter_EN_Eval' media Id, overwriting an existing image with the same name. .LINK Get-LabMedia Get-LabImage #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.PSCustomObject])] param ( ## Lab media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Id, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Force the re(creation) of the master/parent image [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) process { ## Download media if required.. [ref] $null = $PSBoundParameters.Remove('Force') [ref] $null = $PSBoundParameters.Remove('WhatIf') [ref] $null = $PSBoundParameters.Remove('Confirm') $media = Resolve-LabMedia @PSBoundParameters ## Update media Id if alias was/is used $Id = $media.Id if ((Test-LabImage @PSBoundParameters) -and $Force) { $image = Get-LabImage @PSBoundParameters Write-Verbose -Message ($localized.RemovingDiskImage -f $image.ImagePath) [ref] $null = Remove-Item -Path $image.ImagePath -Force -ErrorAction Stop } elseif (Test-LabImage @PSBoundParameters) { throw ($localized.ImageAlreadyExistsError -f $Id) } ## Check Dism requirement (if present) #167 if ($null -ne $media.CustomData.MinimumDismVersion) { $minimumDismVersion = $media.CustomData.MinimumDismVersion if ($labDefaults.DismVersion -lt $minimumDismVersion) { throw ($localized.DismVersionMismatchError -f $Id, $minimumDismVersion.ToString()) } } $hostDefaults = Get-ConfigurationData -Configuration Host if ($media.MediaType -eq 'VHD') { $mediaFileInfo = Invoke-LabMediaImageDownload -Media $media Write-Verbose -Message ($localized.ImportingExistingDiskImage -f $media.Description) $imageName = $media.Filename $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName } #end if VHD elseif ($media.MediaType -eq 'NULL') { Write-Verbose -Message ($localized.CreatingDiskImage -f $media.Description) $imageName = '{0}.vhdx' -f $Id $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName ## Create disk image and refresh PSDrives $newEmptyDiskImageParams = @{ Path = $imagePath Force = $true ErrorAction = 'Stop' } if ($media.CustomData.DiskType) { $newEmptyDiskImageParams['Type'] = $media.CustomData.DiskType } if ($media.CustomData.DiskSize) { $newEmptyDiskImageParams['Size'] = $media.CustomData.DiskSize } $image = New-EmptyDiskImage @newEmptyDiskImageParams } else { ## Create VHDX $mediaFileInfo = Invoke-LabMediaImageDownload -Media $media if ($media.CustomData.PartitionStyle) { ## Custom partition style has been defined so use that $partitionStyle = $media.CustomData.PartitionStyle } elseif ($media.Architecture -eq 'x86') { ## Otherwise default to MBR for x86 media $partitionStyle = 'MBR' } else { $partitionStyle = 'GPT' } Write-Verbose -Message ($localized.CreatingDiskImage -f $media.Description) $imageName = '{0}.vhdx' -f $Id $imagePath = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $imageName ## Apply WIM (Expand-LabImage) and add specified features $expandLabImageParams = @{ MediaPath = $mediaFileInfo.FullName PartitionStyle = $partitionStyle } ## Determine whether we're using the WIM image index or image name. This permits ## specifying an integer image index in a media's 'ImageName' property. [System.Int32] $wimImageIndex = $null if ([System.Int32]::TryParse($media.ImageName, [ref] $wimImageIndex)) { $expandLabImageParams['WimImageIndex'] = $wimImageIndex } else { if ([System.String]::IsNullOrEmpty($media.ImageName)) { throw ($localized.ImageNameRequiredError -f 'ImageName') } $expandLabImageParams['WimImageName'] = $media.ImageName } $imageCreationFailed = $false try { ## Create disk image and refresh PSDrives $newDiskImageParams = @{ Path = $imagePath PartitionStyle = $partitionStyle Passthru = $true Force = $true ErrorAction = 'Stop' } if ($media.CustomData.DiskType) { $newDiskImageParams['Type'] = $media.CustomData.DiskType } if ($media.CustomData.DiskSize) { $newDiskImageParams['Size'] = $media.CustomData.DiskSize } $image = New-DiskImage @newDiskImageParams [ref] $null = Get-PSDrive $expandLabImageParams['Vhd'] = $image if ($media.CustomData.SourcePath) { $expandLabImageParams['SourcePath'] = $media.CustomData.SourcePath } if ($media.CustomData.WimPath) { $expandLabImageParams['WimPath'] = $media.CustomData.WimPath } if ($media.CustomData.WindowsOptionalFeature) { $expandLabImageParams['WindowsOptionalFeature'] = $media.CustomData.WindowsOptionalFeature } if ($media.CustomData.PackagePath) { $expandLabImageParams['PackagePath'] = $media.CustomData.PackagePath } if ($media.CustomData.Package) { $expandLabImageParams['Package'] = $media.CustomData.Package } if ($media.CustomData.PackageLocale) { $expandLabImageParams['PackageLocale'] = $media.CustomData.PackageLocale } Expand-LabImage @expandLabImageParams ## Apply hotfixes (Add-DiskImageHotfix) $addDiskImageHotfixParams = @{ Id = $Id Vhd = $image PartitionStyle = $partitionStyle } if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $addDiskImageHotfixParams['ConfigurationData'] = $ConfigurationData } Add-DiskImageHotfix @addDiskImageHotfixParams ## Configure boot volume (Set-DiskImageBootVolume) Set-DiskImageBootVolume -Vhd $image -PartitionStyle $partitionStyle } catch { ## Have to ensure VHDX is dismounted before we can delete! $imageCreationFailed = $true Write-Error -Message $_ } finally { ## Dismount VHDX Hyper-V\Dismount-VHD -Path $imagePath } if ($imageCreationFailed -eq $true) { Write-Warning -Message ($localized.RemovingIncompleteImageWarning -f $imagePath) Remove-Item -Path $imagePath -Force } } #end if ISO/WIM return (Get-LabImage $PSBoundParameters) } #end process } #end function New-LabImage function New-LabVM { <# .SYNOPSIS Creates a simple bare-metal virtual machine. .DESCRIPTION The New-LabVM cmdlet creates a bare virtual machine using the specified media. No bootstrap or DSC configuration is applied. NOTE: The mandatory -MediaId parameter is dynamic and is not displayed in the help syntax output. If optional values are not specified, the virtual machine default settings are applied. To list the current default settings run the `Get-LabVMDefault` command. NOTE: If a specified virtual switch cannot be found, an Internal virtual switch will automatically be created. To use any other virtual switch configuration, ensure the virtual switch is created in advance. .LINK Register-LabMedia Unregister-LabMedia Get-LabVMDefault Set-LabVMDefault #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'PSCredential')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams','')] param ( ## Specifies the virtual machine name. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Default virtual machine startup memory (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $StartupMemory, ## Default virtual machine miniumum dynamic memory allocation (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $MinimumMemory, ## Default virtual machine maximum dynamic memory allocation (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $MaximumMemory, ## Default virtual machine processor count. [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 4)] [System.Int32] $ProcessorCount, # Input Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^([a-z]{2,2}-[a-z]{2,2})|(\d{4,4}:\d{8,8})$')] [System.String] $InputLocale, # System Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $SystemLocale, # User Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $UserLocale, # UI Language [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $UILanguage, # Timezone [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Timezone, # Registered Owner [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $RegisteredOwner, # Registered Organization [Parameter(ValueFromPipelineByPropertyName)] [Alias('RegisteredOrganisation')] [ValidateNotNullOrEmpty()] [System.String] $RegisteredOrganization, ## Local administrator password of the VM. The username is NOT used. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'PSCredential')] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential = (& $credentialCheckScriptBlock), ## Local administrator password of the VM. [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Password')] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $Password, ## Virtual machine switch name(s). [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $SwitchName, ## Virtual machine MAC address(es). [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $MACAddress, ## Enable Secure boot status [Parameter(ValueFromPipelineByPropertyName)] [System.Boolean] $SecureBoot, ## Enable Guest Integration Services [Parameter(ValueFromPipelineByPropertyName)] [System.Boolean] $GuestIntegrationServices, ## Custom data [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Collections.Hashtable] $CustomData, ## Skip creating baseline snapshots [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoSnapshot ) DynamicParam { ## Adds a dynamic -MediaId parameter that returns the available media Ids $parameterAttribute = New-Object -TypeName 'System.Management.Automation.ParameterAttribute' $parameterAttribute.ParameterSetName = '__AllParameterSets' $parameterAttribute.Mandatory = $false $attributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]' $attributeCollection.Add($parameterAttribute) $mediaIds = Get-LabMediaId $validateSetAttribute = New-Object -TypeName 'System.Management.Automation.ValidateSetAttribute' -ArgumentList $mediaIds $attributeCollection.Add($validateSetAttribute) $runtimeParameter = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameter' -ArgumentList @('MediaId', [System.String], $attributeCollection) $runtimeParameterDictionary = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameterDictionary' $runtimeParameterDictionary.Add('MediaId', $runtimeParameter) return $runtimeParameterDictionary } begin { ## If we have only a secure string, create a PSCredential if ($PSCmdlet.ParameterSetName -eq 'Password') { $Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList 'LocalAdministrator', $Password } if (-not $Credential) { throw ($localized.CannotProcessCommandError -f 'Credential') } elseif ($Credential.Password.Length -eq 0) { throw ($localized.CannotBindArgumentError -f 'Password') } } process { ## Skeleton configuration node $configurationNode = @{ } if ($CustomData) { ## Add all -CustomData keys/values to the skeleton configuration foreach ($key in $CustomData.Keys) { $configurationNode[$key] = $CustomData.$key } } ## Explicitly defined parameters override any -CustomData $parameterNames = @('StartupMemory','MinimumMemory','MaximumMemory','SwitchName','Timezone','UILanguage','MACAddress', 'ProcessorCount','InputLocale','SystemLocale','UserLocale','RegisteredOwner','RegisteredOrganization','SecureBoot') foreach ($key in $parameterNames) { if ($PSBoundParameters.ContainsKey($key)) { $configurationNode[$key] = $PSBoundParameters.$key } } ## Ensure the specified MediaId is applied after any CustomData media entry! if ($PSBoundParameters.ContainsKey('MediaId')) { $configurationNode['Media'] = $PSBoundParameters.MediaId } else { $configurationNode['Media'] = (Get-LabVMDefault).Media } $currentNodeCount = 0 foreach ($vmName in $Name) { ## Update the node name before creating the VM $configurationNode['NodeName'] = $vmName $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'New-LabVM', $vmName $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingQuickVM -f $vmName, $PSBoundParameters.MediaId) if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $currentNodeCount++ [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1 $activity = $localized.ConfiguringNode -f $vmName Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete $configurationData = @{ AllNodes = @( $configurationNode ) } New-LabVirtualMachine -Name $vmName -ConfigurationData $configurationData -Credential $Credential -NoSnapshot:$NoSnapshot -IsQuickVM } } #end foreach name if (-not [System.String]::IsNullOrEmpty($activity)) { Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete } } #end process } #end function New-LabVM function Register-LabMedia { <# .SYNOPSIS Registers a custom media entry. .DESCRIPTION The Register-LabMedia cmdlet allows adding custom media to the host's configuration. This circumvents the requirement of having to define custom media entries in the DSC configuration document (.psd1). You can use the Register-LabMedia cmdlet to override the default media entries, e.g. you have the media hosted internally or you wish to replace the built-in media with your own implementation. To override a built-in media entry, specify the same media Id with the -Force switch. .PARAMETER Legacy Specifies registering a legacy Windows 10 media as custom media. .EXAMPLE Register-LabMedia -Legacy WIN10_x64_Enterprise_1809_EN_Eval Reregisters the deprecated Windows 10 Enterprise x64 English evaluation media. .LINK Get-LabMedia Unregister-LabMedia #> [CmdletBinding(DefaultParameterSetName = 'ID')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( ## Specifies the media Id to register. You can override the built-in media if required. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [System.String] $Id, ## Specifies the media's type. [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateSet('VHD','ISO','WIM','NULL')] [System.String] $MediaType, ## Specifies the source Uri (http/https/file) of the media. [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [System.Uri] $Uri, ## Specifies the architecture of the media. [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateSet('x64','x86')] [System.String] $Architecture, ## Specifies a description of the media. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNullOrEmpty()] [System.String] $Description, ## Specifies the image name containing the target WIM image. You can specify integer values. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNullOrEmpty()] [System.String] $ImageName, ## Specifies the local filename of the locally cached resource file. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNullOrEmpty()] [System.String] $Filename, ## Specifies the MD5 checksum of the resource file. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNullOrEmpty()] [System.String] $Checksum, ## Specifies custom data for the media. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNull()] [System.Collections.Hashtable] $CustomData, ## Specifies additional Windows hotfixes to install post deployment. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateNotNull()] [System.Collections.Hashtable[]] $Hotfixes, ## Specifies the media type. Linux VHD(X)s do not inject resources. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [ValidateSet('Windows','Linux')] [System.String] $OperatingSystem = 'Windows', ## Specifies the media alias (Id). [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ID')] [System.String] $Alias, ## Registers media via a JSON file hosted externally. [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'FromUri')] [System.String] $FromUri, ## Registers media using a custom Id. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'FromUri')] [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Legacy')] [System.String] $CustomId, ## Specifies that an exiting media entry should be overwritten. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) DynamicParam { ## Adds a dynamic -Legacy parameter that returns the available legacy Windows 10 media Ids $parameterAttribute = New-Object -TypeName 'System.Management.Automation.ParameterAttribute' $parameterAttribute.ParameterSetName = 'Legacy' $parameterAttribute.Mandatory = $true $attributeCollection = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Attribute]' $attributeCollection.Add($parameterAttribute) $mediaIds = (Get-LabMedia -Legacy).Id $validateSetAttribute = New-Object -TypeName 'System.Management.Automation.ValidateSetAttribute' -ArgumentList $mediaIds $attributeCollection.Add($validateSetAttribute) $runtimeParameter = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameter' -ArgumentList @('Legacy', [System.String], $attributeCollection) $runtimeParameterDictionary = New-Object -TypeName 'System.Management.Automation.RuntimeDefinedParameterDictionary' $runtimeParameterDictionary.Add('Legacy', $runtimeParameter) return $runtimeParameterDictionary } process { switch ($PSCmdlet.ParameterSetName) { Legacy { ## Download the json content and convert into a hashtable $legacyMedia = Get-LabMedia -Id $PSBoundParameters.Legacy -Legacy | Convert-PSObjectToHashtable if ($PSBoundParameters.ContainsKey('CustomId')) { $legacyMedia['Id'] = $CustomId } ## Recursively call Register-LabMedia and splat the properties return (Register-LabMedia @legacyMedia -Force:$Force) } FromUri { ## Download the json content and convert into a hashtable $customMedia = Invoke-RestMethod -Uri $FromUri | Convert-PSObjectToHashtable if ($PSBoundParameters.ContainsKey('CustomId')) { $customMedia['Id'] = $CustomId } ## Recursively call Register-LabMedia and splat the properties return (Register-LabMedia @customMedia -Force:$Force) } default { ## Validate Linux VM media type is VHD or NULL if (($OperatingSystem -eq 'Linux') -and ($MediaType -notin 'VHD','NULL')) { throw ($localized.InvalidOSMediaTypeError -f $MediaType, $OperatingSystem) } ## Validate ImageName when media type is ISO/WIM if (($MediaType -eq 'ISO') -or ($MediaType -eq 'WIM')) { if (-not $PSBoundParameters.ContainsKey('ImageName')) { throw ($localized.ImageNameRequiredError -f '-ImageName') } } ## Resolve the media Id to see if it's already been used try { $media = Resolve-LabMedia -Id $Id -ErrorAction SilentlyContinue } catch { Write-Debug -Message ($localized.CannotLocateMediaError -f $Id) } if ($media -and (-not $Force)) { throw ($localized.MediaAlreadyRegisteredError -f $Id, '-Force') } if ($PSBoundParameters.ContainsKey('Alias')) { ## Check to see whether the alias is already registered $existingMediaIds = Get-LabMediaId if ($existingMediaIds.Contains($Alias)) { throw ($localized.MediaAliasAlreadyRegisteredError -f $Alias) } } ## Get the custom media list (not the built in media) $existingCustomMedia = @(Get-ConfigurationData -Configuration CustomMedia) if (-not $existingCustomMedia) { $existingCustomMedia = @() } $customMedia = [PSCustomObject] @{ Id = $Id Alias = $Alias Filename = $Filename Description = $Description Architecture = $Architecture ImageName = $ImageName MediaType = $MediaType OperatingSystem = $OperatingSystem Uri = $Uri Checksum = $Checksum CustomData = $CustomData Hotfixes = $Hotfixes } $hasExistingMediaEntry = $false for ($i = 0; $i -lt $existingCustomMedia.Count; $i++) { if ($existingCustomMedia[$i].Id -eq $Id) { Write-Verbose -Message ($localized.OverwritingCustomMediaEntry -f $Id) $hasExistingMediaEntry = $true $existingCustomMedia[$i] = $customMedia } } if (-not $hasExistingMediaEntry) { ## Add it to the array Write-Verbose -Message ($localized.AddingCustomMediaEntry -f $Id) $existingCustomMedia += $customMedia } Write-Verbose -Message ($localized.SavingConfiguration -f $Id) Set-ConfigurationData -Configuration CustomMedia -InputObject @($existingCustomMedia) return $customMedia } #end default } #end switch } #end process } #end function function Remove-LabConfiguration { <# .SYNOPSIS Removes all VMs and associated snapshots of all nodes defined in a PowerShell DSC configuration document. .DESCRIPTION The Remove-LabConfiguration removes all virtual machines that have a corresponding NodeName defined in the AllNode array of the PowerShell DSC configuration document. WARNING: ALL EXISTING VIRTUAL MACHINE DATA WILL BE LOST WHEN VIRTUAL MACHINES ARE REMOVED. By default, associated virtual machine switches are not removed as they may be used by other virtual machines or lab configurations. If you wish to remove any virtual switche defined in the PowerShell DSC configuration document, specify the -RemoveSwitch parameter. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document used to remove existing virtual machines. One virtual machine is removed per node defined in the AllNodes array. .PARAMETER RemoveSwitch Specifies that any connected virtual switch should also be removed when the virtual machine is removed. .LINK about_ConfigurationData Start-LabConfiguration #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Include removal of virtual switch(es). By default virtual switches are not removed. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $RemoveSwitch ) process { Write-Verbose -Message $localized.StartedLabConfiguration; $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' }; $currentNodeCount = 0; foreach ($node in $nodes) { $currentNodeCount++; $nodeProperties = Resolve-NodePropertyValue -NodeName $node.NodeName -ConfigurationData $ConfigurationData; [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1; $activity = $localized.ConfiguringNode -f $nodeProperties.NodeDisplayName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; ##TODO: Should this not ensure that VMs are powered off $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Remove-Lab', $nodeProperties.NodeDisplayName; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingVM -f $nodeProperties.NodeDisplayName); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { Remove-LabVirtualMachine -Name $node.NodeName -ConfigurationData $ConfigurationData -RemoveSwitch:$RemoveSwitch -Confirm:$false; } } #end foreach node ## TODO: Remove empty (environment name) differencing disk folder(s) Write-Progress -Id 42 -Activity $activity -Completed; Write-Verbose -Message $localized.FinishedLabConfiguration; } #end process } #end function Remove-LabConfiguration function Remove-LabVM { <# .SYNOPSIS Removes a bare-metal virtual machine and differencing VHD(X). .DESCRIPTION The Remove-LabVM cmdlet removes a virtual machine and it's VHD(X) file. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( ## Virtual machine name [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $currentNodeCount = 0; foreach ($vmName in $Name) { $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Remove-LabVM', $vmName; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.RemovingVM -f $vmName); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $currentNodeCount++; [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1; $activity = $localized.ConfiguringNode -f $vmName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; ## Create a skeleton config data if one wasn't supplied if (-not $PSBoundParameters.ContainsKey('ConfigurationData')) { try { <# If we don't have configuration document, we need to locate the lab image id so that the VM's VHD/X can be removed #182 #> $mediaId = (Resolve-LabVMImage -Name $vmName).Id; } catch { throw ($localized.CannotResolveMediaIdError -f $vmName); } $configurationData = @{ AllNodes = @( @{ NodeName = $vmName; Media = $mediaId; } ) }; } Remove-LabVirtualMachine -Name $vmName -ConfigurationData $configurationData; } #end if should process } #end foreach VM if (-not [System.String]::IsNullOrEmpty($activity)) { Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; } } #end process } #end function Remove-LabVM function Reset-Lab { <# .SYNOPSIS Reverts all VMs in a lab back to their initial configuration. .DESCRIPTION The Reset-Lab cmdlet will reset all the nodes defined in a PowerShell DSC configuration document, back to their initial state. If virtual machines are powered on, they will automatically be powered off when restoring the snapshot. When virtual machines are created - before they are powered on - a baseline snapshot is created. This snapshot is taken before the Sysprep process has been run and/or any PowerShell DSC configuration has been applied. WARNING: You will lose all changes to all virtual machines that have not been committed via another snapshot. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document. .LINK Checkpoint-Lab .NOTES This cmdlet uses the baseline snapshot snapshot created by the Start-LabConfiguration cmdlet. If the baseline was not created or the baseline snapshot does not exist, the lab VMs can be recreated with the Start-LabConfiguration -Force. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { ## Revert to Base/Lab snapshots... $snapshotName = $localized.BaselineSnapshotName -f $labDefaults.ModuleName; Restore-Lab -ConfigurationData $ConfigurationData -SnapshotName $snapshotName -Force; } #end process } #end function Reset-Lab function Reset-LabHostDefault { <# .SYNOPSIS Resets lab host default settings to default. .DESCRIPTION The Reset-LabHostDefault cmdlet resets the lab host's settings to default values. .LINK Get-LabHostDefault Set-LabHostDefault #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ) process { Remove-ConfigurationData -Configuration Host; Get-LabHostDefault; } #end process } #end function Reset-LabHostDefault function Reset-LabMedia { <# .SYNOPSIS Reset the lab media entries to default settings. .DESCRIPTION The Reset-LabMedia removes all custom media entries, reverting them to default values. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ) process { Remove-ConfigurationData -Configuration CustomMedia Get-Labmedia } } function Reset-LabVM { <# .SYNOPSIS Recreates a lab virtual machine. .DESCRIPTION The Reset-LabVM cmdlet deletes and recreates a lab virtual machine, reapplying the MOF. To revert a single VM to a previous state, use the Restore-VMSnapshot cmdlet. To revert an entire lab environment, use the Restore-Lab cmdlet. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'PSCredential')] param ( ## Specifies the lab virtual machine/node name. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Local administrator password of the virtual machine. The username is NOT used. [Parameter(ParameterSetName = 'PSCredential', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential = (& $credentialCheckScriptBlock), ## Local administrator password of the virtual machine. [Parameter(Mandatory, ParameterSetName = 'Password', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $Password, ## Directory path containing the virtual machines' .mof file(s). [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Skip creation of the initial baseline snapshot. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoSnapshot, ## Ignores missing MOF file [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $SkipMofCheck ) begin { ## If we have only a secure string, create a PSCredential if ($PSCmdlet.ParameterSetName -eq 'Password') { $Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList 'LocalAdministrator', $Password; } if (-not $Credential) { throw ($localized.CannotProcessCommandError -f 'Credential'); } elseif ($Credential.Password.Length -eq 0) { throw ($localized.CannotBindArgumentError -f 'Password'); } } process { ## There is an assumption here is all .mofs are in the same folder $resolveConfigurationPathParams = @{ ConfigurationData = $ConfigurationData; Name = $Name | Select-Object -First 1; Path = $Path; UseDefaultPath = $SkipMofCheck; } $Path = Resolve-ConfigurationPath @resolveConfigurationPathParams; $currentNodeCount = 0; foreach ($vmName in $Name) { $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Reset-LabVM', $vmName; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.ResettingVM -f $vmName); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $currentNodeCount++; [System.Int32] $percentComplete = (($currentNodeCount / $Name.Count) * 100) - 1; $activity = $localized.ConfiguringNode -f $vmName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; [ref] $null = Remove-LabVirtualMachine -Name $vmName -ConfigurationData $ConfigurationData; $newLabVirtualMachineParams = @{ Name = $vmName; ConfigurationData = $ConfigurationData; Path = $Path; NoSnapshot = $NoSnapshot; Credential = $Credential; } New-LabVirtualMachine @newLabVirtualMachineParams; } #end if should process } #end foreach VMd if (-not [System.String]::IsNullOrEmpty($activity)) { Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; } } #end process } #end function Reset-LabVM function Reset-LabVMDefault { <# .SYNOPSIS Reset the current lab virtual machine default settings back to defaults. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ) process { Remove-ConfigurationData -Configuration VM; Get-LabVMDefault; } } #end function function Restore-Lab { <# .SYNOPSIS Restores all lab VMs to a previous configuration. .DESCRIPTION The Restore-Lab reverts all the nodes defined in a PowerShell DSC configuration document, back to a previously captured configuration. When creating the snapshots, they are created using a snapshot name. To restore a lab to a previous configuration, you must supply the same snapshot name. All virtual machines should be powered off when the snapshots are restored. If VMs are powered on, an error will be generated. You can override this behaviour by specifying the -Force parameter. WARNING: If the -Force parameter is used, running virtual machines will be powered off automatically. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document. .PARAMETER SnapshotName Specifies the virtual machine snapshot name to be restored. You must use the same snapshot name used when creating the snapshot with the Checkpoint-Lab cmdlet. .PARAMETER Force Forces virtual machine snapshots to be restored - even if there are any running virtual machines. .LINK Checkpoint-Lab Reset-Lab #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Snapshot name [Parameter(Mandatory)] [Alias('Name')] [System.String] $SnapshotName, ## Force snapshots if virtual machines are on [System.Management.Automation.SwitchParameter] $Force ) process { $nodes = @(); $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { $nodes += Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData; }; $runningNodes = $nodes | ForEach-Object { Hyper-V\Get-VM -Name $_.NodeDisplayName } | Where-Object { $_.State -ne 'Off' } $currentNodeCount = 0; if ($runningNodes -and $Force) { $nodes | Sort-Object { $_.BootOrder } | ForEach-Object { $currentNodeCount++; [System.Int32] $percentComplete = ($currentNodeCount / $nodes.Count) * 100; $activity = $localized.ConfiguringNode -f $_.NodeDisplayName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; Write-Verbose -Message ($localized.RestoringVirtualMachineSnapshot -f $_.NodeDisplayName, $SnapshotName); Get-LabVMSnapshot -Name $_.NodeDisplayName -SnapshotName $SnapshotName | Hyper-V\Restore-VMSnapshot; } } elseif ($runningNodes) { foreach ($runningNode in $runningNodes) { ## The running nodes are Get-VM objects! Write-Error -Message ($localized.CannotSnapshotNodeError -f $runningNode.Name); } } else { $nodes | Sort-Object { $_.BootOrder } | ForEach-Object { $currentNodeCount++; [System.Int32] $percentComplete = ($currentNodeCount / $nodes.Count) * 100; $activity = $localized.ConfiguringNode -f $_.NodeDisplayName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; Write-Verbose -Message ($localized.RestoringVirtualMachineSnapshot -f $_.NodeDisplayName, $SnapshotName); Get-LabVMSnapshot -Name $_.NodeDisplayName -SnapshotName $SnapshotName | Hyper-V\Restore-VMSnapshot; } Write-Progress -Id 42 -Activity $activity -Completed; } } #end process } #end function Restore-Lab function Set-LabHostDefault { <# .SYNOPSIS Sets the lab host's default settings. .DESCRIPTION The Set-LabHostDefault cmdlet sets one or more lab host default settings. .LINK Get-LabHostDefault Reset-LabHostDefault #> [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ## Lab host .mof configuration document search path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ConfigurationPath, ## Lab host Media/ISO storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $IsoPath, ## Lab host parent/master VHD(X) storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ParentVhdPath, ## Lab host virtual machine differencing VHD(X) storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DifferencingVhdPath, ## Lab module storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('ModulePath')] [System.String] $ModuleCachePath, ## Lab custom resource storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourcePath, ## Lab host DSC resource share name (for SMB Pull Server). [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourceShareName, ## Lab host media hotfix storage location/path. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $HotfixPath, ## Disable local caching of file-based ISO and WIM files. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $DisableLocalFileCaching, ## Enable call stack logging in verbose output [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $EnableCallStackLogging, ## Custom DISM/ADK path. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $DismPath, ## Specifies an custom/internal PS repository Uri. Use the full Uri without any package ## name(s). '/{PackageName}/{PackageVersion}' will automatically be appended as needed. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $RepositoryUri, ## Specifies whether environment name prefixes/suffixes are applied to virtual switches. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $DisableSwitchEnvironmentName, ## Specifies whether VM differencing disks are placed into a subdirectory when an ## environment name is defined. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $DisableVhdEnvironmentName ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $resolvablePaths = @( 'IsoPath', 'ParentVhdPath', 'DifferencingVhdPath', 'ResourcePath', 'HotfixPath', 'UpdatePath', 'ConfigurationPath', 'ModuleCachePath' ) foreach ($path in $resolvablePaths) { if ($PSBoundParameters.ContainsKey($path)) { $resolvedPath = Resolve-PathEx -Path $PSBoundParameters[$path]; if (-not ((Test-Path -Path $resolvedPath -PathType Container -IsValid) -and (Test-Path -Path (Split-Path -Path $resolvedPath -Qualifier))) ) { throw ($localized.InvalidPathError -f $resolvedPath, $PSBoundParameters[$path]); } else { $hostDefaults.$path = $resolvedPath.Trim('\'); } } } if ($PSBoundParameters.ContainsKey('ResourceShareName')) { $hostDefaults.ResourceShareName = $ResourceShareName; } if ($PSBoundParameters.ContainsKey('DisableLocalFileCaching')) { $hostDefaults.DisableLocalFileCaching = $DisableLocalFileCaching.ToBool(); } if ($PSBoundParameters.ContainsKey('EnableCallStackLogging')) { ## Set the global script variable read by Write-Verbose $script:labDefaults.CallStackLogging = $EnableCallStackLogging; $hostDefaults.EnableCallStackLogging = $EnableCallStackLogging.ToBool(); } if ($PSBoundParameters.ContainsKey('DismPath')) { $hostDefaults.DismPath = Resolve-DismPath -Path $DismPath; Write-Warning -Message ($localized.DismSessionRestartWarning); } if ($PSBoundParameters.ContainsKey('RepositoryUri')) { $hostDefaults.RepositoryUri = $RepositoryUri.TrimEnd('/'); } if ($PSBoundParameters.ContainsKey('DisableSwitchEnvironmentName')) { $hostDefaults.DisableSwitchEnvironmentName = $DisableSwitchEnvironmentName.ToBool(); } if ($PSBoundParameters.ContainsKey('DisableVhdEnvironmentName')) { $hostDefaults.DisableVhdEnvironmentName = $DisableVhdEnvironmentName.ToBool(); } Set-ConfigurationData -Configuration Host -InputObject $hostDefaults; Import-DismModule; ## Refresh the defaults to ensure environment variables are updated $hostDefaults = Get-ConfigurationData -Configuration Host; return $hostDefaults; } #end process } #end function function Set-LabVMDefault { <# .SYNOPSIS Sets the lab virtual machine default settings. #> [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)] [OutputType([System.Management.Automation.PSCustomObject])] param ( ## Default virtual machine startup memory (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $StartupMemory, ## Default virtual machine miniumum dynamic memory allocation (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $MinimumMemory, ## Default virtual machine maximum dynamic memory allocation (bytes). [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(536870912, 1099511627776)] [System.Int64] $MaximumMemory, ## Default virtual machine processor count. [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 4)] [System.Int32] $ProcessorCount, ## Default virtual machine media Id. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Media, ## Lab host internal switch name. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $SwitchName, # Input Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^([a-z]{2,2}-[a-z]{2,2})|(\d{4,4}:\d{8,8})$')] [System.String] $InputLocale, # System Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $SystemLocale, # User Locale [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $UserLocale, # UI Language [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[a-z]{2,2}-[a-z]{2,2}$')] [System.String] $UILanguage, # Timezone [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Timezone, # Registered Owner [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $RegisteredOwner, # Registered Organization [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] [Alias('RegisteredOrganisation')] $RegisteredOrganization, ## Client PFX certificate bundle used to encrypt DSC credentials [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $ClientCertificatePath, ## Client certificate's issuing Root Certificate Authority (CA) certificate [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $RootCertificatePath, ## Boot delay/pause between VM operations [Parameter(ValueFromPipelineByPropertyName)] [System.UInt16] $BootDelay, ## Secure boot status. Could be a SwitchParameter but boolean is more explicit? [Parameter(ValueFromPipelineByPropertyName)] [System.Boolean] $SecureBoot, ## Custom bootstrap order [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('ConfigurationFirst','ConfigurationOnly','Disabled','MediaFirst','MediaOnly')] [System.String] $CustomBootstrapOrder = 'MediaFirst', ## Enable Guest Integration Services [Parameter(ValueFromPipelineByPropertyName)] [System.Boolean] $GuestIntegrationServices, ## Enable Automatic Checkpoints (Windows 10 build 1709 and later) [Parameter(ValueFromPipelineByPropertyName)] [System.Boolean] $AutomaticCheckpoints, ## WSMan maximum envelope size [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $MaxEnvelopeSizeKb, ## By default, all virtual machines and their associated disks are created using the node name as it is ## defined in the .psd1 configuration file. This name can be either a FQDN or a NetBIOS name format. Enabling ## the 'UseNetBIOSName' forces all VMs and disk files to be created using the NetBIOS (short) name format. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseNetBIOSName ) process { $vmDefaults = Get-ConfigurationData -Configuration VM; if ($PSBoundParameters.ContainsKey('StartupMemory')) { $vmDefaults.StartupMemory = $StartupMemory; } if ($PSBoundParameters.ContainsKey('MinimumMemory')) { $vmDefaults.MinimumMemory = $MinimumMemory; } if ($PSBoundParameters.ContainsKey('MaximumMemory')) { $vmDefaults.MaximumMemory = $MaximumMemory; } if ($PSBoundParameters.ContainsKey('ProcessorCount')) { $vmDefaults.ProcessorCount = $ProcessorCount; } if ($PSBoundParameters.ContainsKey('Media')) { ## Resolve-LabMedia will throw if media cannot be resolved $labMedia = Resolve-LabMedia -Id $Media; $vmDefaults.Media = $labMedia.Id; } if ($PSBoundParameters.ContainsKey('SwitchName')) { $vmDefaults.SwitchName = $SwitchName; } if ($PSBoundParameters.ContainsKey('Timezone')) { $vmDefaults.Timezone = Assert-TimeZone -TimeZone $Timezone; } if ($PSBoundParameters.ContainsKey('UILanguage')) { $vmDefaults.UILanguage = $UILanguage; } if ($PSBoundParameters.ContainsKey('InputLocale')) { $vmDefaults.InputLocale = $InputLocale; } if ($PSBoundParameters.ContainsKey('SystemLocale')) { $vmDefaults.SystemLocale = $SystemLocale; } if ($PSBoundParameters.ContainsKey('UserLocale')) { $vmDefaults.UserLocale = $UserLocale; } if ($PSBoundParameters.ContainsKey('RegisteredOwner')) { $vmDefaults.RegisteredOwner = $RegisteredOwner; } if ($PSBoundParameters.ContainsKey('RegisteredOrganization')) { $vmDefaults.RegisteredOrganization = $RegisteredOrganization; } if ($PSBoundParameters.ContainsKey('ClientCertificatePath')) { if (-not [System.String]::IsNullOrWhitespace($ClientCertificatePath)) { $ClientCertificatePath = [System.Environment]::ExpandEnvironmentVariables($ClientCertificatePath); if (-not (Test-Path -Path $ClientCertificatePath -Type Leaf)) { throw ($localized.CannotFindCertificateError -f 'Client', $ClientCertificatePath); } } $vmDefaults.ClientCertificatePath = $ClientCertificatePath; } if ($PSBoundParameters.ContainsKey('RootCertificatePath')) { if (-not [System.String]::IsNullOrWhitespace($RootCertificatePath)) { $RootCertificatePath = [System.Environment]::ExpandEnvironmentVariables($RootCertificatePath); if (-not (Test-Path -Path $RootCertificatePath -Type Leaf)) { throw ($localized.CannotFindCertificateError -f 'Root', $RootCertificatePath); } } $vmDefaults.RootCertificatePath = $RootCertificatePath; } if ($PSBoundParameters.ContainsKey('BootDelay')) { $vmDefaults.BootDelay = $BootDelay; } if ($PSBoundParameters.ContainsKey('CustomBootstrapOrder')) { $vmDefaults.CustomBootstrapOrder = $CustomBootstrapOrder; } if ($PSBoundParameters.ContainsKey('SecureBoot')) { $vmDefaults.SecureBoot = $SecureBoot; } if ($PSBoundParameters.ContainsKey('GuestIntegrationServices')) { $vmDefaults.GuestIntegrationServices = $GuestIntegrationServices; } if ($PSBoundParameters.ContainsKey('AutomaticCheckpoints')) { $vmDefaults.AutomaticCheckpoints = $AutomaticCheckpoints; } if ($PSBoundParameters.ContainsKey('MaxEnvelopeSizeKb')) { $vmDefaults.MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb; } if ($PSBoundParameters.ContainsKey('UseNetBIOSName')) { $vmDefaults.UseNetBIOSName = $UseNetBIOSName.ToBool(); } if ($vmDefaults.StartupMemory -lt $vmDefaults.MinimumMemory) { throw ($localized.StartMemLessThanMinMemError -f $vmDefaults.StartupMemory, $vmDefaults.MinimumMemory); } elseif ($vmDefaults.StartupMemory -gt $vmDefaults.MaximumMemory) { throw ($localized.StartMemGreaterThanMaxMemError -f $vmDefaults.StartupMemory, $vmDefaults.MaximumMemory); } $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Set-LabVMDefault', $vmName; $verboseProcessMessage = $localized.SettingVMDefaults; if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { Set-ConfigurationData -Configuration VM -InputObject $vmDefaults; } ## BootOrder property should not be exposed via the Get-LabVMDefault/Set-LabVMDefault $vmDefaults.PSObject.Properties.Remove('BootOrder'); return $vmDefaults; } } #end function function Start-DscCompilation { <# .SYNOPSIS Starts compilation of one or more DSC configurations. .DESCRIPTION The Start-DscCompilation cmdlet can compile multiple PowerShell DSC configurations (.ps1 files), in parallel using jobs. Each PowerShell DSC configuration (.ps1) is loaded into a separate PowerShell.exe process, and called using the supplied configuration parameter hashtable to each each instance. .NOTES The Start-DscCompilation cmdlet assumes/requires that each node has its own PowerShell DSC configuration (.psd1) document. .PARAMETER Configuration Specifies the file path(s) to PowerShell DSC configuration (.ps1) files to compile. .PARAMETER InoutObject Specifies the file references to PowerShell DSC configuration (.ps1) files to complile. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document used to create the virtual machines. One virtual machine is created per node defined in the AllNodes array. .PARAMETER Path Specifies the directory path containing the PowerShell DSC configuration files. If this parameter is not specified, it defaults to the current working directory. .PARAMETER NodeName Specifies one or more node names contained in the PowerShell DSC configuration (.psd1) document to compile. If no node names are specified, all nodes defined within the configuration are compiled. .PARAMETER OutputPath Specifies the output path of the compiled DSC .mof file(s). If this parameter is not specified, it defaults to the current working directory. .PARAMETER AsJob Specifies that the cmdlet return a PowerShell job for each PowerShell DSC compilation instance. By default, the cmdlet will block the console until it finishes all comilation tasks. .EXAMPLE Start-DscCompilation -ConfigurationData .\config.psd1 -ConfigurationParameters @{ Credential = $credential; } Initiates compilation of all nodes defined in the .\config.psd1 file. For each node, the matching <NodeName.ps1> PowerShell DSC configuration file is loaded in to a separate PowerShell.exe process, and is subsequently called. The 'Credential' parameter is passed to each PowerShell.exe instance. The resulting PowerShell DSC .mof files are written out to the current working directory. #> [CmdletBinding(DefaultParameterSetName = 'Configuration')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.IO.FileInfo], [System.Management.Automation.Job])] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Configuration')] [ValidateNotNullOrEmpty()] [System.String[]] $Configuration, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')] [ValidateNotNullOrEmpty()] [System.IO.FileInfo[]] $InputObject, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [ValidateNotNullOrEmpty()] [System.String] $ConfigurationData, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [ValidateNotNullOrEmpty()] [System.String] $Path = (Get-Location -PSProvider Filesystem).Path, [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [ValidateNotNullOrEmpty()] [System.String[]] $NodeName, [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] $ConfigurationParameters, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OutputPath = (Get-Location -PSProvider Filesystem).Path, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $AsJob ) begin { if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $ConfigurationData = Resolve-Path -Path $ConfigurationData -ErrorAction Stop; } if (-not ($PSBoundParameters.ContainsKey('ConfigurationParameters'))) { ## Set the output path $ConfigurationParameters = @{ OutputPath = $OutputPath }; if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $ConfigurationParameters['ConfigurationData'] = $ConfigurationData; } } else { if (($PSBoundParameters.ContainsKey('OutputPath')) -and ($ConfigurationParameters.ContainsKey('OutputPath'))) { ## OutputPath was explicitly passed and is also defined in ConfigurationParameters Write-Warning -Message ($localized.ExplicitOutputPathWarning -f $OutputPath); $ConfigurationParameters['OutputPath'] = $OutputPath; } elseif (-not ($ConfigurationParameters.ContainsKey('OutputPath'))) { $ConfigurationParameters['OutputPath'] = $OutputPath; } if (($ConfigurationParameters.ContainsKey('ConfigurationData')) -and ($PSBoundParameters.ContainsKey('ConfigurationData'))) { ## ConfigurationData was explicitly passed and is also defined in ConfigurationParameters Write-Warning -Message ($localized.ExplicitConfigurationDataWarning -f $ConfigurationData); $ConfigurationParameters['ConfigurationData'] = $ConfigurationData; } elseif (-not ($ConfigurationParameters.ContainsKey('ConfigurationData')) -and ($PSBoundParameters.ContainsKey('ConfigurationData'))) { $ConfigurationParameters['ConfigurationData'] = $ConfigurationData; } } $filePaths = @(); } #end begin process { if ($PSCmdlet.ParameterSetName -eq 'InputObject') { foreach ($object in $InputObject) { Write-Verbose -Message ($localized.AddingConfiguration -f $object.FullName); if (Test-Path -Path $object.FullName) { $filePaths += $object.FullName; } else { Write-Error -Message ($localized.InvalidPathError -f 'File', $nodePath); } } #end foreach configuration fileinfo } elseif ($PSCmdlet.ParameterSetName -eq 'ConfigurationData') { <# Can't pass a hashtable to Start-Job due to Array/ArrayList serialization/deserialization issue. Need to pass -ConfigurationData by file path. ConfigurationData parameter property AllNodes needs to be a collection. + CategoryInfo : InvalidOperation: (:) [Write-Error], InvalidOperationException + FullyQualifiedErrorId : ConfiguratonDataAllNodesNeedHashtable,ValidateUpdate-ConfigurationData + PSComputerName : localhost #> $configData = ConvertTo-ConfigurationData -ConfigurationData $ConfigurationData; if (-not ($PSBoundParameters.ContainsKey('NodeName'))) { $nodeName = $configData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { $_.NodeName }; } foreach ($node in $nodeName) { $nodePath = Join-Path -Path $Path -ChildPath "$node.ps1"; Write-Verbose -Message ($localized.AddingConfiguration -f $nodePath); if (Test-Path -Path $nodePath) { $filePaths += $nodePath; } else { Write-Error -Message ($localized.InvalidPathError -f 'File', $nodePath); } } #end foreach node in configuration data } elseif ($PSCmdlet.ParameterSetName -eq 'Configuration') { foreach ($filePath in $Configuration) { try { $resolvedFilePath = Resolve-Path -Path $filePath -ErrorAction Stop; if (Test-Path -Path $resolvedFilePath) { Write-Verbose -Message ($localized.AddingConfiguration -f $resolvedFilePath); $filePaths += $resolvedFilePath; } else { Write-Error -Message ($localized.InvalidPathError -f 'File', $resolvedFilePath); } } catch { Write-Error -Message ($localized.InvalidPathError -f 'File', $filePath); } } #end foreach configuration file path } } #end process end { if ($filePaths.Count -eq 0) { throw ($localized.NoConfigurationToCompileError); } $jobs = @(); ## Start the jobs foreach ($filePath in $filePaths) { $invokeDscConfigurationCompilationParams = @{ Configuration = $filePath; AsJob = $true; } if ($ConfigurationParameters.Keys.Count -gt 0) { $invokeDscConfigurationCompilationParams['ConfigurationParameters'] = $ConfigurationParameters; } Write-Verbose -Message ($localized.AddingCompilationJob -f $filePath); $jobs += Start-DscConfigurationCompilation @invokeDscConfigurationCompilationParams; } if ($AsJob) { return $jobs; } else { ## Wait for compilation to finish $isJobsComplete = $false; $completedJobs = @(); $activity = $localized.CompilingConfigurationActivity; $totalPercentComplete = 0; $stopwatch = [System.Diagnostics.Stopwatch]::StartNew(); while ($isJobsComplete -eq $false) { $isJobsComplete = $true; $jobPercentComplete++; if ($jobPercentComplete -gt 100) { ## Loop progress $jobPercentComplete = 1; } if ($jobPercentComplete % 2 -eq 0) { ## Ensure total progresses at a different speed $totalPercentComplete++; if ($totalPercentComplete -gt 100) { $totalPercentComplete = 1; } } $elapsedTime = $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff'); Write-Progress -Id $pid -Activity $activity -Status $elapsedTime -PercentComplete $totalPercentComplete; foreach ($job in $jobs) { if ($job.HasMoreData -or $job.State -eq 'Running') { Write-Progress -Id $job.Id -ParentId $pid -Activity $job.Name -PercentComplete $jobPercentComplete; $isJobsComplete = $false; $job | Receive-Job; } elseif ($job.State -ne 'NotStarted') { if ($job -notin $completedJobs) { $elapsedTime = $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff'); $compilationStatus = $localized.ProcessedComilationStatus; Write-Verbose -Message ("{0} '{1}' in '{2}'." -f $compilationStatus, $job.Name, $elapsedTime); Write-Progress -Id $job.Id -ParentId $pid -Activity $job.Name -Completed; $completedJobs += $job; } } } #end foreach job Start-Sleep -Milliseconds 750; } #end while active job(s) $elapsedTime = $stopwatch.Elapsed.ToString('hh\:mm\:ss\.ff'); Write-Verbose -Message ($localized.CompletedCompilationProcessing -f $elapsedTime); Write-Progress -Id $pid -Activity $activity -Completed; $stopwatch = $null; } #end not job } #end end } #end function function Start-Lab { <# .SYNOPSIS Starts all VMs in a lab in a predefined order. .DESCRIPTION The Start-Lab cmdlet starts all nodes defined in a PowerShell DSC configuration document, in a preconfigured order. Unlike the standard Start-VM cmdlet, the Start-Lab cmdlet will read the specified PowerShell DSC configuration document and infer the required start up order. The PowerShell DSC configuration document can define the start/stop order of the virtual machines and the boot delay between each VM power operation. This is defined with the BootOrder and BootDelay properties. The lower the virtual machine's BootOrder index, the earlier it is started (in relation to the other VMs). For example, a VM with a BootOrder index of 10 will be started before a VM with a BootOrder index of 11. All virtual machines receive a BootOrder value of 99 unless specified otherwise. The delay between each power operation is defined with the BootDelay property. This value is specified in seconds and is enforced between starting or stopping a virtual machine. For example, a VM with a BootDelay of 30 will enforce a 30 second delay after being powered on or after the power off command is issued. All VMs receive a BootDelay value of 0 (no delay) unless specified otherwise. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document. .LINK about_ConfigurationData Stop-Lab #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $nodes = @(); $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { $nodes += [PSCustomObject] (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData); }; $currentGroupCount = 0; $bootGroups = $nodes | Sort-Object -Property BootOrder | Group-Object -Property BootOrder; $bootGroups | ForEach-Object { $nodeDisplayNames = $_.Group.NodeDisplayName; $nodeDisplayNamesString = $nodeDisplayNames -join ', '; $currentGroupCount++; [System.Int32] $percentComplete = ($currentGroupCount / $bootGroups.Count) * 100; $activity = $localized.ConfiguringNode -f $nodeDisplayNamesString; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; Write-Verbose -Message ($localized.StartingVirtualMachine -f $nodeDisplayNamesString); Hyper-V\Start-VM -Name $nodeDisplayNames; $maxGroupBootDelay = $_.Group.BootDelay | Sort-Object -Descending | Select-Object -First 1; if (($maxGroupBootDelay -gt 0) -and ($currentGroupCount -lt $bootGroups.Count)) { Write-Verbose -Message ($localized.WaitingForVirtualMachine -f $maxGroupBootDelay, $nodeDisplayNamesString); for ($i = 1; $i -le $maxGroupBootDelay; $i++) { [System.Int32] $waitPercentComplete = ($i / $maxGroupBootDelay) * 100; $waitActivity = $localized.WaitingForVirtualMachine -f $maxGroupBootDelay, $nodeDisplayNamesString; Write-Progress -ParentId 42 -Activity $waitActivity -PercentComplete $waitPercentComplete; Start-Sleep -Seconds 1; } Write-Progress -Activity $waitActivity -Completed; } #end if boot delay } #end foreach boot group Write-Progress -Id 42 -Activity $activity -Completed; } #end process } #end function Start-Lab function Start-LabConfiguration { <# .SYNOPSIS Invokes the deployment and configuration of a VM for each node defined in a PowerShell DSC configuration document. .DESCRIPTION The Start-LabConfiguration initiates the configuration of all nodes defined in a PowerShell DSC configuration document. The AllNodes array is enumerated and a virtual machine is created per node, using the NodeName property. If any existing virtual machine exists with the same name as the NodeName declaration of the AllNodes array, the cmdlet will throw an error. If this behaviour is not desired, you can forcibly remove the existing VM with the -Force parameter. NOTE: THE VIRTUAL MACHINE'S EXISTING DATA WILL BE LOST. The virtual machines' local administrator password must be specified when creating the lab VMs. The local administrator password can be specified as a [PSCredential] or a [SecureString] type. If a [PSCredential] is used then the username is not used. It is possible to override the module's virtual machine default settings by specifying the required property on the node hashtable in the PowerShell DSC configuration document. Default settings include the Operating System image to use, the amount of memory assigned to the virtual machine and/or the virtual switch to connect the virtual machine to. If the settings are not overridden, the module's defaults are used. Use the Get-LabVMDefault cmdlet to view the module's default values. Each virtual machine created by the Start-LabConfiguration cmdlet, has its PowerShell DSC configuraion (.mof) file injected into the VHD file as it is created. This configuration is then applied during the first boot process to ensure the virtual machine is configured as required. If the path to the VM's .mof files is not specified, the module's default Configuration directory is used. Use the Get-LabHostDefault cmdlet to view the module's default Configuration directory path. The virtual machine .mof files must be created before creating the lab. If any .mof files are missing, the Start-LabConfiguration cmdlet will generate an error. You can choose to ignore this error by specifying the -SkipMofCheck parameter. If you skip the .mof file check - and no .mof file is found - no configuration will be applied to the virtual machine's Operating System settings. When deploying a lab, the module will create a default baseline snapshot of all virtual machines. This snapshot can be used to revert all VMs back to their default configuration. If this snapshot is not required, it can be surpressed with the -NoSnapshot parameter. .NOTES The same local administrator password is used for all virtual machines created in the same lab configuration. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document used to create the virtual machines. One virtual machine is created per node defined in the AllNodes array. .PARAMETER Credential Specifies the local administrator password of all virtual machines in the lab configuration. The same password is used for all virtual machines in the same lab configuration. The username is not used. .PARAMETER Password Specifies the local administrator password of all virtual machines in the lab configuration. The same password is used for all virtual machines in the same lab configuration. .PARAMETER Path Specifies the directory path containing the individual PowerShell DSC .mof files. If not specified, the module's default location is used. .PARAMETER NoSnapshot Specifies that no default snapshot will be taken when creating the virtual machine. NOTE: If no default snapshot is not created, the lab cannot be restored to its initial configuration with the Reset-Lab cmdlet. .PARAMETER SkipMofCheck Specifies that the module will configure a virtual machines that do not have a corresponding .mof file located in the -Path specfified. By default, if any .mof file cannot be located then the cmdlet will generate an error. NOTE: If no .mof file is found and the -SkipMofCheck parameter is specified, no configuration will be applied to the virtual machine's Operating System configuration. .PARAMETER IgnorePendingReboot The host's configuration is checked before invoking a lab configuration, including checking for pending reboots. The -IgnorePendingReboot specifies that a pending reboot should be ignored and the lab configuration applied. .PARAMETER Force Specifies that any existing virtual machine with a matching name, will be removed and recreated. By default, if a virtual machine already exists with the same name, the cmdlet will generate an error. NOTE: If the -Force parameter is specified - and a virtual machine with the same name already exists - ALL EXISTING DATA WITHIN THE VM WILL BE LOST. .PARAMETER FeedCredential A [PSCredential] object containing the credentials to use when accessing a private Azure DevOps feed. .LINK about_ConfigurationData about_Bootstrap Get-LabHostDefault Set-LabHostDefault Get-LabVMDefault Set-LabVMDefault Reset-Lab #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'PSCredential')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Local administrator password of the VM. The username is NOT used. [Parameter(ParameterSetName = 'PSCredential', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential = (& $credentialCheckScriptBlock), ## Local administrator password of the VM. [Parameter(Mandatory, ParameterSetName = 'Password', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $Password, ## Path to .MOF files created from the DSC configuration [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Skip creating baseline snapshots [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoSnapshot, ## Forces a reconfiguration/redeployment of all nodes. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## Ignores missing MOF file [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $SkipMofCheck, ## Skips pending reboot check [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $IgnorePendingReboot, ## Credentials to access the a private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ) begin { ## If we have only a secure string, create a PSCredential if ($PSCmdlet.ParameterSetName -eq 'Password') { $Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList 'LocalAdministrator', $Password; } if (-not $Credential) { throw ($localized.CannotProcessCommandError -f 'Credential'); } elseif ($Credential.Password.Length -eq 0) { throw ($localized.CannotBindArgumentError -f 'Password'); } if (-not (Test-LabHostConfiguration -IgnorePendingReboot:$IgnorePendingReboot) -and (-not $Force)) { throw $localized.HostConfigurationTestError; } } process { Write-Verbose -Message $localized.StartedLabConfiguration; $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' }; ## There is an assumption here is all .mofs are in the same folder $resolveConfigurationPathParams = @{ ConfigurationData = $ConfigurationData; Name = $nodes | Select-Object -First 1 | ForEach-Object { $_.NodeName }; Path = $Path; UseDefaultPath = $SkipMofCheck; } $Path = Resolve-ConfigurationPath @resolveConfigurationPathParams; foreach ($node in $nodes) { $assertLabConfigurationMofParams = @{ ConfigurationData = $ConfigurationData; Name = $node.NodeName; Path = $Path; } Assert-LabConfigurationMof @assertLabConfigurationMofParams -SkipMofCheck:$SkipMofCheck; } #end foreach node $currentNodeCount = 0; foreach ($node in (Test-LabConfiguration -ConfigurationData $ConfigurationData -WarningAction SilentlyContinue)) { ## Ignore Lability warnings during the test phase, e.g. existing switches and .mof files $currentNodeCount++; [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1; $activity = $localized.ConfiguringNode -f $node.Name; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; if ($node.IsConfigured -and $Force) { Write-Verbose -Message ($localized.NodeForcedConfiguration -f $node.Name); $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'New-VM', $node.Name; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVM -f $node.Name); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $newLabVirtualMachineParams = @{ Name = $node.Name; ConfigurationData = $ConfigurationData; Path = $Path; NoSnapshot = $NoSnapshot; Credential = $Credential; } New-LabVirtualMachine @newLabVirtualMachineParams; } } elseif ($node.IsConfigured) { Write-Verbose -Message ($localized.NodeAlreadyConfigured -f $node.Name); } else { Write-Verbose -Message ($localized.NodeMissingOrMisconfigured -f $node.Name); $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Start-LabConfiguration', $node.Name; $verboseProcessMessage = Get-FormattedMessage -Message ($localized.CreatingVM -f $node.Name); if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $newLabVirtualMachineParams = @{ Name = $node.Name; ConfigurationData = $ConfigurationData; Path = $Path; NoSnapshot = $NoSnapshot; Credential = $Credential; FeedCredential = $FeedCredential; } [ref] $null = New-LabVirtualMachine @newLabVirtualMachineParams; } } } #end foreach node Write-Progress -Id 42 -Activity $activity -Completed; Write-Verbose -Message $localized.FinishedLabConfiguration; } #end process } #end function Start-LabConfiguration function Start-LabHostConfiguration { <# .SYNOPSIS Invokes the configuration of the lab host. .DESCRIPTION The Start-LabHostConfiguration cmdlet invokes the configuration of the local host computer. .PARAMETER IgnorePendingReboot Specifies a pending reboot does not fail the test. .LINK Test-LabHostConfiguration Get-LabHostConfiguration #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.Boolean])] param ( ## Ignores pending reboot check [Parameter()] [System.Management.Automation.SwitchParameter] $IgnorePendingReboot ) process { Write-Verbose -Message $localized.StartedHostConfiguration; ## Create required directory structure $hostDefaults = Get-ConfigurationData -Configuration Host; foreach ($property in $hostDefaults.PSObject.Properties) { if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) { ## DismPath is not a folder and should be ignored (#159) if ($property.Name -ne 'DismPath') { [ref] $null = New-Directory -Path $(Resolve-PathEx -Path $Property.Value) -ErrorAction Stop; } } } # Once all the path are created, check if the hostdefaults.Json file in the $env:ALLUSERSPROFILE is doesn't have entries with %SYSTEMDRIVE% in it # Many subsequent call are failing to Get-LabImage, Test-LabHostConfiguration which do not resolve the "%SYSTEMDRIVE%" in the path for Host defaults foreach ($property in $($hostDefaults.PSObject.Properties | Where-Object -Property TypeNameOfValue -eq 'System.String')) { if ($property.Value.Contains('%')) { # if the Path for host defaults contains a '%' character then resolve it $resolvedPath = Resolve-PathEx -Path $Property.Value; # update the hostdefaults Object $hostDefaults.($property.Name) = $resolvedPath; $hostDefaultsUpdated = $true; } } if ($hostDefaultsUpdated) { # Write the changes back to the json file in the $env:ALLUSERSPROFILE $hostDefaults | ConvertTo-Json | Out-File -FilePath $(Resolve-ConfigurationDataPath -Configuration Host); } $labHostSetupConfiguation = Get-LabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguation) { if ($IgnorePendingReboot -and ($configuration.ModuleName -eq 'xPendingReboot')) { Write-Warning -Message ($localized.IgnorePendingRebootWarning -f $configuration.Prefix); continue; } Import-LabDscResource -ModuleName $configuration.ModuleName -ResourceName $configuration.ResourceName -Prefix $configuration.Prefix -UseDefault:$configuration.UseDefault; Write-Verbose -Message ($localized.TestingNodeConfiguration -f $Configuration.Description); [ref] $null = Invoke-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters; ## TODO: Need to check for pending reboots.. } Write-Verbose -Message $localized.FinishedHostConfiguration; } #end process } #end function function Stop-Lab { <# .SYNOPSIS Stops all VMs in a lab in a predefined order. .DESCRIPTION The Stop-Lab cmdlet stops all nodes defined in a PowerShell DSC configuration document, in a preconfigured order. Unlike the standard Stop-VM cmdlet, the Stop-Lab cmdlet will read the specified PowerShell DSC configuration document and infer the required shutdown order. The PowerShell DSC configuration document can define the start/stop order of the virtual machines and the boot delay between each VM power operation. This is defined with the BootOrder and BootDelay properties. The higher the virtual machine's BootOrder index, the earlier it is stopped (in relation to the other VMs). For example, a VM with a BootOrder index of 11 will be stopped before a VM with a BootOrder index of 10. All virtual machines receive a BootOrder value of 99 unless specified otherwise. The delay between each power operation is defined with the BootDelay property. This value is specified in seconds and is enforced between starting or stopping a virtual machine. For example, a VM with a BootDelay of 30 will enforce a 30 second delay after being powered on or after the power off command is issued. All VMs receive a BootDelay value of 0 (no delay) unless specified otherwise. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document. .LINK about_ConfigurationData Start-Lab #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','nodes')] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $nodes = @(); $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' } | ForEach-Object { $nodes += [PSCustomObject] (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData); }; $currentGroupCount = 0; $bootGroups = $nodes | Sort-Object -Property BootOrder -Descending | Group-Object -Property BootOrder; $bootGroups | ForEach-Object { $nodeDisplayNames = $_.Group.NodeDisplayName; $nodeDisplayNamesString = $nodeDisplayNames -join ', '; $currentGroupCount++; [System.Int32] $percentComplete = ($currentGroupCount / $bootGroups.Count) * 100; $activity = $localized.ConfiguringNode -f $nodeDisplayNamesString; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; Write-Verbose -Message ($localized.StoppingVirtualMachine -f $nodeDisplayNamesString); Hyper-V\Stop-VM -Name $nodeDisplayNames -Force; } #end foreach boot group Write-Progress -Id 42 -Activity $activity -Completed; } #end process } #end function Stop-Lab function Test-LabConfiguration { <# .SYNOPSIS Tests the configuration of all VMs in a lab. .DESCRIPTION The Test-LabConfiguration determines whether all nodes defined in a PowerShell DSC configuration document are configured correctly and returns the results. WANRING: Only the virtual machine configuration is checked, not in the internal VM configuration. For example, the virtual machine's memory configuraiton, virtual switch configuration and processor count are tested. The VM's operating system configuration is not checked. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document used to create the virtual machines. Each node defined in the AllNodes array is tested. .LINK about_ConfigurationData Start-LabConfiguration #> [CmdletBinding()] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { Write-Verbose -Message $localized.StartedLabConfigurationTest; $currentNodeCount = 0; $nodes = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -ne '*' }; foreach ($node in $nodes) { $currentNodeCount++; $nodeProperties = Resolve-NodePropertyValue -NodeName $node.NodeName -ConfigurationData $ConfigurationData; [System.Int16] $percentComplete = (($currentNodeCount / $nodes.Count) * 100) - 1; $activity = $localized.ConfiguringNode -f $nodeProperties.NodeDisplayName; Write-Progress -Id 42 -Activity $activity -PercentComplete $percentComplete; $nodeResult = [PSCustomObject] @{ Name = $nodeProperties.NodeName; IsConfigured = Test-LabVM -Name $node.NodeName -ConfigurationData $ConfigurationData; DisplayName = $nodeProperties.NodeDisplayName; } Write-Output -InputObject $nodeResult; } Write-Verbose -Message $localized.FinishedLabConfigurationTest; } #end process } #end function Test-LabConfiguration function Test-LabHostConfiguration { <# .SYNOPSIS Tests the lab host's configuration. .DESCRIPTION The Test-LabHostConfiguration tests the current configuration of the lab host. .PARAMETER IgnorePendingReboot Specifies a pending reboot does not fail the test. .LINK Get-LabHostConfiguration Test-LabHostConfiguration #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Skips pending reboot check [Parameter()] [System.Management.Automation.SwitchParameter] $IgnorePendingReboot ) process { Write-Verbose -Message $localized.StartedHostConfigurationTest; ## Test folders/directories $hostDefaults = Get-ConfigurationData -Configuration Host; foreach ($property in $hostDefaults.PSObject.Properties) { if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) { ## DismPath is not a folder and should be ignored (#159) if ($property.Name -ne 'DismPath') { Write-Verbose -Message ($localized.TestingPathExists -f $property.Value); $resolvedPath = Resolve-PathEx -Path $property.Value; if (-not (Test-Path -Path $resolvedPath -PathType Container)) { Write-Verbose -Message ($localized.PathDoesNotExist -f $resolvedPath); return $false; } } } } $labHostSetupConfiguration = Get-LabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguration) { $importDscResourceParams = @{ ModuleName = $configuration.ModuleName; ResourceName = $configuration.ResourceName; Prefix = $configuration.Prefix; UseDefault = $configuration.UseDefault; } Import-LabDscResource @importDscResourceParams; Write-Verbose -Message ($localized.TestingNodeConfiguration -f $Configuration.Description); if (-not (Test-LabDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters)) { if ($configuration.Prefix -eq 'PendingReboot') { Write-Warning -Message $localized.PendingRebootWarning; if (-not $IgnorePendingReboot) { return $false; } } else { return $false; } } } #end foreach labHostSetupConfiguration Write-Verbose -Message $localized.FinishedHostConfigurationTest; return $true; } #end process } #end function function Test-LabImage { <# .SYNOPSIS Tests whether a master/parent lab image is present. .DESCRIPTION The Test-LabImage cmdlet returns whether a specified disk image is present. .PARAMETER Id Specifies the media Id of the image to test. .PARAMETER ConfigurationData Specifies a PowerShell DSC configuration data hashtable or a path to an existing PowerShell DSC .psd1 configuration document that contains the required media definition. .EXAMPLE Test-LabImage -Id 2016_x64_Datacenter_EN_Eval Tests whether the '-Id 2016_x64_Datacenter_EN_Eval' lab image is present. .LINK Get-LabImage #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Id, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if (Get-LabImage @PSBoundParameters) { return $true; } else { return $false; } } #end process } #end function Test-LabImage function Test-LabMedia { <# .SYNOPSIS Tests whether lab media has already been successfully downloaded. .DESCRIPTION The Test-LabMedia cmdlet will check whether the specified media Id has been downloaded and its checksum is correct. .PARAMETER Id Specifies the media Id (or alias) to test. #> [CmdletBinding()] [OutputType([System.Boolean])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Id ) process { $hostDefaults = Get-ConfigurationData -Configuration Host $media = Get-LabMedia -Id $Id if ($media) { if (-not $hostDefaults.DisableLocalFileCaching) { $testResourceDownloadParams = @{ DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename Uri = $media.Uri Checksum = $media.Checksum } return Test-ResourceDownload @testResourceDownloadParams } else { ## Local file resource caching is disabled return $true } } else { return $false } } #end process } #end function function Test-LabResource { <# .SYNOPSIS Tests whether a lab's resources are present. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## PowerShell DSC configuration document (.psd1) containing lab metadata. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Lab resource Id to test. [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourceId, ## Lab resource path [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $ResourcePath ) begin { if (-not $ResourcePath) { $hostDefaults = Get-ConfigurationData -Configuration Host; $ResourcePath = $hostDefaults.ResourcePath; } } process { if ($resourceId) { $resources = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId; } else { $resources = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource; } foreach ($resource in $resources) { $fileName = $resource.Id; if ($resource.Filename) { $fileName = $resource.Filename; } $testResourceDownloadParams = @{ DestinationPath = Join-Path -Path $ResourcePath -ChildPath $fileName;; Uri = $resource.Uri; } if ($resource.Checksum) { $testResourceDownloadParams['Checksum'] = $resource.Checksum; } if (-not (Test-ResourceDownload @testResourceDownloadParams)) { return $false; } } #end foreach resource return $true; } #end process } #end function function Test-LabStatus { <# .SYNOPSIS Queries computers' LCM state to determine whether an existing DSC configuration has applied successfully. .EXAMPLE Test-LabStatus -ComputerName CONTROLLER, XENAPP Tests the CONTROLLER and XENAPP computers' LCM state, using the current user credentials, returning $true if both have finished. .EXAMPLE Test-LabStatus -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential) Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query the LCM state, returning $true if both have finished. .EXAMPLE Test-LabStatus -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential) Prompts for credentials to connect to all the computers defined in the DSC configuration document (.psd1) and query the LCM state, returning $true if all have finished. .EXAMPLE Test-LabStatus -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential) Prompts for credentials to connect to the computers by their IPAddress node property as defined in the DSC configuration document (.psd1) and query the LCM state. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Connect to the computer name(s) specified. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')] [System.String[]] $ComputerName, ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [System.String] $PreferNodeProperty, ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN. ## The complete identifier for the remote endpoint is in the following format: ## ## <transport>://<server>:<port>/<ApplicationName> ## ## For example: `http://server01:8080/WSMAN` ## ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell. ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ApplicationName, ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are: ## ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy. ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. - ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. - ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. - ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP) ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one ## remote computer but collect data from or run additional commands on other remote computers. ## ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials ## are passed to it, the credentials can be used to control the network session. ## ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote ## computer anonymously, without using authentication. If the request is made anonymously, it returns no ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0). [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')] [System.String] $Authentication = 'Default', ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this ## action. Enter the certificate thumbprint of the certificate. ## ## Certificates are used in client certificate-based authentication. They can be mapped only to local user ## accounts; they do not work with domain accounts. ## ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert: ## drive. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $CertificateThumbprint, ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the ## default port is 80. When the transport is HTTPS, the default port is 443. ## ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's ## certificate common name (CN). [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $Port, ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote ## computer. By default, SSL is not used. ## ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the ## port that is used for the connection, and you specify this parameter, the command fails. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseSSL, ## Credential used to connect to the remote computer. [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential ) begin { ## Authentication might not be explicitly passed, add it so it gets splatted $PSBoundParameters['Authentication'] = $Authentication; } process { $labStatuses = @(Get-LabStatus @PSBoundParameters); $isDeploymentCompleted = $true; foreach ($labStatus in $labStatuses) { Write-Verbose -Message ($localized.TestingNodeStatus -f $labStatus.ComputerName, $labStatus.LCMState); if ($labStatus.Completed -eq $false) { ## VM still applying (or has failed) $isDeploymentCompleted = $false; } } return $isDeploymentCompleted; } #end process } #end function function Test-LabVM { <# .SYNOPSIS Checks whether the (external) lab virtual machine is configured as required. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Specifies the lab virtual machine/node name. [Parameter(ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if (-not $Name) { $Name = $ConfigurationData.AllNodes | Where-Object NodeName -ne '*' | ForEach-Object { $_.NodeName } } foreach ($vmName in $Name) { $isNodeCompliant = $true; $node = Resolve-NodePropertyValue -NodeName $vmName -ConfigurationData $ConfigurationData; Write-Verbose -Message ($localized.TestingNodeConfiguration -f $node.NodeDisplayName); Write-Verbose -Message ($localized.TestingVMConfiguration -f 'Image', $node.Media); if (-not (Test-LabImage -Id $node.Media -ConfigurationData $ConfigurationData)) { $isNodeCompliant = $false; } else { ## No point testing switch, vhdx and VM if the image isn't available Write-Verbose -Message ($localized.TestingVMConfiguration -f 'Virtual Switch', $node.SwitchName); foreach ($switchName in $node.SwitchName) { if (-not (Test-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData)) { $isNodeCompliant = $false; } } Write-Verbose -Message ($localized.TestingVMConfiguration -f 'VHDX', $node.Media); $testLabVMDiskParams = @{ Name = $node.NodeDisplayName; Media = $node.Media; ConfigurationData = $ConfigurationData; } if (-not (Test-LabVMDisk @testLabVMDiskParams -ErrorAction SilentlyContinue)) { $isNodeCompliant = $false; } else { ## No point testing VM if the VHDX isn't available Write-Verbose -Message ($localized.TestingVMConfiguration -f 'VM', $vmName); $testLabVirtualMachineParams = @{ Name = $node.NodeDisplayName; SwitchName = $node.SwitchName; Media = $node.Media; StartupMemory = $node.StartupMemory; MinimumMemory = $node.MinimumMemory; MaximumMemory = $node.MaximumMemory; ProcessorCount = $node.ProcessorCount; MACAddress = $node.MACAddress; SecureBoot = $node.SecureBoot; GuestIntegrationServices = $node.GuestIntegrationServices; ConfigurationData = $ConfigurationData; } if (-not (Test-LabVirtualMachine @testLabVirtualMachineParams)) { $isNodeCompliant = $false; } } } Write-Output -InputObject $isNodeCompliant; } #end foreach vm } #end process } #end function Test-LabVM function Unregister-LabMedia { <# .SYNOPSIS Unregisters a custom media entry. .DESCRIPTION The Unregister-LabMedia cmdlet allows removing custom media entries from the host's configuration. .LINK Get-LabMedia Register-LabMedia #> [CmdletBinding(SupportsShouldProcess)] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSProvideDefaultParameterValue', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( ## Specifies the custom media Id (or alias) to unregister. You cannot unregister the built-in media. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Id ) process { ## Get the custom media list $customMedia = Get-ConfigurationData -Configuration CustomMedia if (-not $customMedia) { ## We don't have anything defined Write-Warning -Message ($localized.NoCustomMediaFoundWarning -f $Id) return } else { ## Check if we have a matching Id $media = $customMedia | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id } if (-not $media) { ## We don't have a custom matching Id registered Write-Warning -Message ($localized.NoCustomMediaFoundWarning -f $Id) return } } $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Unregister-LabMedia', $Id $verboseProcessMessage = $localized.RemovingCustomMediaEntry -f $Id if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) { $customMedia = $customMedia | Where-Object { $_.Id -ne $Id } Write-Verbose -Message ($localized.SavingConfiguration -f $Id) Set-ConfigurationData -Configuration CustomMedia -InputObject @($customMedia) return $media } } #end process } #end function function Wait-Lab { <# .SYNOPSIS Queries computers' LCM state, using current user credentials, waiting for DSC configurations to be applied. .EXAMPLE Wait-Lab -ComputerName CONTROLLER, XENAPP Connects to the CONTROLLER and XENAPP machine to query and Wait for the LCM to finish applying the current configuration(s). .EXAMPLE Wait-Lab -ComputerName CONTROLLER, EXCHANGE -Credential (Get-Credential) Prompts for credentials to connect to the CONTROLLER and EXCHANGE computers to query and wait for the LCM to finish applying the current configuration(s). .EXAMPLE Wait-Lab -ConfigurationData .\TestLabGuide.psd1 -Credential (Get-Credential) Prompts for credentials to connect to all the computers defined in the DSC configuration document (.psd1) to query and wait for the LCM to finish applying the current configuration(s). .EXAMPLE Wait-Lab -ConfigurationData .\TestLabGuide.psd1 -PreferNodeProperty IPAddress -Credential (Get-Credential) Prompts for credentials to connect to all the computers by their IPAddress node property as defined in the DSC configuration document (.psd1) to query and wait for the LCM to finish applying the current configuration(s). .NOTES The default timeout is 15 minutes. #> #> [CmdletBinding()] param ( ## Connect to the computer name(s) specified. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ComputerName')] [System.String[]] $ComputerName, ## Connect to all nodes defined in the a Desired State Configuration (DSC) configuration (.psd1) document. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ConfigurationData')] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Use an alternative property for the computer name to connect to. Use this option when a configuration document's ## node name does not match the computer name, e.g. use the IPAddress property instead of the NodeName property. [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ConfigurationData')] [System.String] $PreferNodeProperty, ## Specifies the application name in the connection. The default value of the ApplicationName parameter is WSMAN. ## The complete identifier for the remote endpoint is in the following format: ## ## <transport>://<server>:<port>/<ApplicationName> ## ## For example: `http://server01:8080/WSMAN` ## ## Internet Information Services (IIS), which hosts the session, forwards requests with this endpoint to the ## specified application. This default setting of WSMAN is appropriate for most uses. This parameter is designed ## to be used if many computers establish remote connections to one computer that is running Windows PowerShell. ## In this case, IIS hosts Web Services for Management (WS-Management) for efficiency. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ApplicationName, ## Specifies the authentication mechanism to be used at the server. The acceptable values for this parameter are: ## ## - Basic. Basic is a scheme in which the user name and password are sent in clear text to the server or proxy. ## - Default. Use the authentication method implemented by the WS-Management protocol. This is the default. - ## Digest. Digest is a challenge-response scheme that uses a server-specified data string for the challenge. - ## Kerberos. The client computer and the server mutually authenticate by using Kerberos certificates. - ## Negotiate. Negotiate is a challenge-response scheme that negotiates with the server or proxy to determine the ## scheme to use for authentication. For example, this parameter value allows for negotiation to determine ## whether the Kerberos protocol or NTLM is used. - CredSSP. Use Credential Security Support Provider (CredSSP) ## authentication, which lets the user delegate credentials. This option is designed for commands that run on one ## remote computer but collect data from or run additional commands on other remote computers. ## ## Caution: CredSSP delegates the user credentials from the local computer to a remote computer. This practice ## increases the security risk of the remote operation. If the remote computer is compromised, when credentials ## are passed to it, the credentials can be used to control the network session. ## ## Important: If you do not specify the Authentication parameter,, the Test-WSMan request is sent to the remote ## computer anonymously, without using authentication. If the request is made anonymously, it returns no ## information that is specific to the operating-system version. Instead, this cmdlet displays null values for ## the operating system version and service pack level (OS: 0.0.0 SP: 0.0). [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('None','Default','Digest','Negotiate','Basic','Kerberos','ClientCertificate','Credssp')] [System.String] $Authentication = 'Default', ## Specifies the digital public key certificate (X509) of a user account that has permission to perform this ## action. Enter the certificate thumbprint of the certificate. ## ## Certificates are used in client certificate-based authentication. They can be mapped only to local user ## accounts; they do not work with domain accounts. ## ## To get a certificate thumbprint, use the Get-Item or Get-ChildItem command in the Windows PowerShell Cert: ## drive. [Parameter(ValueFromPipelineByPropertyName)] [System.String] $CertificateThumbprint, ## Specifies the port to use when the client connects to the WinRM service. When the transport is HTTP, the ## default port is 80. When the transport is HTTPS, the default port is 443. ## ## When you use HTTPS as the transport, the value of the ComputerName parameter must match the server's ## certificate common name (CN). [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $Port, ## Specifies that the Secure Sockets Layer (SSL) protocol is used to establish a connection to the remote ## computer. By default, SSL is not used. ## ## WS-Management encrypts all the Windows PowerShell content that is transmitted over the network. The UseSSL ## parameter lets you specify the additional protection of HTTPS instead of HTTP. If SSL is not available on the ## port that is used for the connection, and you specify this parameter, the command fails. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseSSL, ## Credential used to connect to the remote computer. [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, ## Specifies the interval between connection reties. Defaults to 60 seconds intervals. [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $RetryIntervalSeconds = 60, ## Specifies the number of connection attempts before failing. Defaults to 15 retries. [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $RetryCount = 15 ) begin { ## Authentication might not be explicitly passed, add it so it gets splatted $PSBoundParameters['Authentication'] = $Authentication; ## Remove parameters that can't be splatted again Test-LabStatus [ref] $null = $PSBoundParameters.Remove('RetryIntervalSeconds'); [ref] $null = $PSBoundParameters.Remove('RetryCount'); [ref] $null = $PSBoundParameters.Remove('AsJob'); } process { $timer = New-Object System.Diagnostics.Stopwatch; $timer.Start(); Write-Verbose -Message ($localized.StartingWaitForLabDeployment); for ($iteration = 0; $iteration -le $RetryCount; $iteration++) { if (-not (Test-LabStatus @PSBoundParameters)) { if ($iteration -lt $RetryCount) { ## Only sleep if this isn't the last iteration Write-Verbose -Message ($localized.SleepingWaitingForLabDeployment -f $RetryIntervalSeconds) Start-Sleep -Seconds $RetryIntervalSeconds; } } else { $elapsedTime = $timer.Elapsed.ToString('hh\:mm\:ss\.ff'); Write-Verbose -Message ($localized.WaitForLabDeploymentComplete -f $elapsedTime); $timer = $null; return; } } #end for ## We have timed out.. $elapsedTime = $timer.Elapsed.ToString('hh\:mm\:ss\.ff'); Write-Error -Message ($localized.WaitLabDeploymentTimeoutError -f $elapsedTime); $timer = $null; } #end process } #end function # SIG # Begin signature block # MIIuugYJKoZIhvcNAQcCoIIuqzCCLqcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAqDPjaT8xDwZu7 # d+mYeoQEbTOVjOzuHCj3FRoz3Hskx6CCE6QwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdYMIIFQKADAgECAhAIfHT3o/FeY5ksO94AUhTmMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjMxMDE4MDAwMDAwWhcNMjYxMjE2MjM1OTU5WjBgMQsw # CQYDVQQGEwJHQjEPMA0GA1UEBxMGTG9uZG9uMR8wHQYDVQQKExZWaXJ0dWFsIEVu # Z2luZSBMaW1pdGVkMR8wHQYDVQQDExZWaXJ0dWFsIEVuZ2luZSBMaW1pdGVkMIIC # IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtyhrsCMi6pgLcX5sWY7I09dO # WKweRHfDwW5AN6ffgLCYO9dqWWxvqu95FqnNVRyt1VNzEl3TevKVhRE0GGdirei3 # VqnFFjLDwD2jHhGY8qoSYyfffj/WYq2DkvNI62C3gUwSeP3FeqKRalc2c3V2v4jh # yEYhrgG3nfnWQ/Oq2xzuiCqHy1E4U+IKKDtrXls4JX2Z4J/uAHZIAyKfrcTRQOhZ # R4ZS1cQkeSBU9Urx578rOmxL0si0GAoaYQC49W7OimRelbahxZw/R+f5ch+C1ycU # CpeXLg+bFhpa0+EXnkGidlILZbJiZJn7qvMQTZgipQKZ8nhX3rtJLqTeodPWzcCk # tXQUy0q5fxhR3e6Ls7XQesq/G2yMcCMTCd6eltze0OgabvL6Xkhir5lATHaJtnmw # FlcKzRr1YXK1k1D84hWfKSAdUg8T1O72ztimbqFLg6WoC8M2qqsHcm2DOc9hM3i2 # CWRpegikitRvZ9i1wkA9arGh7+a7UD+nLh2hnGmO06wONLNqABOEn4JOVnNrQ1gY # eDeH9FDx7IYuAvMsfXG9Bo+I97TR2VfwDAx+ccR+UQLON3aQyFZ3BefYnvUu0gUR # ikEAnAS4Jnc3BHizgb0voz0iWRDjFoTTmCmrInCVDGc+5KMy0xyoUwdQvYvRGAWB # 61OCWnXBXbAEPniTZ80CAwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hf # EYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRuAv58K4EDYLmb7WNcxt5+r4NfnzA+BgNV # HSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj # ZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMD # MIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEu # Y3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCBlAYIKwYB # BQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv # bTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQw # CQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnXMg6efkBrwLIvd1Xmuh0dam # 9FhUtDEj+P5SIqdP/U4veOv66NEQhBHLbW2Dvrdm6ec0HMj9b4e8pt4ylKFzHIPj # fpuRffHVR9JQSx8qpryN6pP49DfCkAYeZGqjY3pGRzd/xQ0cfwcuYbUF+vwVk7tj # q8c93VHCM0rb5M4N2hD1Ze1pvZxtaf9QnFKFzgXZjr02K6bswQc2+n5jFCp7zV1f # KTstyb68rhSJBWKK1tMeFk6a6HXr5buTD3skluC0oyPmD7yAd97r2owjDMEveEso # kADP/z7XQk7wqbwbpi4W6Uju2qHK/9UUsVRF5KTVEAIzVw2V1Aq/Jh3JuSV7b7C1 # 4CghNekltBb+w7YVp8/IFcj7axqnpNQ/+f7RVc3A5hyjV+MkoSwn8Sg7a7hn6SzX # jec/TfRVvWCmG94MQHko+6206uIXrZnmQ6UQYFyOHRlyKDEozzkZhIcVlsZloUjL # 3FZ5V/l8TIIzbc3bkEnu4iByksNvRxI6c5264OLauYlWv50ZUPwXmZ9gX8bs3BqZ # avbGUrOW2PIjtWhtvs4zHhMBCoU1Z0OMvXcF9rUDqefmVCZK46xz3DGKVkDQnxY6 # UWQ3GL60/lEzju4YS99LJVQks2UGmP6LAzlEZ1dnGqi1aQ51OidCYEs39B75PsvO # By2iAR8pBVi/byWBypExghpsMIIaaAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBH # NCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAh8dPej8V5j # mSw73gBSFOYwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA # oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w # DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgqY/woHF+WpuaErBW3zboWF7T # icTf+z7IvxrvkNfbutYwDQYJKoZIhvcNAQEBBQAEggIASvQozS+eGQtu+DxihscR # AGQa+VlzB1ud1Pw1NlYHSbOHH8ZtUfI6W+FxzjBdIWWxYAzuLTyIbsoAGg17pSYh # bjDh5DogjsBfFg8WbXnE0mNSNeF7eR59ZTWQTzprWxYFPUed593wo+WOIlGBeUvN # UDtKU8WD50a/a7d7l+zWnPVYlHQPx13DQOgGJAkdWSMVVznMlTfHXPX59wbOwm9V # UZcVlr3ZBRBenMIpIv/AwOtVCyJO+Tdsk/mHw+wm2ZnqI9WQVUYe+mM2UtNu1Prx # IC5bo4vAarNpMGOhF0vnZzPv9KZoAZ20Y5xxHbbuOA5qJLIrXxAU/+Pm8oBUaExu # r6MBucMeo5lZGZ2IKVq5Bttzw55RMTz8xUvdmhlz++H1ENhHxG+aBJ2S+jaXu1Z6 # gW4pEq2HQaEwI8toaD0bsMDUR3dvWflz5Y5qIJ17SKNGidDN2/2qmFD7ulODm0Wn # OILCLRb7KgDanJVxg7LCBwL4RbnYCCiSVITKOQMKf2ifR0bStW1DNzIJUzW1J14X # pPOShaC/RBDwVANjpjxk1EtrgzdLw1wA72A39RV/5y166wtlo4xNjN+V1FMZqiiX # zgM8DkK1FdNYn6TK+yv6wpCC0y1P/+65Fno/N+NnF82ErK5YNC6jnZ7ukZS9s4MK # EIRQeP5eoUT7CblxI0TbGjChghc5MIIXNQYKKwYBBAGCNwMDATGCFyUwghchBgkq # hkiG9w0BBwKgghcSMIIXDgIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ # EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgztAkn3Hg # egs3hK8yjWL5KQRfFbRais3wyP0uB0EZEsMCEFDGeydtSv9kZHshgqnyn1gYDzIw # MjUwNDA4MTYyNjE1WqCCEwMwgga8MIIEpKADAgECAhALrma8Wrp/lYfG+ekE4zME # MA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI # QTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjQwOTI2MDAwMDAwWhcNMzUxMTI1MjM1 # OTU5WjBCMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxIDAeBgNVBAMT # F0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEAvmpzn/aVIauWMLpbbeZZo7Xo/ZEfGMSIO2qZ46XB/QowIEMSvgjE # dEZ3v4vrrTHleW1JWGErrjOL0J4L0HqVR1czSzvUQ5xF7z4IQmn7dHY7yijvoQ7u # jm0u6yXF2v1CrzZopykD07/9fpAT4BxpT9vJoJqAsP8YuhRvflJ9YeHjes4fduks # THulntq9WelRWY++TFPxzZrbILRYynyEy7rS1lHQKFpXvo2GePfsMRhNf1F41nyE # g5h7iOXv+vjX0K8RhUisfqw3TTLHj1uhS66YX2LZPxS4oaf33rp9HlfqSBePejlY # eEdU740GKQM7SaVSH3TbBL8R6HwX9QVpGnXPlKdE4fBIn5BBFnV+KwPxRNUNK6lY # k2y1WSKour4hJN0SMkoaNV8hyyADiX1xuTxKaXN12HgR+8WulU2d6zhzXomJ2Ple # I9V2yfmfXSPGYanGgxzqI+ShoOGLomMd3mJt92nm7Mheng/TBeSA2z4I78JpwGpT # RHiT7yHqBiV2ngUIyCtd0pZ8zg3S7bk4QC4RrcnKJ3FbjyPAGogmoiZ33c1HG93V # p6lJ415ERcC7bFQMRbxqrMVANiav1k425zYyFMyLNyE1QulQSgDpW9rtvVcIH7Wv # G9sqYup9j8z9J1XqbBZPJ5XLln8mS8wWmdDLnBHXgYly/p1DhoQo5fkCAwEAAaOC # AYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM # MAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAf # BgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQUn1csA3cO # KBWQZqVjXu5Pkh92oFswWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFt # cGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6 # Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVT # dGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAPa0eH3aZW+M4hBJH2UOR # 9hHbm04IHdEoT8/T3HuBSyZeq3jSi5GXeWP7xCKhVireKCnCs+8GZl2uVYFvQe+p # PTScVJeCZSsMo1JCoZN2mMew/L4tpqVNbSpWO9QGFwfMEy60HofN6V51sMLMXNTL # fhVqs+e8haupWiArSozyAmGH/6oMQAh078qRh6wvJNU6gnh5OruCP1QUAvVSu4kq # VOcJVozZR5RRb/zPd++PGE3qF1P3xWvYViUJLsxtvge/mzA75oBfFZSbdakHJe2B # VDGIGVNVjOp8sNt70+kEoMF+T6tptMUNlehSR7vM+C13v9+9ZOUKzfRUAYSyyEmY # tsnpltD/GWX8eM70ls1V6QG/ZOB6b6Yum1HvIiulqJ1Elesj5TMHq8CWT/xrW7tw # ipXTJ5/i5pkU5E16RSBAdOp12aw8IQhhA/vEbFkEiF2abhuFixUDobZaA0VhqAsM # HOmaT3XThZDNi5U2zHKhUs5uHHdG6BoQau75KiNbh0c+hatSF+02kULkftARjsyE # pHKsF7u5zKRbt5oK5YGwFvgc4pEVUNytmB3BpIiowOIIuDgP5M9WArHYSAR16gc0 # dP2XdkMEP5eBsX7bf/MGN4K3HP50v/01ZHo/Z5lGLvNwQ7XHBx1yomzLP8lx4Q1z # ZKDyHcp4VQJLu2kWTsKsOqQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5b # MA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5 # NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG # A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh # bXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPB # PXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/ # nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLc # Z47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mf # XazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3N # Ng1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yem # j052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g # 3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD # 4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDS # LFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwM # O1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU # 7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPO # vxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQ # TGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWae # LJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPBy # oyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfB # wWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8l # Y5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/ # O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbb # bxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3 # OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBl # dkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt # 1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwF # ADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL # ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElE # IFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYD # VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln # aWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKn # JS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/W # BTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHi # LQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhm # V1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHE # tWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 # MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mX # aXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZ # xd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfh # vbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvl # EFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn1 # 5GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV # HQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4Ix # LVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggr # BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdo # dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290 # Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAA # MA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzs # hV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre # +i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8v # C6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38 # dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNr # Iv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEw # dzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNV # BAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1w # aW5nIENBAhALrma8Wrp/lYfG+ekE4zMEMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqG # SIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwNDA4MTYy # NjE1WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZb # RTAvBgkqhkiG9w0BCQQxIgQgi01fvStPn/A8V9z9pJ+3wKjik+Xl5uL8YLRV5tZk # fmcwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13Pb # BdZluQWTmEOPmtswDQYJKoZIhvcNAQEBBQAEggIAEDB5uHPSFU00zX8uVuoBJL8U # b9c2QRrPmrLoz7MJV8tLv0ya1rXw3h0e0Ro9+fHfbyrC+V5RMTKu0KQ48UMSZigf # YNr75oeohp3eeZyiyWNXCHR5mroeEalSAf6ghUoJc34LcsS7BAOb5HE6DaJ0KowU # j8ZI6vl51wiGMcD58IxVsBt80NXtK3uiC6ShmzMMOGvhJiM4eGSleZi+adwXMwNm # vlkT4LWuwV38mFTU5o/kWxoGAMDBOUSyTrTltMus2qHTLb/bl+T+na9bBEDyCYtg # 6vw6SkXVtbqUkHx2T8d3HvUR29Vf/Mft2b8nqlrmN2wcOAug0SH15PGzShD9wUr7 # u6lnHG+4INVrzpo6UcNLhHwJlz2bgEubSeksdtO9RMv6UcMi6l9gFXuFVyEgCmdz # IvmRTlvpU3p+zCmHxFmCHVapJzzd6doCJPrmwpdaQNV2G06A1bfQhcXMKGu70obe # g161rI5yvlAhxb/5ng27T0Tk7mV5lx6F5Qn0XSIRMi8DgariXmn/ymGSUOtb5Lop # XbuDL4ag5gTu/RTyKisB+caXuzxO03TBpFodwmyhp9jtFbKY2DParXda3PpOr7/E # WO0XbbusCgIZi28WeO2oYRtvtRC/LidCEHFQjrxgvSFA3W0K6Ke4Tf6V6J2nsgx0 # Z3unlUI8lpMxW874m/M= # SIG # End signature block |