Src/Private/Private.ps1
function Add-DiskImageHotfix { <# .SYMOPSIS Adds a Windows update/hotfix package to an image. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Id, ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk ## Disk image partition scheme [Parameter(Mandatory)] [ValidateSet('MBR','GPT')] [System.String] $PartitionStyle, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if ($PartitionStyle -eq 'MBR') { $partitionType = 'IFS'; } elseif ($PartitionStyle -eq 'GPT') { $partitionType = 'Basic'; } $vhdDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType $partitionType; $resolveLabMediaParams = @{ Id = $Id; } if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $resolveLabMediaParams['ConfigurationData'] = $ConfigurationData; } $media = Resolve-LabMedia @resolveLabMediaParams; foreach ($hotfix in $media.Hotfixes) { if ($hotfix.Id -and $hotfix.Uri) { $invokeLabMediaDownloadParams = @{ Id = $hotfix.Id; Uri = $hotfix.Uri; } if ($null -ne $hotfix.Checksum) { $invokeLabMediaDownloadParams['Checksum'] = $hotfix.Checksum; } $hotfixFileInfo = Invoke-LabMediaDownload @invokeLabMediaDownloadParams; $packageName = [System.IO.Path]::GetFileNameWithoutExtension($hotfixFileInfo.FullName); Add-DiskImagePackage -Name $packageName -Path $hotfixFileInfo.FullName -DestinationPath $vhdDriveLetter; } } } #end process } #end function function Add-DiskImagePackage { <# .SYNOPSIS Adds a Windows package (.cab) to an image. This is implmented primarily to support injection of packages into Nano server images. .NOTES The real difference between a hotfix and package is that a package can either be specified in the master VHD(X) image creation OR be injected into VHD(X) differencing disk. #> [CmdletBinding()] param ( ## Package name (used for logging) [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Name, ## File path to the package (.cab) file [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Path, ## Destination operating system path (mounted VHD), i.e. G:\ [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $DestinationPath ) begin { ## We just want the drive letter if ($DestinationPath.Length -gt 1) { $DestinationPath = $DestinationPath.Substring(0,1); } } process { $logPath = '{0}:\Windows\Logs\{1}' -f $DestinationPath, $labDefaults.ModuleName; [ref] $null = New-Directory -Path $logPath -Verbose:$false; Write-Verbose -Message ($localized.AddingImagePackage -f $Name, $DestinationPath); $addWindowsPackageParams = @{ PackagePath = $Path; Path = '{0}:\' -f $DestinationPath; LogPath = '{0}\{1}.log' -f $logPath, $Name; LogLevel = 'Errors'; } [ref] $null = Microsoft.Dism.Powershell\Add-WindowsPackage @addWindowsPackageParams -Verbose:$false; } #end process } #end function function Add-LabImageWindowsOptionalFeature { <# .SYMOPSIS Enables Windows optional features to an image. #> [CmdletBinding()] param ( ## Source package file path [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $ImagePath, ## Mounted VHD(X) Operating System disk drive [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, ## Windows packages to add to the image after expansion [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String[]] $WindowsOptionalFeature, ## DISM log path [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $LogPath = $DestinationPath ) process { Write-Verbose -Message ($localized.AddingWindowsFeature -f ($WindowsOptionalFeature -join ','), $DestinationPath); $enableWindowsOptionalFeatureParams = @{ Source = $ImagePath; Path = $DestinationPath; LogPath = $LogPath; FeatureName = $WindowsOptionalFeature; LimitAccess = $true; All = $true; Verbose = $false; } $dismOutput = Microsoft.Dism.Powershell\Enable-WindowsOptionalFeature @enableWindowsOptionalFeatureParams; Write-Debug -Message $dismOutput; } #end process } #end function Add-LabImageWindowsOptionalFeature function Add-LabImageWindowsPackage { <# .SYNOPSIS Adds a Windows package to an image. #> [CmdletBinding()] param ( ## Windows packages (.cab) files to add to the image after expansion [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String[]] $Package, ## Path to the .cab files [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $PackagePath, ## Mounted VHD(X) Operating System disk drive [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, ## Package localization directory/extension (primarily used for Nano Server) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $PackageLocale = 'en-US', ## DISM log path [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $LogPath = $DestinationPath ) process { foreach ($packageName in $Package) { Write-Verbose -Message ($localized.AddingWindowsPackage -f $packagename, $DestinationPath); $packageFilename = '{0}.cab' -f $packageName; $packageFilePath = Join-Path -Path $PackagePath -ChildPath $packageFilename; Add-DiskImagePackage -Name $packageName -Path $packageFilePath -DestinationPath $DestinationPath; ## Check for language-specific package (Change from Server 2016 TP releases and Server 2016 Nano RTM) if ($PSBoundParameters.ContainsKey('PackageLocale')) { $localizedPackageName = '{0}_{1}' -f $packageName, $packageLocale; $localizedPackageFilename = '{0}.cab' -f $localizedPackageName; $localizedPackageDirectoryPath = Join-Path -Path $PackagePath -ChildPath $PackageLocale; $localizedPackagePath = Join-Path -Path $localizedPackageDirectoryPath -ChildPath $localizedPackageFilename; if (Test-Path -Path $localizedPackagePath -PathType Leaf) { Write-Verbose -Message ($localized.AddingLocalizedWindowsPackage -f $localizedPackageName, $DestinationPath); $addDiskImagePackageParams = @{ Name = $localizedPackageName; Path = $localizedPackagePath; DestinationPath = $DestinationPath; } Add-DiskImagePackage @addDiskImagePackageParams; } } } #end foreach package } #end process } #end function Add-LabImageWindowsPackage function Assert-BitLockerFDV { <# .SYNOPSIS Enables BitLocker full disk write protection (if enabled on the host system) #> [CmdletBinding()] param ( ) process { if ($fdvDenyWriteAccess) { Write-Verbose -Message $localized.EnablingBitlockerWriteProtection Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE' -Name 'FDVDenyWriteAccess' -Value 1 } } #end process } #end function function Assert-LabConfigurationMof { <# .SYNOPSIS Checks for node MOF and meta MOF configuration files. #> [CmdletBinding()] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Lab vm/node name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## Path to .MOF files created from the DSC configuration [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Path = (Get-LabHostDscConfigurationPath), ## Ignores missing MOF file [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $SkipMofCheck ) process { $Path = Resolve-Path -Path $Path -ErrorAction Stop; $node = $ConfigurationData.AllNodes | Where-Object { $_.NodeName -eq $Name }; $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $node.NodeName); Write-Verbose -Message ($localized.CheckingForNodeFile -f $mofPath); if (-not (Test-Path -Path $mofPath -PathType Leaf)) { if ($SkipMofCheck) { Write-Warning -Message ($localized.CannotLocateMofFileError -f $mofPath) } else { throw ($localized.CannotLocateMofFileError -f $mofPath); } } $metaMofPath = Join-Path -Path $Path -ChildPath ('{0}.meta.mof' -f $node.NodeName); Write-Verbose -Message ($localized.CheckingForNodeFile -f $metaMofPath); if (-not (Test-Path -Path $metaMofPath -PathType Leaf)) { Write-Warning -Message ($localized.CannotLocateLCMFileWarning -f $metaMofPath); } } #end process } #end function Assert-LabConfigurationMof function Assert-TimeZone { <# .SYNOPSIS Validates a timezone string. #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $TimeZone ) process { try { $TZ = [TimeZoneInfo]::FindSystemTimeZoneById($TimeZone) return $TZ.Id; } catch [System.TimeZoneNotFoundException] { throw $_; } } #end process } #end function function Assert-VirtualMachineHardDiskDriveParameter { <# .SYNOPSIS Ensures parameters specified in the Lability_HardDiskDrive hashtable are correct. #> [CmdletBinding()] param ( ## Virtual hard disk generation [Parameter()] [System.String] $Generation, ## Vhd size. Minimum 3MB and maximum 2,040GB [Parameter()] [Alias('Size')] [System.UInt64] $MaximumSizeBytes, [Parameter()] [System.String] $VhdPath, ## Virtual hard disk type [Parameter()] [ValidateSet('Dynamic','Fixed')] [System.String] $Type, [Parameter()] [System.UInt32] $VMGeneration ) process { if ($PSBoundParameters.ContainsKey('VhdPath')) { if (($PSBoundParameters.Keys -contains 'Generation') -or ($PSBoundParameters.Keys -contains 'MaximumSizeBytes')) { throw ($localized.CannotResoleVhdParameterError); } if (-not (Test-Path -Path $VhdPath -PathType Leaf)) { throw ($localized.CannotLocateVhdError -f $VhdPath); } } elseif ($PSBoundParameters.ContainsKey('Generation')) { ## A Generation 2 virtual machine can only utilize VHDX. if (($VMGeneration -eq 2) -and ($Generation -eq 'VHD')) { throw ($localized.InvalidVhdTypeError -f 'VHD', 2); } if (($MaximumSizeBytes -lt 3145728) -or ($MaximumSizeBytes -gt 2190433320960)) { throw ($localized.InvalidVhdSizeError -f $MaximumSizeBytes); } } else { ## Nothing has been specified throw ($localized.CannotProcessCommandError -f '"Generation, MaximumSizeBytes, VhdPath'); } } #end process } #end function function Clear-LabVirtualMachine { <# .SYNOPSIS Removes the current configuration a virtual machine. .DESCRIPTION Invokes/sets a virtual machine configuration using the xVMHyperV DSC resource. .NOTES Should be Remove-LabVirtualMachine but that and Remove-LabVM are already used. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String[]] $SwitchName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Media, [Parameter(Mandatory)] [System.UInt64] $StartupMemory, [Parameter(Mandatory)] [System.UInt64] $MinimumMemory, [Parameter(Mandatory)] [System.UInt64] $MaximumMemory, [Parameter(Mandatory)] [System.Int32] $ProcessorCount, [Parameter()] [AllowNull()] [System.String[]] $MACAddress, [Parameter()] [System.Boolean] $SecureBoot, [Parameter()] [System.Boolean] $GuestIntegrationServices, [Parameter()] [System.Boolean] $AutomaticCheckpoints, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if ($PSCmdlet.ShouldProcess($Name)) { ## Resolve the xVMHyperV resource parameters $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters; $vmHyperVParams['Ensure'] = 'Absent'; Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM; Invoke-LabDscResource -ResourceName VM -Parameters $vmHyperVParams -ErrorAction SilentlyContinue; } } #end process } #end function function Close-GitHubZipArchive { <# .SYNOPSIS Tidies up and closes Zip Archive and file handles #> [CmdletBinding()] param () process { Write-Verbose -Message ($localized.ClosingZipArchive -f $Path); if ($null -ne $zipArchive) { $zipArchive.Dispose(); } if ($null -ne $fileStream) { $fileStream.Close(); } } # end process } #end function function Close-ZipArchive { <# .SYNOPSIS Tidies up and closes Zip Archive and file handles #> [CmdletBinding()] param () process { Write-Verbose -Message ($localized.ClosingZipArchive -f $Path); if ($null -ne $zipArchive) { $zipArchive.Dispose(); } if ($null -ne $fileStream) { $fileStream.Close(); } } # end process } #end function function Convert-PSObjectToHashtable { <# .SYNOPSIS Converts a PSCustomObject's properties to a hashtable. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Object to convert to a hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSObject[]] $InputObject, ## Do not add empty/null values to the generated hashtable [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $IgnoreNullValues ) process { foreach ($object in $InputObject) { $hashtable = @{ } foreach ($property in $object.PSObject.Properties) { if ($IgnoreNullValues -and ($property.TypeNameOfValue -ne 'System.Object[]')) { if ([System.String]::IsNullOrEmpty($property.Value)) { ## Ignore empty strings continue; } } if ($property.TypeNameOfValue -eq 'System.Management.Automation.PSCustomObject') { ## Convert nested custom objects to hashtables $hashtable[$property.Name] = Convert-PSObjectToHashtable -InputObject $property.Value -IgnoreNullValues:$IgnoreNullValues; } elseif ($property.TypeNameOfValue -eq 'System.Object[]') { ## Convert nested arrays of objects to an array of hashtables (#262) $nestedCollection = @(); foreach ($object in $property.Value) { if ($object -is 'System.Management.Automation.PSCustomObject') { $nestedCollection += Convert-PSObjectToHashtable -InputObject $object -IgnoreNullValues:$IgnoreNullValues; } else { ## We have an array of primitive types, e.g. strings $nestedCollection += $object; } } $hashtable[$property.Name] = $nestedCollection; } else { $hashtable[$property.Name] = $property.Value; } } #end foreach property Write-Output $hashtable; } } #end proicess } #end function function ConvertTo-ConfigurationData { <# .SYNOPSIS Converts a file path string to a hashtable. This mimics the -ConfigurationData parameter of the Start-DscConfiguration cmdlet. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $ConfigurationData ) process { $configurationDataPath = Resolve-Path -Path $ConfigurationData -ErrorAction Stop; if (-not (Test-Path -Path $configurationDataPath -PathType Leaf)) { throw ($localized.InvalidConfigurationDataFileError -f $ConfigurationData); } elseif ([System.IO.Path]::GetExtension($configurationDataPath) -ne '.psd1') { throw ($localized.InvalidConfigurationDataFileError -f $ConfigurationData); } $configurationDataContent = Get-Content -Path $configurationDataPath -Raw; $configData = Invoke-Command -ScriptBlock ([System.Management.Automation.ScriptBlock]::Create($configurationDataContent)); if ($configData -isnot [System.Collections.Hashtable]) { throw ($localized.InvalidConfigurationDataType -f $configData.GetType()); } return $configData; } #end process } #end function ConvertTo-ConfigurationData function CopyDirectory { <# .SYNOPSIS Copies a directory structure with progress. #> [CmdletBinding()] param ( ## Source directory path [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.IO.DirectoryInfo] $SourcePath, ## Destination directory path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.IO.DirectoryInfo] $DestinationPath, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Management.Automation.SwitchParameter] $Force ) begin { if ((Get-Item $SourcePath) -isnot [System.IO.DirectoryInfo]) { throw ($localized.CannotProcessArguentError -f 'CopyDirectory', 'SourcePath', $SourcePath, 'System.IO.DirectoryInfo'); } elseif (Test-Path -Path $SourcePath -PathType Leaf) { throw ($localized.InvalidDestinationPathError -f $DestinationPath); } } process { $activity = $localized.CopyingResource -f $SourcePath.FullName, $DestinationPath; $status = $localized.EnumeratingPath -f $SourcePath; Write-Progress -Activity $activity -Status $status -PercentComplete 0; $fileList = Get-ChildItem -Path $SourcePath -File -Recurse; $currentDestinationPath = $SourcePath; for ($i = 0; $i -lt $fileList.Count; $i++) { if ($currentDestinationPath -ne $fileList[$i].DirectoryName) { ## We have a change of directory $destinationDirectoryName = $fileList[$i].DirectoryName.Substring($SourcePath.FullName.Trim('\').Length); $destinationDirectoryPath = Join-Path -Path $DestinationPath -ChildPath $destinationDirectoryName; [ref] $null = New-Item -Path $destinationDirectoryPath -ItemType Directory -ErrorAction Ignore; $currentDestinationPath = $fileList[$i].DirectoryName; } if (($i % 5) -eq 0) { [System.Int16] $percentComplete = (($i + 1) / $fileList.Count) * 100; $status = $localized.CopyingResourceStatus -f $i, $fileList.Count, $percentComplete; Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete; } $targetPath = Join-Path -Path $DestinationPath -ChildPath $fileList[$i].FullName.Replace($SourcePath, ''); # This retry method is needed when AV scanners hold locks on files for a moment too long $copyTryCount = 1; while ($true) { try { Copy-Item -Path $fileList[$i].FullName -Destination $targetPath -Force:$Force; break; } catch { $copyTryCount += 1; if ($copyTryCount -gt 5) { throw; } Write-Warning -Message ($localized.FileCopyFailedRetryingWarning -f $fileList[$i].FullName, $targetPath); Start-Sleep -Seconds 1; } } } #end for Write-Progress -Activity $activity -Completed; } #end process } #end function function Copy-LabModule { <# .SYNOPSIS Copies Lability PowerShell and DSC Resource modules. #> [CmdletBinding(SupportsShouldProcess)] param ( ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $ConfigurationData, ## Module type(s) to install [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('Module','DscResource')] [System.String[]] $ModuleType, ## Install a specific node's modules [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $NodeName, ## Destination module path [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath ) begin { [System.Collections.Hashtable] $ConfigurationData = ConvertTo-ConfigurationData -ConfigurationData $ConfigurationData; } process { ## Copy PowerShell modules if ($ModuleType -contains 'Module') { if ($PSBoundParameters.ContainsKey('NodeName')) { $resolveLabModuleParams = @{ NodeName = $NodeName; ConfigurationData = $ConfigurationData; ModuleType = 'Module'; } $powerShellModules = Resolve-LabModule @resolveLabModuleParams; } else { $powerShellModules = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Module; } if ($null -ne $powerShellModules) { Write-Verbose -Message ($localized.CopyingPowerShellModules -f $DestinationPath); if ($PSCmdlet.ShouldProcess($DestinationPath, $localized.InstallModulesConfirmation)) { Expand-LabModuleCache -Module $powerShellModules -DestinationPath $DestinationPath; } } } #end if PowerShell modules ## Copy DSC resource modules if ($ModuleType -contains 'DscResource') { if ($PSBoundParameters.ContainsKey('NodeName')) { $resolveLabModuleParams = @{ NodeName = $NodeName; ConfigurationData = $ConfigurationData; ModuleType = 'DscResource'; } $dscResourceModules = Resolve-LabModule @resolveLabModuleParams; } else { $dscResourceModules = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).DSCResource; } if ($null -ne $dscResourceModules) { Write-Verbose -Message ($localized.CopyingDscResourceModules -f $DestinationPath); if ($PSCmdlet.ShouldProcess($DestinationPath, $localized.InstallDscResourcesConfirmation)) { Expand-LabModuleCache -Module $dscResourceModules -DestinationPath $DestinationPath; } } } #end if DSC resources } #end process } #end function function Disable-BitLockerFDV { <# .SYNOPSIS Disables BitLocker full disk write protection (if enabled on the host system) #> [CmdletBinding()] param ( ) process { if ($fdvDenyWriteAccess) { Write-Verbose -Message $localized.DisablingBitlockerWriteProtection Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Policies\Microsoft\FVE' -Name 'FDVDenyWriteAccess' -Value 0; } } #end process } #end function function Expand-GitHubZipArchive { <# .SYNOPSIS Extracts a GitHub Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the GitHubRepository (https://github.com/IainBrighton/GitHubRepositoryCompression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] [OutputType([System.IO.FileInfo])] param ( # Source path to the Zip Archive. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [Alias('PSPath','FullName')] [System.String[]] $Path, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [System.String] $DestinationPath, # GitHub repository name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepository, # Overwrite existing files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { ## Validate destination path if (-not (Test-Path -Path $DestinationPath -IsValid)) { throw ($localized.InvalidDestinationPathError -f $DestinationPath); } $DestinationPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath); Write-Verbose -Message ($localized.ResolvedDestinationPath -f $DestinationPath); [ref] $null = New-Directory -Path $DestinationPath; foreach ($pathItem in $Path) { foreach ($resolvedPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathItem)) { Write-Verbose -Message ($localized.ResolvedSourcePath -f $resolvedPath); $LiteralPath += $resolvedPath; } } ## If all tests passed, load the required .NET assemblies Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } # end begin process { foreach ($pathEntry in $LiteralPath) { try { $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($pathEntry); $expandZipArchiveItemParams = @{ InputObject = [ref] $zipArchive.Entries; DestinationPath = $DestinationPath; Repository = $Repository; Force = $Force; } if ($OverrideRepository) { $expandZipArchiveItemParams['OverrideRepository'] = $OverrideRepository; } Expand-GitHubZipArchiveItem @expandZipArchiveItemParams; } # end try catch { Write-Error $_.Exception; } finally { ## Close the file handle Close-GitHubZipArchive; } } # end foreach } # end process } #end function function Expand-GitHubZipArchiveItem { <# .SYNOPSIS Extracts file(s) from a GitHub Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess, ConfirmImpact = 'Medium')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')] [OutputType([System.IO.FileInfo])] param ( # Reference to Zip archive item. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'InputObject')] [System.IO.Compression.ZipArchiveEntry[]] [ref] $InputObject, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [System.String] $DestinationPath, # GitHub repository name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, ## Override repository name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepository, # Overwrite existing physical filesystem files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { Write-Debug 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } process { try { ## Regex for locating the <RepositoryName>-<Branch>\ root directory $searchString = '^{0}-\S+?\\' -f $Repository; $replacementString = '{0}\' -f $Repository; if ($OverrideRepository) { $replacementString = '{0}\' -f $OverrideRepository; } [System.Int32] $fileCount = 0; $moduleDestinationPath = Join-Path -Path $DestinationPath -ChildPath $Repository; $activity = $localized.DecompressingArchive -f $moduleDestinationPath; Write-Progress -Activity $activity -PercentComplete 0; foreach ($zipArchiveEntry in $InputObject) { $fileCount++; if (($fileCount % 5) -eq 0) { [System.Int16] $percentComplete = ($fileCount / $InputObject.Count) * 100 $status = $localized.CopyingResourceStatus -f $fileCount, $InputObject.Count, $percentComplete; Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete; } if ($zipArchiveEntry.FullName.Contains('/')) { ## We need to create the directory path as the ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\ $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\'); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = New-Directory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } # end if else { ## Just a file in the root so just use the $DestinationPath $fullDestinationFilePath = Join-Path -Path $DestinationPath -ChildPath $zipArchiveEntry.Name; } # end else if ([System.String]::IsNullOrEmpty($zipArchiveEntry.Name)) { ## This is a folder and we need to create the directory path as the ## ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } ## Rename the GitHub \<RepositoryName>-<Branch>\ root directory to \<RepositoryName>\ $relativePath = ($relativeDirectoryPath.ToString() -replace $searchString, $replacementString).TrimEnd('\'); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = New-Directory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } elseif (-not $Force -and (Test-Path -Path $fullDestinationFilePath -PathType Leaf)) { ## Are we overwriting existing files (-Force)? Write-Warning ($localized.TargetFileExistsWarning -f $fullDestinationFilePath); } else { ## Just overwrite any existing file if ($Force -or $PSCmdlet.ShouldProcess($fullDestinationFilePath, 'Expand')) { Write-Debug ($localized.ExtractingZipArchiveEntry -f $fullDestinationFilePath); [System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipArchiveEntry, $fullDestinationFilePath, $true); ## Return a FileInfo object to the pipline Write-Output (Get-Item -Path $fullDestinationFilePath); } } # end if } # end foreach zipArchiveEntry Write-Progress -Activity $activity -Completed; } # end try catch { Write-Error $_.Exception; } } # end process } #end function function Expand-LabImage { <# .SYNOPSIS Writes a .wim image to a mounted VHD/(X) file. #> [CmdletBinding(DefaultParameterSetName = 'Index')] param ( ## File path to WIM file or ISO file containing the WIM image [Parameter(Mandatory, ValueFromPipeline)] [System.String] $MediaPath, ## WIM image index to apply [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Index')] [System.Int32] $WimImageIndex, ## WIM image name to apply [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [ValidateNotNullOrEmpty()] [System.String] $WimImageName, ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk ## Disk image partition scheme [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('MBR','GPT')] [System.String] $PartitionStyle, ## Optional Windows features to add to the image after expansion (ISO only) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String[]] $WindowsOptionalFeature, ## Optional Windows features source path [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $SourcePath = '\sources\sxs', ## Relative source WIM file path (only used for ISOs) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $WimPath = '\sources\install.wim', ## Optional Windows packages to add to the image after expansion (primarily used for Nano Server) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String[]] $Package, ## Relative packages (.cab) file path (primarily used for Nano Server) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $PackagePath = '\packages', ## Package localization directory/extension (primarily used for Nano Server) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $PackageLocale = 'en-US' ) process { ## Assume the media path is a literal path to a WIM file $windowsImagePath = $MediaPath; $mediaFileInfo = Get-Item -Path $MediaPath; try { if ($mediaFileInfo.Extension -eq '.ISO') { ## Disable BitLocker fixed drive write protection (if enabled) Disable-BitLockerFDV; ## Mount ISO Write-Verbose -Message ($localized.MountingDiskImage -f $MediaPath); $mountDiskImageParams = @{ ImagePath = $MediaPath; StorageType = 'ISO'; Access = 'ReadOnly'; PassThru = $true; Verbose = $false; ErrorAction = 'Stop'; } $iso = Storage\Mount-DiskImage @mountDiskImageParams; $iso = Storage\Get-DiskImage -ImagePath $iso.ImagePath; $isoDriveLetter = Storage\Get-Volume -DiskImage $iso | Select-Object -ExpandProperty DriveLetter; ## Update the media path to point to the mounted ISO $windowsImagePath = '{0}:{1}' -f $isoDriveLetter, $WimPath; } if ($PSCmdlet.ParameterSetName -eq 'Name') { ## Locate the image index $wimImageIndex = Get-WindowsImageByName -ImagePath $windowsImagePath -ImageName $WimImageName; } if ($PartitionStyle -eq 'MBR') { $partitionType = 'IFS'; } elseif ($PartitionStyle -eq 'GPT') { $partitionType = 'Basic'; } $vhdDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType $partitionType; $logName = '{0}.log' -f [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path); $logPath = Join-Path -Path $env:TEMP -ChildPath $logName; Write-Verbose -Message ($localized.ApplyingWindowsImage -f $wimImageIndex, $Vhd.Path); $expandWindowsImageParams = @{ ImagePath = $windowsImagePath; ApplyPath = '{0}:\' -f $vhdDriveLetter; LogPath = $logPath; Index = $wimImageIndex; Verbose = $false; ErrorAction = 'Stop'; } [ref] $null = Expand-WindowsImage @expandWindowsImageParams; [ref] $null = Get-PSDrive; ## Add additional packages (.cab) files if ($Package) { ## Default to relative package folder path $addLabImageWindowsPackageParams = @{ PackagePath = '{0}:{1}' -f $isoDriveLetter, $PackagePath; DestinationPath = '{0}:\' -f $vhdDriveLetter; LogPath = $logPath; Package = $Package; PackageLocale = $PackageLocale; ErrorAction = 'Stop'; } if (-not $PackagePath.StartsWith('\')) { ## Use the specified/literal path $addLabImageWindowsPackageParams['PackagePath'] = $PackagePath; } [ref] $null = Add-LabImageWindowsPackage @addLabImageWindowsPackageParams; } #end if Package ## Add additional features if required if ($WindowsOptionalFeature) { ## Default to ISO relative source folder path $addLabImageWindowsOptionalFeatureParams = @{ ImagePath = '{0}:{1}' -f $isoDriveLetter, $SourcePath; DestinationPath = '{0}:\' -f $vhdDriveLetter; LogPath = $logPath; WindowsOptionalFeature = $WindowsOptionalFeature; ErrorAction = 'Stop'; } if ($mediaFileInfo.Extension -eq '.WIM') { ## The Windows optional feature source path for .WIM files is a literal path $addLabImageWindowsOptionalFeatureParams['ImagePath'] = $SourcePath; } [ref] $null = Add-LabImageWindowsOptionalFeature @addLabImageWindowsOptionalFeatureParams; } #end if WindowsOptionalFeature } catch { Write-Error -Message $_; } #end catch finally { if ($mediaFileInfo.Extension -eq '.ISO') { ## Always dismount ISO (#166) Write-Verbose -Message ($localized.DismountingDiskImage -f $MediaPath); $null = Storage\Dismount-DiskImage -ImagePath $MediaPath; } ## Enable BitLocker (if required) Assert-BitLockerFDV } #end finally } #end process } #end function Expand-LabImage function Expand-LabIso { <# .SYNOPSIS Expands an ISO disk image resource #> param ( ## Source ISO file path [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Path, ## Destination folder path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $DestinationPath ) process { ## Disable BitLocker fixed drive write protection (if enabled) Disable-BitLockerFDV; Write-Verbose -Message ($localized.MountingDiskImage -f $Path); $iso = Storage\Mount-DiskImage -ImagePath $Path -StorageType ISO -Access ReadOnly -PassThru -Verbose:$false; ## Refresh drives [ref] $null = Get-PSDrive; $isoDriveLetter = $iso | Storage\Get-Volume | Select-Object -ExpandProperty DriveLetter; $sourcePath = '{0}:\' -f $isoDriveLetter; Write-Verbose -Message ($localized.ExpandingIsoResource -f $DestinationPath); CopyDirectory -SourcePath $sourcePath -DestinationPath $DestinationPath -Force -Verbose:$false; Write-Verbose -Message ($localized.DismountingDiskImage -f $Path); $null = Storage\Dismount-DiskImage -ImagePath $Path; ## Enable BitLocker (if required) Assert-BitLockerFDV; } #end process } #end function function Expand-LabModuleCache { <# .SYNOPSIS Extracts a cached PowerShell module to the specified destination module path. #> [CmdletBinding()] [OutputType([System.IO.DirectoryInfo])] param ( ## PowerShell module hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $Module, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $DestinationPath, ## Removes existing module directory if present [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Clean, ## Catch all to be able to pass parameter via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) begin { [ref] $null = $PSBoundParameters.Remove('RemainingArguments'); } process { foreach ($moduleInfo in $Module) { $moduleFileInfo = Get-LabModuleCache @moduleInfo; $moduleSourcePath = $moduleFileInfo.FullName; $moduleDestinationPath = Join-Path -Path $DestinationPath -ChildPath $moduleInfo.Name; if ($Clean -and (Test-Path -Path $moduleDestinationPath -PathType Container)) { Write-Verbose -Message ($localized.CleaningModuleDirectory -f $moduleDestinationPath); Remove-Item -Path $moduleDestinationPath -Recurse -Force -Confirm:$false; } if ((-not $moduleInfo.ContainsKey('Provider')) -or ($moduleInfo.Provider -in 'PSGallery', 'AzDo')) { Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath); $expandZipArchiveParams = @{ Path = $moduleSourcePath; DestinationPath = $moduleDestinationPath; ExcludeNuSpecFiles = $true; Force = $true; Verbose = $false; WarningAction = 'SilentlyContinue'; Confirm = $false; } [ref] $null = Expand-ZipArchive @expandZipArchiveParams; } #end if PSGallery or Azdo elseif (($moduleInfo.ContainsKey('Provider')) -and ($moduleInfo.Provider -eq 'GitHub')) { Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath); $expandGitHubZipArchiveParams = @{ Path = $moduleSourcePath; ## GitHub modules include the module directory. Therefore, we need the parent root directory DestinationPath = Split-Path -Path $moduleDestinationPath -Parent;; Repository = $moduleInfo.Name; Force = $true; Verbose = $false; WarningAction = 'SilentlyContinue'; Confirm = $false; } if ($moduleInfo.ContainsKey('OverrideRepository')) { $expandGitHubZipArchiveParams['OverrideRepository'] = $moduleInfo.OverrideRepository; } [ref] $null = Expand-GitHubZipArchive @expandGitHubZipArchiveParams; } #end if GitHub elseif (($moduleInfo.ContainsKey('Provider')) -and ($moduleInfo.Provider -eq 'FileSystem')) { if ($null -ne $moduleFileInfo) { if ($moduleFileInfo -is [System.IO.FileInfo]) { Write-Verbose -Message ($localized.ExpandingModule -f $moduleDestinationPath); $expandZipArchiveParams = @{ Path = $moduleSourcePath; DestinationPath = $moduleDestinationPath; ExcludeNuSpecFiles = $true; Force = $true; Verbose = $false; WarningAction = 'SilentlyContinue'; Confirm = $false; } [ref] $null = Expand-ZipArchive @expandZipArchiveParams; } elseif ($moduleFileInfo -is [System.IO.DirectoryInfo]) { Write-Verbose -Message ($localized.CopyingModuleDirectory -f $moduleFileInfo.Name, $moduleDestinationPath); ## If the target doesn't exist create it. We may be copying a versioned ## module, i.e. \xJea\0.2.16.6 to \xJea.. if (-not (Test-Path -Path $moduleDestinationPath -PathType Container)) { New-Item -Path $moduleDestinationPath -ItemType Directory -Force; } $copyItemParams = @{ Path = "$moduleSourcePath\*"; Destination = $moduleDestinationPath; Recurse = $true; Force = $true; Verbose = $false; Confirm = $false; } Copy-Item @copyItemParams; } } } #end if FileSystem ## Only output if we found a module during this pass if ($null -ne $moduleFileInfo) { Write-Output -InputObject (Get-Item -Path $moduleDestinationPath); } } #end foreach module } #end process } #end function function Expand-LabResource { <# .SYNOPSIS Copies files, e.g. EXEs, ISOs and ZIP file resources into a lab VM's mounted VHDX differencing disk image. .NOTES VHDX should already be mounted and passed in via the $DestinationPath parameter Can expand ISO and ZIP files if the 'Expand' property is set to $true on the resource's properties. #> param ( ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Lab VM name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## Destination mounted VHDX path to expand resources into [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, ## Source resource path [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourcePath ) begin { if (-not $ResourcePath) { $hostDefaults = Get-ConfigurationData -Configuration Host; $ResourcePath = $hostDefaults.ResourcePath; } } process { ## Create the root destination (\Resources) container if (-not (Test-Path -Path $DestinationPath -PathType Container)) { [ref] $null = New-Item -Path $DestinationPath -ItemType Directory -Force -Confirm:$false; } $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -ErrorAction Stop; foreach ($resourceId in $node.Resource) { Write-Verbose -Message ($localized.AddingResource -f $resourceId); $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $resourceId; ## Default to resource.Id unless there is a filename property defined! $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Id; if ($resource.Filename) { $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Filename; if ($resource.IsLocal) { $resourceSourcePath = Resolve-Path -Path $resource.Filename; } } if (-not (Test-Path -Path $resourceSourcePath) -and (-not $resource.IsLocal)) { $invokeLabResourceDownloadParams = @{ ConfigurationData = $ConfigurationData; ResourceId = $resourceId; } [ref] $null = Invoke-LabResourceDownload @invokeLabResourceDownloadParams; } if (-not (Test-Path -Path $resourceSourcePath)) { throw ($localized.CannotResolveResourceIdError -f $resourceId); } $resourceItem = Get-Item -Path $resourceSourcePath; $resourceDestinationPath = $DestinationPath; if ($resource.DestinationPath -and (-not [System.String]::IsNullOrEmpty($resource.DestinationPath))) { $destinationDrive = Split-Path -Path $DestinationPath -Qualifier; $resourceDestinationPath = Join-Path -Path $destinationDrive -ChildPath $resource.DestinationPath; ## We can't create a drive-rooted folder! if (($resource.DestinationPath -ne '\') -and (-not (Test-Path -Path $resourceDestinationPath))) { [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force -Confirm:$false; } } elseif ($resource.IsLocal -and ($resource.IsLocal -eq $true)) { $relativeLocalPath = ($resource.Filename).TrimStart('.'); $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $relativeLocalPath; } if (($resource.Expand) -and ($resource.Expand -eq $true)) { if ([System.String]::IsNullOrEmpty($resource.DestinationPath)) { ## No explicit destination path, so expand into the <DestinationPath>\<ResourceId> folder $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $resource.Id; } if (-not (Test-Path -Path $resourceDestinationPath)) { [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force -Confirm:$false; } switch ([System.IO.Path]::GetExtension($resourceSourcePath)) { '.iso' { Expand-LabIso -Path $resourceItem.FullName -DestinationPath $resourceDestinationPath; } '.zip' { Write-Verbose -Message ($localized.ExpandingZipResource -f $resourceItem.FullName); $expandZipArchiveParams = @{ Path = $resourceItem.FullName; DestinationPath = $resourceDestinationPath; Verbose = $false; } [ref] $null = Expand-ZipArchive @expandZipArchiveParams; } Default { throw ($localized.ExpandNotSupportedError -f $resourceItem.Extension); } } #end switch } else { Write-Verbose -Message ($localized.CopyingFileResource -f $resourceDestinationPath); $copyItemParams = @{ Path = "$($resourceItem.FullName)"; Destination = "$resourceDestinationPath"; Force = $true; Recurse = $true; Verbose = $false; Confirm = $false; } Copy-Item @copyItemParams; } } #end foreach ResourceId } #end process } #end function function Expand-ZipArchive { <# .SYNOPSIS Extracts a Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([System.IO.FileInfo])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( # Source path to the Zip Archive. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [Alias('PSPath','FullName')] [System.String[]] $Path, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [System.String] $DestinationPath, # Excludes NuGet .nuspec specific files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $ExcludeNuSpecFiles, # Overwrite existing files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { ## Validate destination path if (-not (Test-Path -Path $DestinationPath -IsValid)) { throw ($localized.InvalidDestinationPathError -f $DestinationPath); } $DestinationPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath); Write-Verbose -Message ($localized.ResolvedDestinationPath -f $DestinationPath); [ref] $null = New-Directory -Path $DestinationPath; foreach ($pathItem in $Path) { foreach ($resolvedPath in $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($pathItem)) { Write-Verbose -Message ($localized.ResolvedSourcePath -f $resolvedPath); $LiteralPath += $resolvedPath; } } ## If all tests passed, load the required .NET assemblies Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } # end begin process { foreach ($pathEntry in $LiteralPath) { try { $zipArchive = [System.IO.Compression.ZipFile]::OpenRead($pathEntry); $expandZipArchiveItemParams = @{ InputObject = [ref] $zipArchive.Entries; DestinationPath = $DestinationPath; ExcludeNuSpecFiles = $ExcludeNuSpecFiles; Force = $Force; } Expand-ZipArchiveItem @expandZipArchiveItemParams; } # end try catch { Write-Error -Message $_.Exception; } finally { ## Close the file handle Close-ZipArchive; } } # end foreach } # end process } #end function function Expand-ZipArchiveItem { <# .SYNOPSIS Extracts file(s) from a Zip archive. .NOTES This is an internal function and should not be called directly. .LINK This function is derived from the VirtualEngine.Compression (https://github.com/VirtualEngine/Compression) module. .OUTPUTS A System.IO.FileInfo object for each extracted file. #> [CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess, ConfirmImpact = 'Medium')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')] [OutputType([System.IO.FileInfo])] param ( # Reference to Zip archive item. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'InputObject')] [ValidateNotNullOrEmpty()] [System.IO.Compression.ZipArchiveEntry[]] [ref] $InputObject, # Destination file path to extract the Zip Archive item to. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, # Excludes NuGet .nuspec specific files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $ExcludeNuSpecFiles, # Overwrite existing physical filesystem files [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) begin { Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.'; Add-Type -AssemblyName 'System.IO.Compression'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; } process { try { [System.Int32] $fileCount = 0; $activity = $localized.DecompressingArchive -f $DestinationPath; Write-Progress -Activity $activity -PercentComplete 0; foreach ($zipArchiveEntry in $InputObject) { $fileCount++; if (($fileCount % 5) -eq 0) { [System.Int16] $percentComplete = ($fileCount / $InputObject.Count) * 100 $status = $localized.CopyingResourceStatus -f $fileCount, $InputObject.Count, $percentComplete; Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete; } ## Exclude the .nuspec specific files if ($ExcludeNuSpecFiles -and ($zipArchiveEntry.FullName -match '(_rels\/)|(\[Content_Types\]\.xml)|(\w+\.nuspec)')) { Write-Verbose -Message ($localized.IgnoringNuspecZipArchiveEntry -f $zipArchiveEntry.FullName); continue; } if ($zipArchiveEntry.FullName.Contains('/')) { ## We need to create the directory path as the ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object -TypeName System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } $relativePath = $relativeDirectoryPath.ToString(); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = New-Directory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } # end if else { ## Just a file in the root so just use the $DestinationPath $fullDestinationFilePath = Join-Path -Path $DestinationPath -ChildPath $zipArchiveEntry.Name; } # end else if ([System.String]::IsNullOrEmpty($zipArchiveEntry.Name)) { ## This is a folder and we need to create the directory path as the ## ExtractToFile extension method won't do this and will throw an exception $pathSplit = $zipArchiveEntry.FullName.Split('/'); $relativeDirectoryPath = New-Object -TypeName System.Text.StringBuilder; ## Generate the relative directory name for ($pathSplitPart = 0; $pathSplitPart -lt ($pathSplit.Count -1); $pathSplitPart++) { [ref] $null = $relativeDirectoryPath.AppendFormat('{0}\', $pathSplit[$pathSplitPart]); } $relativePath = $relativeDirectoryPath.ToString(); ## Create the destination directory path, joining the relative directory name $directoryPath = Join-Path -Path $DestinationPath -ChildPath $relativePath; [ref] $null = New-Directory -Path $directoryPath; $fullDestinationFilePath = Join-Path -Path $directoryPath -ChildPath $zipArchiveEntry.Name; } elseif (-not $Force -and (Test-Path -Path $fullDestinationFilePath -PathType Leaf)) { ## Are we overwriting existing files (-Force)? Write-Warning -Message ($localized.TargetFileExistsWarning -f $fullDestinationFilePath); } else { ## Just overwrite any existing file if ($Force -or $PSCmdlet.ShouldProcess($fullDestinationFilePath, 'Expand')) { Write-Verbose -Message ($localized.ExtractingZipArchiveEntry -f $fullDestinationFilePath); [System.IO.Compression.ZipFileExtensions]::ExtractToFile($zipArchiveEntry, $fullDestinationFilePath, $true); ## Return a FileInfo object to the pipline Write-Output -InputObject (Get-Item -Path $fullDestinationFilePath); } } # end if } # end foreach zipArchiveEntry Write-Progress -Activity $activity -Completed; } # end try catch { Write-Error -Message $_.Exception; } } # end process } #end function function Get-ConfigurationData { <# .SYNOPSIS Gets lab configuration data. #> [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter(Mandatory)] [ValidateSet('Host','VM','Media','CustomMedia')] [System.String] $Configuration ) process { $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration -IncludeDefaultPath; if (Test-Path -Path $configurationPath) { $configurationData = Get-Content -Path $configurationPath -Raw | ConvertFrom-Json; switch ($Configuration) { 'VM' { ## This property may not be present in the original VM default file TODO: Could be deprecated in the future if ($configurationData.PSObject.Properties.Name -notcontains 'CustomBootstrapOrder') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'CustomBootstrapOrder' -Value 'MediaFirst'; } ## This property may not be present in the original VM default file TODO: Could be deprecated in the future if ($configurationData.PSObject.Properties.Name -notcontains 'SecureBoot') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'SecureBoot' -Value $true; } ## This property may not be present in the original VM default file TODO: Could be deprecated in the future if ($configurationData.PSObject.Properties.Name -notcontains 'GuestIntegrationServices') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'GuestIntegrationServices' -Value $false; } ## This property may not be present in the original VM default file TODO: Could be deprecated in the future if ($configurationData.PSObject.Properties.Name -notcontains 'AutomaticCheckpoints') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'AutomaticCheckpoints' -Value $false; } ## This property may not be present in the original VM default file TODO: Could be deprecated in the future if ($configurationData.PSObject.Properties.Name -notcontains 'MaxEnvelopeSizeKb') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'MaxEnvelopeSizeKb' -Value 1024; } ## This property may not be present in the original VM default file. Defaults to $false if ($configurationData.PSObject.Properties.Name -notcontains 'UseNetBIOSName') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'UseNetBIOSName' -Value $false; } } 'CustomMedia' { foreach ($mediaItem in $configurationData) { ## Add missing OperatingSystem property if ($mediaItem.PSObject.Properties.Name -notcontains 'OperatingSystem') { [ref] $null = Add-Member -InputObject $mediaItem -MemberType NoteProperty -Name 'OperatingSystem' -Value 'Windows'; } } #end foreach media item } 'Host' { ## This property may not be present in the original machine configuration file if ($configurationData.PSObject.Properties.Name -notcontains 'DisableLocalFileCaching') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableLocalFileCaching' -Value $false; } ## This property may not be present in the original machine configuration file if ($configurationData.PSObject.Properties.Name -notcontains 'EnableCallStackLogging') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'EnableCallStackLogging' -Value $false; } ## This property may not be present in the original machine configuration file if ($configurationData.PSObject.Properties.Name -notcontains 'ModuleCachePath') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'ModuleCachePath' -Value '%ALLUSERSPROFILE%\Lability\Modules'; } if ($configurationData.PSObject.Properties.Name -notcontains 'DismPath') { $dismDllName = 'Microsoft.Dism.PowerShell.dll'; $dismDllPath = Join-Path -Path "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules\Dism" -ChildPath $dismDllName -Resolve; [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DismPath' -Value $dismDllPath; } ## This property may not be present in the original machine configuration file if ($configurationData.PSObject.Properties.Name -notcontains 'RepositoryUri') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'RepositoryUri' -Value $labDefaults.RepositoryUri; } ## This property may not be present in the original machine configuration file. Defaults to $true for existing ## deployments, but is disabled ($false) in the default HostDefaults.json for new installs. if ($configurationData.PSObject.Properties.Name -notcontains 'DisableSwitchEnvironmentName') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableSwitchEnvironmentName' -Value $true; } ## This property may not be present in the original machine configuration file. Defaults to $true for existing ## deployments, but is disabled ($false) in the default HostDefaults.json for new installs. if ($configurationData.PSObject.Properties.Name -notcontains 'DisableVhdEnvironmentName') { [ref] $null = Add-Member -InputObject $configurationData -MemberType NoteProperty -Name 'DisableVhdEnvironmentName' -Value $true; } ## Remove deprecated UpdatePath, if present (Issue #77) $configurationData.PSObject.Properties.Remove('UpdatePath'); } } #end switch # Expand any environment variables in configuration data $configurationData.PSObject.Members | Where-Object { ($_.MemberType -eq 'NoteProperty') -and ($_.IsSettable) -and ($_.TypeNameOfValue -eq 'System.String') } | ForEach-Object { $_.Value = [System.Environment]::ExpandEnvironmentVariables($_.Value); } return $configurationData; } } #end process } #end function function Get-DiskImageDriveLetter { <# .SYNOPSIS Return a disk image's associated/mounted drive letter. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $DiskImage, [Parameter(Mandatory)] [ValidateSet('Basic','System','IFS')] [System.String] $PartitionType ) process { # Microsoft.Vhd.PowerShell.VirtualHardDisk $driveLetter = Storage\Get-Partition -DiskNumber $DiskImage.DiskNumber | Where-Object Type -eq $PartitionType | Where-Object DriveLetter | Select-Object -Last 1 -ExpandProperty DriveLetter; if (-not $driveLetter) { throw ($localized.CannotLocateDiskImageLetter -f $DiskImage.Path); } return $driveLetter; } } #end function function Get-DscResourceModule { <# .SYNOPSIS Enumerates a directory path and returns list of all valid DSC resources. .DESCRIPTION The Get-DscResourceModule returns all the PowerShell DSC resource modules in the specified path. This is used to determine which directories to copy to a VM's VHD(X) file. Only the latest version of module that is installed is returned, removing any versioned folders that are introduced in WMF 5.0, but cannot be interpreted by down-level WMF versions. .NOTES THIS METHOD IS DEPRECATED IN FAVOUR OF THE NEW MODULE CACHE FUNCTIONALITY More context can be found here https://github.com/VirtualEngine/Lab/issues/25 #> [CmdletBinding()] [OutputType([System.Boolean])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock','')] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String[]] $Path ) process { foreach ($basePath in $Path) { Get-ChildItem -Path $basePath -Directory | ForEach-Object { $moduleInfo = $PSItem; ## Check to see if we have a MOF or class resource in the module if (Test-DscResourceModule -Path $moduleInfo.FullName -ModuleName $moduleInfo.Name) { Write-Debug -Message ('Discovered DSC resource ''{0}''.' -f $moduleInfo.FullName); $testModuleManifestPath = '{0}\{1}.psd1' -f $moduleInfo.FullName, $moduleInfo.Name; ## Convert the .psd1 file into a hashtable (Test-ModuleManifest can actually load the module) if (Test-Path -Path $testModuleManifestPath -PathType Leaf) { $module = ConvertTo-ConfigurationData -ConfigurationData $testModuleManifestPath; Write-Output -InputObject ([PSCustomObject] @{ ModuleName = $moduleInfo.Name; ModuleVersion = $module.ModuleVersion -as [System.Version]; Path = $moduleInfo.FullName; }); } } else { ## Enumerate each module\<number>.<number> subdirectory Get-ChildItem -Path $moduleInfo.FullName -Directory | Where-Object Name -match '^\d+\.\d+' | ForEach-Object { Write-Debug -Message ('Checking module versioned directory ''{0}''.' -f $PSItem.FullName); ## Test to see if it's a DSC resource module if (Test-DscResourceModule -Path $PSItem.FullName -ModuleName $moduleInfo.Name) { try { Write-Debug -Message ('Discovered DSC resource ''{0}''.' -f $PSItem.FullName); $testModuleManifestPath = '{0}\{1}.psd1' -f $PSItem.FullName, $moduleInfo.Name; ## Convert the .psd1 file into a hashtable (Test-ModuleManifest can actually load the module) $module = ConvertTo-ConfigurationData -ConfigurationData $testModuleManifestPath; Write-Output -InputObject ([PSCustomObject] @{ ModuleName = $moduleInfo.Name; ModuleVersion = [System.Version] $module.ModuleVersion; Path = $PSItem.FullName; }); } catch { } } } | #end foreach module\<number>.<number> sub directory Sort-Object -Property ModuleVersion -Descending | Select-Object -First 1; } } #end foreach module directory } #end foreach path } #end process } #end function function Get-FormattedMessage { <# .SYNOPSIS Generates a formatted output message with timestamp. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Message ) process { if (($labDefaults.CallStackLogging) -and ($labDefaults.CallStackLogging -eq $true)) { $parentCallStack = (Get-PSCallStack)[1]; # store the parent Call Stack $functionName = $parentCallStack.FunctionName; $lineNumber = $parentCallStack.ScriptLineNumber; $scriptName = ($parentCallStack.Location -split ':')[0]; $formattedMessage = '[{0}] [Script:{1}] [Function:{2}] [Line:{3}] {4}' -f (Get-Date).ToLongTimeString(), $scriptName, $functionName, $lineNumber, $Message; } else { $formattedMessage = '[{0}] {1}' -f (Get-Date).ToLongTimeString(), $Message; } return $formattedMessage; } #end process } #end function function Get-LabDscModule { <# .SYNOPSIS Locates the directory path of the ResourceName within the specified DSC ModuleName. #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory)] [System.String] $ModuleName, [Parameter()] [System.String] $ResourceName, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $MinimumVersion ) process { $module = Get-Module -Name $ModuleName -ListAvailable; $dscModulePath = Split-Path -Path $module.Path -Parent; if ($ResourceName) { $ModuleName = '{0}\{1}' -f $ModuleName, $ResourceName; $dscModulePath = Join-Path -Path $dscModulePath -ChildPath "DSCResources\$ResourceName"; } if (-not (Test-Path -Path $dscModulePath)) { Write-Error -Message ($localized.DscResourceNotFoundError -f $ModuleName); return $null; } if ($MinimumVersion) { if ($Module.Version -lt [System.Version]$MinimumVersion) { Write-Error -Message ($localized.ResourceVersionMismatchError -f $ModuleName, $module.Version.ToString(), $MinimumVersion); return $null; } } return $dscModulePath; } #end process } #end function function Get-LabDscResource { <# .SYNOPSIS Gets the ResourceName DSC resource configuration. .DESCRIPTION The Get-LabDscResource cmdlet invokes the target $ResourceName\Get-TargetResource function using the supplied $Parameters hashtable. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Name of the DSC resource to get [Parameter(Mandatory, ValueFromPipeline)] [System.String] $ResourceName, ## The DSC resource's Get-TargetResource parameter hashtable [Parameter(Mandatory)] [System.Collections.Hashtable] $Parameters ) process { $getTargetResourceCommand = 'Get-{0}TargetResource' -f $ResourceName; Write-Debug ($localized.InvokingCommand -f $getTargetResourceCommand); # Code to factor in the parameters which can be passed to the Get-<Prefix>TargetResource function. $CommandInfo = Get-Command -Name $getTargetResourceCommand; $RemoveParameters = $Parameters.Keys | Where-Object -FilterScript { $($CommandInfo.Parameters.Keys) -notcontains $PSItem }; $RemoveParameters | ForEach-Object -Process { [ref] $null = $Parameters.Remove($PSItem) }; try { $getDscResourceResult = & $getTargetResourceCommand @Parameters; } catch { Write-Warning -Message ($localized.DscResourceFailedError -f $getTargetResourceCommand, $_); } return $getDscResourceResult; } #end process } #end function function Get-LabHostDscConfigurationPath { <# .SYNOPSIS Shortcut function to resolve the host's default ConfigurationPath property #> [CmdletBinding()] [OutputType([System.String])] param ( ) process { $labHostDefaults = Get-ConfigurationData -Configuration Host; return $labHostDefaults.ConfigurationPath; } #end process } #end function Get-LabHostDscConfigurationPath function Get-LabHostSetupConfiguration { <# .SYNOPSIS Returns an array of hashtables defining the desired host configuration. .DESCRIPTION The Get-LabHostSetupConfiguration function returns an array of hashtables used to determine whether the host is in the desired configuration. .NOTES The configuration is passed to avoid repeated calls to Get-LabHostDefault and polluting verbose output. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ) process { [System.Boolean] $isDesktop = (Get-CimInstance -ClassName Win32_OperatingSystem -Verbose:$false).ProductType -eq 1; ## Due to the differences in client/server deployment for Hyper-V, determine the correct method before creating the host configuration array. $labHostSetupConfiguration = @(); if ($isDesktop) { Write-Debug -Message 'Implementing desktop configuration.'; $labHostSetupConfiguration += @{ UseDefault = $false; Description = 'Hyper-V role'; ModuleName = 'xPSDesiredStateConfiguration'; ResourceName = 'DSC_xWindowsOptionalFeature'; Prefix = 'xWindowsOptionalFeature'; Parameters = @{ Ensure = 'Present'; Name = 'Microsoft-Hyper-V-All'; } }; } else { Write-Debug -Message 'Implementing server configuration.'; $labHostSetupConfiguration += @{ UseDefault = $false; Description = 'Hyper-V Role'; ModuleName = 'xPSDesiredStateConfiguration'; ResourceName = 'DSC_xWindowsFeature'; Prefix = 'xWindowsFeature'; Parameters = @{ Ensure = 'Present'; Name = 'Hyper-V'; IncludeAllSubFeature = $true; } }; $labHostSetupConfiguration += @{ UseDefault = $false; Description = 'Hyper-V Tools'; ModuleName = 'xPSDesiredStateConfiguration'; ResourceName = 'DSC_xWindowsFeature'; Prefix = 'xWindowsFeature'; Parameters = @{ Ensure = 'Present'; Name = 'RSAT-Hyper-V-Tools'; IncludeAllSubFeature = $true; } }; } #end Server configuration $labHostSetupConfiguration += @{ ## Check for a reboot before continuing UseDefault = $false; Description = 'Pending reboot'; ModuleName = 'xPendingReboot'; ResourceName = 'MSFT_xPendingReboot'; Prefix = 'PendingReboot'; Parameters = @{ Name = 'TestingForHypervReboot'; SkipCcmClientSDK = $true; } }; return $labHostSetupConfiguration; } #end process } #end function function Get-LabMediaId { <# .SYNOPSIS Helper method for dynamic media Id parameters, returning all valid media Ids and Aliases. #> [CmdletBinding()] param( ) process { $availableMedia = Get-LabMedia $mediaIds = @{ } foreach ($media in $availableMedia) { $mediaIds[$media.Id] = $media.Id if ($null -ne $media.Alias) { if ($mediaIds.ContainsKey($media.Alias)) { Write-Warning -Message ($localizedData.DuplicateMediaAliasIgnoredWarning -f $media.Id, $media.Alias) } else { $mediaIds[$media.Alias] = $media.Alias } } } return $mediaIds.Keys } } function Get-LabModule { <# .SYNOPSIS Tests whether an exising PowerShell module meets the minimum or required version #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name ) process { Write-Verbose -Message ($localized.LocatingModule -f $Name); ## Only return modules in the %ProgramFiles%\WindowsPowerShell\Modules location, ignore other $env:PSModulePaths $programFiles = [System.Environment]::GetFolderPath('ProgramFiles'); $modulesPath = ('{0}\WindowsPowerShell\Modules' -f $programFiles).Replace('\','\\'); $module = Get-Module -Name $Name -ListAvailable -Verbose:$false | Where-Object Path -match $modulesPath; if (-not $module) { Write-Verbose -Message ($localized.ModuleNotFound -f $Name); } else { Write-Verbose -Message ($localized.ModuleFoundInPath -f $module.Path); } return $module; } #end process } #end function function Get-LabModuleCache { <# .SYNOPSIS Returns the requested cached PowerShell module zip [System.IO.FileInfo] object. .NOTES File system modules are not stored in the module cache. #> [CmdletBinding(DefaultParameterSetName = 'Name')] [OutputType([System.IO.FileInfo])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Name, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## GitHub repository owner [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Owner, ## GitHub repository branch [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Branch, ## Source Filesystem module path [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Provider used to download the module [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateSet('PSGallery','GitHub', 'AzDo', 'FileSystem')] [System.String] $Provider, ## Lability PowerShell module info hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')] [ValidateNotNullOrEmpty()] [System.Collections.Hashtable] $Module, ## Catch all to be able to pass parameter via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) begin { if ([System.String]::IsNullOrEmpty($Provider)) { $Provider = 'PSGallery' } if ($PSCmdlet.ParameterSetName -eq 'Module') { $requiredParameters = 'Name'; foreach ($parameterName in $requiredParameters) { if ([System.String]::IsNullOrEmpty($Module[$parameterName])) { throw ($localized.RequiredModuleParameterError -f $parameterName); } } #end foreach required parameter $validParameters = 'Name','Provider','MinimumVersion','RequiredVersion','Owner','Branch','Path'; foreach ($parameterName in $Module.Keys) { if ($parameterName -notin $validParameters) { throw ($localized.InvalidtModuleParameterError -f $parameterName); } else { Set-Variable -Name $parameterName -Value $Module[$parameterName]; } } #end foreach parameter } #end if Module if ($Provider -eq 'GitHub') { if ([System.String]::IsNullOrEmpty($Owner)) { throw ($localized.RequiredModuleParameterError -f 'Owner'); } ## Default to master branch if none specified if ([System.String]::IsNullOrEmpty($Branch)) { $Branch = 'master'; } $Branch = $Branch.Replace('/','_') # Fix branch names with slashes (#361) } #end if GitHub elseif ($Provider -eq 'FileSystem') { if ([System.String]::IsNullOrEmpty($Path)) { throw ($localized.RequiredModuleParameterError -f 'Path'); } elseif (-not (Test-Path -Path $Path)) { throw ($localized.InvalidPathError -f 'Module', $Path); } else { ## If we have a file, ensure it's a .Zip file $fileSystemInfo = Get-Item -Path $Path; if ($fileSystemInfo -is [System.IO.FileInfo]) { if ($fileSystemInfo.Extension -ne '.zip') { throw ($localized.InvalidModulePathExtensionError -f $Path); } } } } #end if FileSystem } process { $moduleCachePath = (Get-ConfigurationData -Configuration Host).ModuleCachePath; ## If no provider specified, default to the PSGallery if (([System.String]::IsNullOrEmpty($Provider)) -or ($Provider -eq 'PSGallery')) { ## PowerShell Gallery modules are just suffixed with -v<Version>.zip $moduleRegex = '^{0}-v.+\.zip$' -f $Name; } elseif ($Provider -eq 'GitHub') { ## GitHub modules are suffixed with -v<Version>_<Owner>_<Branch>.zip $moduleRegex = '^{0}(-v.+)?_{1}_{2}\.zip$' -f $Name, $Owner, $Branch; } Write-Debug -Message ("Searching for files matching pattern '$moduleRegex'."); if ($Provider -in 'FileSystem') { ## We have a directory or a .zip file, so just return this return (Get-Item -Path $Path); } elseif ($Provider -in 'PSGallery', 'AzDo', 'GitHub') { $modules = Get-ChildItem -Path $moduleCachePath -ErrorAction SilentlyContinue | Where-Object Name -match $moduleRegex | ForEach-Object { Write-Debug -Message ("Discovered file '$($_.FullName)'."); $trimStart = '{0}-v' -f $Name; $moduleVersionString = $PSItem.Name.TrimStart($trimStart); $moduleVersionString = $moduleVersionString -replace '(_\S+_\S+)?\.zip', ''; ## If we have no version number, default to the lowest version if ([System.String]::IsNullOrEmpty($moduleVersionString)) { $moduleVersionString = '0.0'; } $discoveredModule = [PSCustomObject] @{ Name = $Name; Version = $moduleVersionString -as [System.Version]; FileInfo = $PSItem; } Write-Output -InputObject $discoveredModule; } } if ($null -ne $RequiredVersion) { Write-Debug -Message ("Checking for modules that match version '$RequiredVersion'."); Write-Output -InputObject ( $modules | Where-Object { $_.Version -eq $RequiredVersion } | Select-Object -ExpandProperty FileInfo); } elseif ($null -ne $MinimumVersion) { Write-Debug -Message ("Checking for modules with a minimum version of '$MinimumVersion'."); Write-Output -InputObject ( $modules | Where-Object Version -ge $MinimumVersion | Sort-Object -Property Version | Select-Object -Last 1 -ExpandProperty FileInfo); } else { Write-Debug -Message ("Checking for the latest module version."); Write-Output -InputObject ( $modules | Sort-Object -Property Version | Select-Object -Last 1 -ExpandProperty FileInfo); } } #end process } #end function Get-ModuleCache function Get-LabModuleCacheManifest { <# .SYNOPSIS Returns a zipped module's manifest. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## File path to the zipped module [Parameter(Mandatory)] [System.String] $Path, [ValidateSet('PSGallery','GitHub', 'AzDo')] [System.String] $Provider = 'PSGallery' ) begin { if (-not (Test-Path -Path $Path -PathType Leaf)) { throw ($localized.InvalidPathError -f 'Module', $Path); } } process { Write-Debug -Message 'Loading ''System.IO.Compression'' .NET binaries.'; [ref] $null = [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression"); [ref] $null = [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem"); $moduleFileInfo = Get-Item -Path $Path; if ($Provider -in 'PSGallery', 'AzDo') { $moduleName = $moduleFileInfo.Name -replace '\.zip', ''; }elseif ($Provider -eq 'GitHub') { ## If we have a GitHub module, trim the _Owner_Branch.zip; if we have a PSGallery module, trim the .zip $moduleName = $moduleFileInfo.Name -replace '_\S+_\S+\.zip', ''; } $moduleManifestName = '{0}.psd1' -f $moduleName; $temporaryArchivePath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "$moduleName.psd1"; try { ### Open the ZipArchive with read access Write-Verbose -Message ($localized.OpeningArchive -f $moduleFileInfo.FullName); $archive = New-Object System.IO.Compression.ZipArchive(New-Object System.IO.FileStream($moduleFileInfo.FullName, [System.IO.FileMode]::Open)); ## Zip archive entries are case-sensitive, therefore, we need to search for a match and can't use ::GetEntry() foreach ($archiveEntry in $archive.Entries) { if ($archiveEntry.Name -eq $moduleManifestName) { $moduleManifestArchiveEntry = $archiveEntry; } } [System.IO.Compression.ZipFileExtensions]::ExtractToFile($moduleManifestArchiveEntry, $temporaryArchivePath, $true); $moduleManifest = ConvertTo-ConfigurationData -ConfigurationData $temporaryArchivePath; } catch { Write-Error ($localized.ReadingArchiveItemError -f $moduleManifestName); } finally { if ($null -ne $archive) { Write-Verbose -Message ($localized.ClosingArchive -f $moduleFileInfo.FullName); $archive.Dispose(); } Remove-Item -Path $temporaryArchivePath -Force; } return $moduleManifest; } #end process } #end function function Get-LabMofModule { <# .SYNOPSIS Retrieves a list of DSC resource modules defined in a MOF file. #> [CmdletBinding(DefaultParameterSetName='Path')] [OutputType([System.Collections.Hashtable])] param ( # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('PSPath','FullName')] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $LiteralPath ) begin { $definedModules = @{ }; } process { if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } if ($Path -match '\.meta\.mof$') { Write-Warning -Message ($localized.SkippingMetaConfigurationWarning -f $Path); } else { Write-Verbose -Message ($localized.ProcessingMofFile -f $Path); $currentModule = $null; $currentLineNumber = 0; foreach ($line in [System.IO.File]::ReadLines($Path)) { $currentLineNumber++; if ($line -match '^instance of (?!MSFT_Credential|MSFT_xWebBindingInformation)') { ## Ignore MSFT_Credential and MSFT_xWebBindingInformation types. There may be ## other types that need suppressing, but they'll be resource specific if ($null -eq $currentModule) { ## Ignore the very first instance definition! } elseif (($currentModule.ContainsKey('Name')) -and ($currentModule.ContainsKey('RequiredVersion'))) { $definedModules[($currentModule.Name)] = $currentModule; } else { Write-Warning -Message ($localized.CannotResolveMofModuleWarning -f $instanceLineNumber); } $instanceLineNumber = $currentLineNumber; $currentModule = @{ }; } elseif ($line -match '(?<=\s?ModuleName\s?=\s?")\w+(?=";)') { $currentModule['Name'] = $Matches[0]; } elseif ($line -match '(?<=\s?ModuleVersion\s?=\s?")\d+(\.\d+){1,3}(?=";)') { $currentModule['RequiredVersion'] = $Matches[0]; } } #end foreach line } } #end process end { foreach ($module in $definedModules.GetEnumerator()) { ## Exclude the default/built-in PSDesiredStateConfiguration module if ($module.Key -ne 'PSDesiredStateConfiguration') { Write-Output -InputObject $module.Value; } } } #end end } #end function function Get-LabVirtualMachineProperty { <# .SYNOPSIS Gets the properties required by DSC xVMHyperV. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String[]] $SwitchName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Media, [Parameter(Mandatory)] [System.UInt64] $StartupMemory, [Parameter(Mandatory)] [System.UInt64] $MinimumMemory, [Parameter(Mandatory)] [System.UInt64] $MaximumMemory, [Parameter(Mandatory)] [System.Int32] $ProcessorCount, [Parameter()] [AllowNull()] [System.String[]] $MACAddress, [Parameter()] [System.Boolean] $SecureBoot, [Parameter()] [System.Boolean] $GuestIntegrationServices, [Parameter()] [System.Boolean] $AutomaticCheckpoints, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { ## Resolve the media to determine whether we require a Generation 1 or 2 VM.. if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $labMedia = Resolve-LabMedia -Id $Media -ConfigurationData $ConfigurationData; $labImage = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData; } else { $labMedia = Resolve-LabMedia -Id $Media; $labImage = Get-LabImage -Id $Media; } if (-not $labImage) { ## Should only trigger during a Reset-VM where parent image is not available?! ## It will be downloaded during any New-LabVirtualMachine calls.. $labImage = @{ Generation = 'VHDX'; } } $labMediaArchitecture = $labMedia.Architecture; if (-not [System.String]::IsNullOrEmpty($labMedia.CustomData.PartitionStyle)) { ## The partition style has been overridden so use this if ($labMedia.CustomData.PartitionStyle -eq 'MBR') { $labMediaArchitecture = 'x86'; } elseif ($labMedia.CustomData.PartitionStyle -eq 'GPT') { $labMediaArchitecture = 'x64'; } } if ($null -ne $labMedia.CustomData.VmGeneration) { ## Use the specified VM generation $PSBoundParameters.Add('Generation', $labMedia.CustomData.VmGeneration); } elseif ($labImage.Generation -eq 'VHD') { ## VHD files are only supported in G1 VMs $PSBoundParameters.Add('Generation', 1); } elseif ($labMediaArchitecture -eq 'x86') { ## Assume G1 for x86 media $PSBoundParameters.Add('Generation', 1); } elseif ($labMediaArchitecture -eq 'x64') { ## Assume G2 for x64 media $PSBoundParameters.Add('Generation', 2); } if ($null -eq $MACAddress) { [ref] $null = $PSBoundParameters.Remove('MACAddress'); } if ($PSBoundParameters.ContainsKey('GuestIntegrationServices')) { [ref] $null = $PSBoundParameters.Add('EnableGuestService', $GuestIntegrationServices); [ref] $null = $PSBoundParameters.Remove('GuestIntegrationServices'); } if ($PSBoundParameters.ContainsKey('AutomaticCheckpoints')) { ## Always remove 'AutomaticCheckpoints' property (#294) [ref] $null = $PSBoundParameters.Remove('AutomaticCheckpoints'); ## Automatic checkpoints were only introduced in 1709 (and later) builds. if (Test-WindowsBuildNumber -MinimumVersion 16299) { [ref] $null = $PSBoundParameters.Add('AutomaticCheckpointsEnabled', $AutomaticCheckpoints); } else { Write-Debug -Message ($localized.AutomaticCheckPointsNotSupported); } } $resolveLabVMDiskPathParams = @{ Name = $Name; Generation = $labImage.Generation; EnvironmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; } $vhdPath = Resolve-LabVMDiskPath @resolveLabVMDiskPathParams; [ref] $null = $PSBoundParameters.Remove('Media'); [ref] $null = $PSBoundParameters.Remove('ConfigurationData'); [ref] $null = $PSBoundParameters.Add('VhdPath', $vhdPath); [ref] $null = $PSBoundParameters.Add('RestartIfNeeded', $true); return $PSBoundParameters; } #end process } #end function function Get-LabVMDisk { <# .SYNOPSIS Retrieves lab virtual machine disk (VHDX) if present. .DESCRIPTION Gets a VM disk configuration using the xVHD DSC resource. #> [CmdletBinding()] param ( ## VM/node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData; } else { $image = Get-LabImage -Id $Media; } $vhd = @{ Name = $Name; Path = $hostDefaults.DifferencingVhdPath; ParentPath = $image.ImagePath; Generation = $image.Generation; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD; Get-LabDscResource -ResourceName VHD -Parameters $vhd; } #end process } #end function function Get-LabVMSnapshot { <# .SYNOPSIS Gets snapshots of all virtual machines with the specified snapshot name. #> [CmdletBinding()] param ( ## VM/node name. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, ## Snapshot name to restore. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $SnapshotName ) process { foreach ($vmName in $Name) { $snapshot = Hyper-V\Get-VMSnapshot -VMName $vmName -Name $SnapshotName -ErrorAction SilentlyContinue; if (-not $snapshot) { Write-Warning -Message ($localized.SnapshotMissingWarning -f $SnapshotName, $vmName); } else { Write-Output -InputObject $snapshot; } } #end foreach VM } #end process } #end function function Get-ResourceDownload { <# .SYNOPSIS Retrieves a downloaded resource's details. .NOTES Based upon https://github.com/iainbrighton/cRemoteFile/blob/master/DSCResources/VE_RemoteFile/VE_RemoteFile.ps1 #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Checksum, [Parameter(ValueFromPipelineByPropertyName)] [System.UInt32] $BufferSize = 64KB ##TODO: Support Headers and UserAgent ) process { $checksumPath = '{0}.checksum' -f $DestinationPath; if (-not (Test-Path -Path $DestinationPath)) { Write-Verbose -Message ($localized.MissingResourceFile -f $DestinationPath); } elseif (-not (Test-Path -Path $checksumPath)) { [ref] $null = Set-ResourceChecksum -Path $DestinationPath; } if (Test-Path -Path $checksumPath) { Write-Debug -Message ('MD5 checksum file ''{0}'' found.' -f $checksumPath); $md5Checksum = (Get-Content -Path $checksumPath -Raw).Trim(); Write-Debug -Message ('Discovered MD5 checksum ''{0}''.' -f $md5Checksum); } else { Write-Debug -Message ('MD5 checksum file ''{0}'' not found.' -f $checksumPath); } $resource = @{ DestinationPath = $DestinationPath; Uri = $Uri; Checksum = $md5Checksum; } return $resource; } #end process } #end function function Get-WindowsImageByIndex { <# .SYNOPSIS Locates the specified WIM image name by its index. #> [CmdletBinding()] [OutputType([System.String])] param ( # WIM image path [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $ImagePath, # Windows image index [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Int32] $ImageIndex ) process { Write-Verbose -Message ($localized.LocatingWimImageName -f $ImageIndex); Get-WindowsImage -ImagePath $ImagePath -Verbose:$false | Where-Object ImageIndex -eq $ImageIndex | Select-Object -ExpandProperty ImageName; } #end process } #end function Get-WindowsImageByIndex function Get-WindowsImageByName { <# .SYNOPSIS Locates the specified WIM image index by its name, i.e. SERVERSTANDARD or SERVERDATACENTERSTANDARD. .OUTPUTS The WIM image index. #> [CmdletBinding()] [OutputType([System.Int32])] param ( # WIM image path [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $ImagePath, # Windows image name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ImageName ) process { Write-Verbose -Message ($localized.LocatingWimImageIndex -f $ImageName); Get-WindowsImage -ImagePath $ImagePath -Verbose:$false | Where-Object ImageName -eq $ImageName | Select-Object -ExpandProperty ImageIndex; } #end process } #end function Get-WindowsImageByName function Import-DismModule { <# .SYNOPSIS Imports the required DISM dll. #> [CmdletBinding()] param ( ) process { $dismPath = (Get-LabHostDefault).DismPath; Remove-Module -Name 'Microsoft.Dism.PowerShell' -ErrorAction SilentlyContinue; $dismModule = Import-Module -Name $dismPath -Force -Scope Global -PassThru -Verbose:$false; $labDefaults.DismVersion = $dismModule.Version; Write-Verbose -Message ($localized.LoadedModuleVersion -f 'Dism', $dismModule.Version); } #end process } #end function function Import-LabDscResource { <# .SYNOPSIS Imports a DSC module resource. .DESCRIPTION Imports a DSC resource as Test-<Prefix>TargetResource and Set-<Prefix>TargetResource etc. #> [CmdletBinding()] param ( ## DSC resource's module name containing the resource [Parameter(Mandatory, ValueFromPipeline)] [System.String] $ModuleName, ## DSC resource's name to import [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $ResourceName, ## Local prefix, defaults to the resource name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Prefix = $ResourceName, ## Use the built-in/default DSC resource [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseDefault ) process { ## Check whether the resource is already imported/registered Write-Debug ($localized.CheckingDscResource -f $ModuleName, $ResourceName); $testCommandName = 'Test-{0}TargetResource' -f $Prefix; if (-not (Get-Command -Name $testCommandName -ErrorAction SilentlyContinue)) { if ($UseDefault) { Write-Verbose -Message ($localized.ImportingDscResource -f $ModuleName, $ResourceName); $resourcePath = Get-LabDscModule -ModuleName $ModuleName -ResourceName $ResourceName -ErrorAction Stop; } else { Write-Verbose -Message ($localized.ImportingBundledDscResource -f $ModuleName, $ResourceName); $dscModuleRootPath = '{0}\{1}\{2}\DSCResources' -f $labDefaults.ModuleRoot, $labDefaults.DscResourceDirectory, $ModuleName; $dscResourcePath = '{0}\{0}.psm1' -f $ResourceName; $resourcePath = Join-Path -Path $dscModuleRootPath -ChildPath $dscResourcePath; } if ($resourcePath) { ## Import the DSC module into the module's global scope to improve performance Import-Module -Name $resourcePath -Prefix $Prefix -Force -Scope Global -Verbose:$false; } } else { Write-Debug -Message ($localized.DscResourceAlreadyImported -f $ModuleName, $ResourceName); } } #end process } #end function function Invoke-Executable { <# .SYNOPSIS Runs an executable and redirects StdOut and StdErr. #> [CmdletBinding()] [OutputType([System.Int32])] param ( # Executable path [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Path, # Executable arguments [Parameter(Mandatory)] [ValidateNotNull()] [System.Array] $Arguments, # Redirected StdOut and StdErr log name [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $LogName = ('{0}.log' -f $Path) ) process { $processArgs = @{ FilePath = $Path; ArgumentList = $Arguments; Wait = $true; RedirectStandardOutput = '{0}\{1}-StdOut.log' -f $env:temp, $LogName; RedirectStandardError = '{0}\{1}-StdErr.log' -f $env:temp, $LogName; NoNewWindow = $true; PassThru = $true; } Write-Debug -Message ($localized.RedirectingOutput -f 'StdOut', $processArgs.RedirectStandardOutput); Write-Debug -Message ($localized.RedirectingOutput -f 'StdErr', $processArgs.RedirectStandardError); Write-Verbose -Message ($localized.StartingProcess -f $Path, [System.String]::Join(' ', $Arguments)); $process = Start-Process @processArgs; if ($process.ExitCode -ne 0) { Write-Warning -Message ($localized.ProcessExitCode -f $Path, $process.ExitCode) } else { Write-Verbose -Message ($localized.ProcessExitCode -f $Path, $process.ExitCode); } ##TODO: Should this actually return the exit code?! } #end process } #end function function Invoke-LabDscResource { <# .SYNOPSIS Runs the ResourceName DSC resource ensuring it's in the desired state. .DESCRIPTION The Invoke-LabDscResource cmdlet invokes the target $ResourceName\Test-TargetResource function using the supplied $Parameters hastable. If the resource is not in the desired state, the $ResourceName\Set-TargetResource function is called with the $Parameters hashtable to attempt to correct the resource. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [System.String] $ResourceName, [Parameter(Mandatory)] [System.Collections.Hashtable] $Parameters ) process { ## Attempt to expand any paths where parameter name contains 'Path'. Requires ## creating another hashtable to avoid modifying the collection. $resolvedParameters = @{ } foreach ($key in $Parameters.Keys) { $resolvedParameters[$key] = $Parameters[$key]; if ($key -match 'Path') { $resolvedParameters[$key] = Resolve-PathEx -Path $Parameters[$key]; if ($Parameters[$key] -ne $resolvedParameters[$key]) { Write-Debug -Message ("Expanding path '{0}' with value '{1}'." -f $key, $Parameters[$key]); Write-Debug -Message ("Resolved path '{0}' to value '{1}'." -f $key, $resolvedParameters[$key]); } } } $PSBoundParameters['Parameters'] = $resolvedParameters; if (-not (Test-LabDscResource @PSBoundParameters)) { if ($ResourceName -match 'PendingReboot') { throw $localized.PendingRebootWarning; } return (Set-LabDscResource @PSBoundParameters); } else { $setTargetResourceCommand = 'Set-{0}TargetResource' -f $ResourceName; Write-Verbose -Message ($localized.SkippingCommand -f $setTargetResourceCommand); } } #end process } #end function function Invoke-LabMediaDownload { <# .SYNOPSIS Downloads resources. .DESCRIPTION Initiates a download of a media resource. If the resource has already been downloaded and the checksum is correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch. .NOTES ISO/WIM media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the ParentVhdPath location. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Id, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Checksum, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $destinationPath = Join-Path -Path $hostDefaults.HotfixPath -ChildPath $Id; $invokeResourceDownloadParams = @{ DestinationPath = $destinationPath; Uri = $Uri; } if ($Checksum) { [ref] $null = $invokeResourceDownloadParams.Add('Checksum', $Checksum); } [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force; return (Get-Item -Path $destinationPath); } #end process } #end function function Invoke-LabMediaImageDownload { <# .SYNOPSIS Downloads ISO/WIM/VHDX media resources. .DESCRIPTION Initiates a download of a media resource. If the resource has already been downloaded and the checksum is correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch. .NOTES ISO media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the ParentVhdPath location. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( ## Lab media object [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Media, ## Force (re)download of the resource [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $invokeResourceDownloadParams = @{ DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename; Uri = $media.Uri; Checksum = $media.Checksum; } if ($media.MediaType -eq 'VHD') { $invokeResourceDownloadParams['DestinationPath'] = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $media.Filename; } $mediaUri = New-Object -TypeName System.Uri -ArgumentList $Media.Uri; if ($mediaUri.Scheme -eq 'File') { ## Use a bigger buffer for local file copies.. $invokeResourceDownloadParams['BufferSize'] = 1MB; } if ($media.MediaType -eq 'VHD') { ## Always download VHDXs regardless of Uri type [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force; } elseif (($mediaUri.Scheme -eq 'File') -and ($media.MediaType -eq 'WIM') -and $hostDefaults.DisableLocalFileCaching) ## TODO: elseif (($mediaUri.Scheme -eq 'File') -and $hostDefaults.DisableLocalFileCaching) { ## NOTE: Only WIM media can currently be run from a file share (see https://github.com/VirtualEngine/Lab/issues/28) ## Caching is disabled and we have a file resource, so just return the source URI path Write-Verbose -Message ($localized.MediaFileCachingDisabled -f $Media.Id); $invokeResourceDownloadParams['DestinationPath'] = $mediaUri.LocalPath; } else { ## Caching is enabled or it's a http/https source [ref] $null = Invoke-ResourceDownload @invokeResourceDownloadParams -Force:$Force; } return (Get-Item -Path $invokeResourceDownloadParams.DestinationPath); } #end process } #end function function Invoke-LabModuleCacheDownload { <# .SYNOPSIS Downloads a PowerShell module (DSC resource) into the module cache. #> [CmdletBinding(DefaultParameterSetName = 'Name')] [OutputType([System.IO.FileInfo])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [System.String] $Name, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [System.Version] $RequiredVersion, ## GitHub repository owner [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Owner, ## The GitHub repository name, normally the DSC module's name [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Repository = $Name, ## GitHub repository branch [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Branch, ## Source Filesystem module path [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Provider used to download the module [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateSet('PSGallery','GitHub','AzDo','FileSystem')] [System.String] $Provider, ## Lability PowerShell module info hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')] [System.Collections.Hashtable[]] $Module, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath, ## Credentials to access the an Azure DevOps private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential, ## Force a download of the module(s) even if they already exist in the cache. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## Catch all to be able to pass parameter via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) begin { ## Remove -RemainingArguments to stop it being passed on. [ref] $null = $PSBoundParameters.Remove('RemainingArguments'); if ($PSCmdlet.ParameterSetName -ne 'Module') { ## Create a module hashtable $newModule = @{ Name = $Name; Repository = $Repository; } if ($PSBoundParameters.ContainsKey('MinimumVersion')) { $newModule['MinimumVersion'] = $MinimumVersion; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { $newModule['RequiredVersion'] = $RequiredVersion; } if ($PSBoundParameters.ContainsKey('Owner')) { $newModule['Owner'] = $Owner; } if ($PSBoundParameters.ContainsKey('Branch')) { $newModule['Branch'] = $Branch; } if ($PSBoundParameters.ContainsKey('Path')) { $newModule['Path'] = $Path; } if ($PSBoundParameters.ContainsKey('Provider')) { $newModule['Provider'] = $Provider; } $Module = $newModule; } } process { foreach ($moduleInfo in $Module) { if ((-not (Test-LabModuleCache @moduleInfo)) -or ($Force) -or ($moduleInfo.Latest -eq $true)) { if ($moduleInfo.ContainsKey('RequiredVersion')) { Write-Verbose -Message ($localized.ModuleVersionNotCached -f $moduleInfo.Name, $moduleInfo.RequiredVersion); } elseif ($moduleInfo.ContainsKey('MinimumVersion')) { Write-Verbose -Message ($localized.ModuleMinmumVersionNotCached -f $moduleInfo.Name, $moduleInfo.MinimumVersion); } else { Write-Verbose -Message ($localized.ModuleNotCached -f $moduleInfo.Name); } if ((-not $moduleInfo.ContainsKey('Provider')) -or ($moduleInfo['Provider'] -eq 'PSGallery')) { Invoke-LabModuleDownloadFromPSGallery @moduleInfo; } elseif ($moduleInfo['Provider'] -eq 'AzDo') { Invoke-LabModuleDownloadFromAzDo @moduleInfo -FeedCredential $FeedCredential } elseif ($moduleInfo['Provider'] -eq 'GitHub') { Invoke-LabModuleDownloadFromGitHub @moduleInfo; } elseif ($moduleInfo['Provider'] -eq 'FileSystem') { ## We should never get here as filesystem modules are not cached. ## If the test doesn't throw, it should return $true. } } else { Get-LabModuleCache @moduleInfo; } } #end foreach module } #end process } #end function function Invoke-LabModuleDownloadFromAzDo { <# .SYNOPSIS Downloads a PowerShell module/DSC resource from an Azure DevOps Feed to the host's module cache. #> [CmdletBinding(DefaultParameterSetName = 'Latest')] [OutputType([System.IO.FileInfo])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Name, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')] [System.Version] $RequiredVersion, ## Credentials to access the an Azure DevOps private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential, ## Catch all, for splatting parameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $destinationModuleName = '{0}.zip' -f $Name; $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName; # we need to remove the property to pass all remaing arguments else the credentials are not passed $null = $PSBoundParameters.Remove('RemainingArguments'); $setResourceDownloadParams = @{ DestinationPath = $moduleCacheDestinationPath; Uri = Resolve-AzDoModuleUri @PSBoundParameters; NoCheckSum = $true; FeedCredential = $FeedCredential; } $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams; $renameLabModuleCacheVersionParams = @{ Name = $Name; Path = $moduleDestinationPath; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion } elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) { $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion } return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams); } #end process } #end function function Invoke-LabModuleDownloadFromGitHub { <# .SYNOPSIS Downloads a DSC resource if it has not already been downloaded from Github. .NOTES Uses the GitHubRepository module! #> [CmdletBinding(DefaultParameterSetName = 'Latest')] [OutputType([System.IO.DirectoryInfo])] param ( ## PowerShell DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Name, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath, ## The GitHub repository owner, typically 'PowerShell' [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Owner, ## The GitHub repository name, normally the DSC module's name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Repository = $Name, ## The GitHub branch to download, defaults to the 'master' branch [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master', ## Override the local directory name. Only used if the repository name does not ## match the DSC module name [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $OverrideRepositoryName = $Name, ## Force a download, overwriting any existing resources [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')] [System.Version] $RequiredVersion, ## Catch all, for splatting parameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) begin { if (-not $PSBoundParameters.ContainsKey('Owner')) { throw ($localized.MissingParameterError -f 'Owner'); } if (-not $PSBoundParameters.ContainsKey('Branch')) { Write-Warning -Message ($localized.NoModuleBranchSpecified -f $Name); } ## Remove -RemainingArguments to stop it being passed on. [ref] $null = $PSBoundParameters.Remove('RemainingArguments'); ## Add Repository and Branch as they might not have been explicitly passed. $PSBoundParameters['Repository'] = $Repository; $PSBoundParameters['Branch'] = $Branch; $Branch = $Branch.Replace('/', '_') # Fix branch names with slashes (#361) } process { ## GitHub modules are suffixed with '_Owner_Branch.zip' $destinationModuleName = '{0}_{1}_{2}.zip' -f $Name, $Owner, $Branch; $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName; $setResourceDownloadParams = @{ DestinationPath = $moduleCacheDestinationPath; Uri = Resolve-GitHubModuleUri @PSBoundParameters; NoCheckSum = $true; } $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams; $renameLabModuleCacheVersionParams = @{ Name = $Name; Path = $moduleDestinationPath; Owner = $Owner; Branch = $Branch } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion } elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) { $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion } return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams); } #end process } #end function function Invoke-LabModuleDownloadFromPSGallery { <# .SYNOPSIS Downloads a PowerShell module/DSC resource from the PowerShell gallery to the host's module cache. #> [CmdletBinding(DefaultParameterSetName = 'Latest')] [OutputType([System.IO.FileInfo])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Name, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath = (Get-ConfigurationData -Configuration Host).ModuleCachePath, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')] [System.Version] $RequiredVersion, ## Catch all, for splatting parameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $destinationModuleName = '{0}.zip' -f $Name; $moduleCacheDestinationPath = Join-Path -Path $DestinationPath -ChildPath $destinationModuleName; $setResourceDownloadParams = @{ DestinationPath = $moduleCacheDestinationPath; Uri = Resolve-PSGalleryModuleUri @PSBoundParameters; NoCheckSum = $true; } $moduleDestinationPath = Set-ResourceDownload @setResourceDownloadParams; $renameLabModuleCacheVersionParams = @{ Name = $Name; Path = $moduleDestinationPath; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { $renameLabModuleCacheVersionParams['RequiredVersion'] = $RequiredVersion } elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) { $renameLabModuleCacheVersionParams['MinimumVersion'] = $MinimumVersion } return (Rename-LabModuleCacheVersion @renameLabModuleCacheVersionParams); } #end process } #end function function Invoke-ResourceDownload { <# .SYNOPSIS Downloads a web resource if it has not already been downloaded or the checksum is incorrect. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Checksum, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, [Parameter(ValueFromPipelineByPropertyName)] [System.UInt32] $BufferSize = 64KB, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ##TODO: Support Headers and UserAgent ) process { [ref] $null = $PSBoundParameters.Remove('Force'); if (-not (Test-ResourceDownload @PSBoundParameters) -or $Force) { Set-ResourceDownload @PSBoundParameters -Verbose:$false; [ref] $null = Test-ResourceDownload @PSBoundParameters -ThrowOnError; } $resource = Get-ResourceDownload @PSBoundParameters; return [PSCustomObject] $resource; } #end process } #end function function Invoke-WebClientDownload { <# .SYNOPSIS Downloads a (web) resource using System.Net.WebClient. .NOTES This solves issue #19 when running downloading resources using BITS under alternative credentials. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [System.UInt32] $BufferSize = 64KB, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential ) process { try { [System.Net.WebClient] $webClient = New-Object -TypeName 'System.Net.WebClient'; $webClient.Headers.Add('user-agent', $labDefaults.ModuleName); $webClient.Proxy = [System.Net.WebRequest]::GetSystemWebProxy(); if (-not $webClient.Proxy.IsBypassed($Uri)) { $proxyInfo = $webClient.Proxy.GetProxy($Uri); Write-Verbose -Message ($localized.UsingProxyServer -f $proxyInfo.AbsoluteUri); } if ($Credential) { $webClient.Credentials = $Credential; $webClient.Proxy.Credentials = $Credential; } else { $webClient.UseDefaultCredentials = $true; $webClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials; } [System.IO.Stream] $inputStream = $webClient.OpenRead($Uri); [System.UInt64] $contentLength = $webClient.ResponseHeaders['Content-Length']; $path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath); [System.IO.Stream] $outputStream = [System.IO.File]::Create($path); [System.Byte[]] $buffer = New-Object -TypeName System.Byte[] -ArgumentList $BufferSize; [System.UInt64] $bytesRead = 0; [System.UInt64] $totalBytes = 0; $writeProgessActivity = $localized.DownloadingActivity -f $Uri; do { $iteration ++; $bytesRead = $inputStream.Read($buffer, 0, $buffer.Length); $totalBytes += $bytesRead; $outputStream.Write($buffer, 0, $bytesRead); ## Avoid divide by zero if ($contentLength -gt 0) { if ($iteration % 30 -eq 0) { [System.Byte] $percentComplete = ($totalBytes / $contentLength) * 100; $writeProgressParams = @{ Activity = $writeProgessActivity; PercentComplete = $percentComplete; Status = $localized.DownloadStatus -f $totalBytes, $contentLength, $percentComplete; } Write-Progress @writeProgressParams; } } } while ($bytesRead -ne 0) $outputStream.Close(); return (Get-Item -Path $path); } catch { throw ($localized.WebResourceDownloadFailedError -f $Uri); } finally { if ($null -ne $writeProgressActivity) { Write-Progress -Activity $writeProgessActivity -Completed; } if ($null -ne $outputStream) { $outputStream.Close(); } if ($null -ne $inputStream) { $inputStream.Close(); } if ($null -ne $webClient) { $webClient.Dispose(); } } } #end process } #end function function New-Directory { <# .SYNOPSIS Creates a filesystem directory. .DESCRIPTION The New-Directory cmdlet will create the target directory if it doesn't already exist. If the target path already exists, the cmdlet does nothing. #> [CmdletBinding(DefaultParameterSetName = 'ByString', SupportsShouldProcess)] [OutputType([System.IO.DirectoryInfo])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( # Target filesystem directory to create [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByDirectoryInfo')] [ValidateNotNullOrEmpty()] [System.IO.DirectoryInfo[]] $InputObject, # Target filesystem directory to create [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'ByString')] [ValidateNotNullOrEmpty()] [Alias('PSPath')] [System.String[]] $Path ) process { Write-Debug -Message ("Using parameter set '{0}'." -f $PSCmdlet.ParameterSetName); switch ($PSCmdlet.ParameterSetName) { 'ByString' { foreach ($directory in $Path) { $directory = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($directory); Write-Debug -Message ("Testing target directory '{0}'." -f $directory); if (-not (Test-Path -Path $directory -PathType Container)) { if ($PSCmdlet.ShouldProcess($directory, "Create directory")) { Write-Verbose -Message ($localized.CreatingDirectory -f $directory); New-Item -Path $directory -ItemType Directory; } } else { Write-Debug -Message ($localized.DirectoryExists -f $directory); Get-Item -Path $directory; } } #end foreach directory } #end byString 'ByDirectoryInfo' { foreach ($directoryInfo in $InputObject) { Write-Debug -Message ("Testing target directory '{0}'." -f $directoryInfo.FullName); if (-not ($directoryInfo.Exists)) { if ($PSCmdlet.ShouldProcess($directoryInfo.FullName, "Create directory")) { Write-Verbose -Message ($localized.CreatingDirectory -f $directoryInfo.FullName); New-Item -Path $directoryInfo.FullName -ItemType Directory; } } else { Write-Debug -Message ($localized.DirectoryExists -f $directoryInfo.FullName); Write-Output -InputObject $directoryInfo; } } #end foreach directoryInfo } #end byDirectoryInfo } #end switch } #end process } #end function function New-DiskImage { <# .SYNOPSIS Create a new formatted disk image. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## VHD/x file path [Parameter(Mandatory)] [System.String] $Path, ## Disk image partition scheme [Parameter(Mandatory)] [ValidateSet('MBR','GPT')] [System.String] $PartitionStyle, ## Disk image size in bytes [Parameter()] [System.UInt64] $Size = 127GB, ## Disk image size in bytes [Parameter()] [ValidateSet('Dynamic','Fixed')] [System.String] $Type = 'Dynamic', ## Overwrite/recreate existing disk image [Parameter()] [System.Management.Automation.SwitchParameter] $Force, ## Do not dismount the VHD/x and return a reference [Parameter()] [System.Management.Automation.SwitchParameter] $Passthru ) begin { if ((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)) { throw ($localized.ImageAlreadyExistsError -f $Path); } elseif ((Test-Path -Path $Path -PathType Leaf) -and ($Force)) { Hyper-V\Dismount-VHD -Path $Path -ErrorAction Stop; Write-Verbose -Message ($localized.RemovingDiskImage -f $Path); Remove-Item -Path $Path -Force -ErrorAction Stop; } } #end begin process { $newVhdParams = @{ Path = $Path; SizeBytes = $Size; $Type = $true; } Write-Verbose -Message ($localized.CreatingDiskImageType -f $Type.ToLower(), $Path, ($Size/1MB)); [ref] $null = Hyper-V\New-Vhd @newVhdParams; ## Disable BitLocker fixed drive write protection (if enabled) Disable-BitLockerFDV; Write-Verbose -Message ($localized.MountingDiskImage -f $Path); $vhdMount = Hyper-V\Mount-VHD -Path $Path -Passthru; Write-Verbose -Message ($localized.InitializingDiskImage -f $Path); [ref] $null = Storage\Initialize-Disk -Number $vhdMount.DiskNumber -PartitionStyle $PartitionStyle -PassThru; switch ($PartitionStyle) { 'MBR' { New-DiskImageMbr -Vhd $vhdMount; } 'GPT' { New-DiskImageGpt -Vhd $vhdMount; } } if ($Passthru) { return $vhdMount; } else { Hyper-V\Dismount-VHD -Path $Path; } ## Enable BitLocker (if required) Assert-BitLockerFDV; } #end process } #end function function New-DiskImageGpt { <# .SYNOPSIS Create a new GPT-formatted disk image. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [ValidateNotNull()] [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk ) process { ## Temporarily disable Windows Explorer popup disk initialization and format notifications ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx Stop-ShellHWDetectionService; Write-Verbose -Message ($localized.CreatingDiskPartition -f 'EFI'); $efiPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -Size 250MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter; Write-Verbose -Message ($localized.FormattingDiskPartition -f 'EFI'); New-DiskPartFat32Partition -DiskNumber $Vhd.DiskNumber -PartitionNumber $efiPartition.PartitionNumber; Write-Verbose -Message ($localized.CreatingDiskPartition -f 'MSR'); [ref] $null = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -Size 128MB -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}'; Write-Verbose -Message ($localized.CreatingDiskPartition -f 'Windows'); $osPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -AssignDriveLetter; Write-Verbose -Message ($localized.FormattingDiskPartition -f 'Windows'); [ref] $null = Storage\Format-Volume -Partition $osPartition -FileSystem NTFS -Force -Confirm:$false; Start-ShellHWDetectionService; } #end process } #end function function New-DiskImageMbr { <# .SYNOPSIS Create a new MBR-formatted disk image. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [ValidateNotNull()] [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk ) process { ## Temporarily disable Windows Explorer popup disk initialization and format notifications ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx Stop-ShellHWDetectionService; Write-Verbose -Message ($localized.CreatingDiskPartition -f 'Windows'); $osPartition = Storage\New-Partition -DiskNumber $Vhd.DiskNumber -UseMaximumSize -MbrType IFS -IsActive | Storage\Add-PartitionAccessPath -AssignDriveLetter -PassThru | Storage\Get-Partition; Write-Verbose -Message ($localized.FormattingDiskPartition -f 'Windows'); [ref] $null = Storage\Format-Volume -Partition $osPartition -FileSystem NTFS -Force -Confirm:$false; Start-ShellHWDetectionService; } #end proces } #end function function New-DiskPartFat32Partition { <# .SYNOPSIS Uses DISKPART.EXE to create a new FAT32 system partition. This permits mocking of DISKPART calls. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( [Parameter(Mandatory)] [System.Int32] $DiskNumber, [Parameter(Mandatory)] [System.Int32] $PartitionNumber ) process { @" select disk $DiskNumber select partition $PartitionNumber format fs=fat32 label="System" "@ | & "$env:SystemRoot\System32\DiskPart.exe" | Out-Null; } #end process } #end function function New-EmptyDiskImage { <# .SYNOPSIS Create an empty disk image. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## VHD/x file path [Parameter(Mandatory)] [System.String] $Path, ## Disk image size in bytes [Parameter()] [System.UInt64] $Size = 127GB, ## Disk image size in bytes [Parameter()] [ValidateSet('Dynamic','Fixed')] [System.String] $Type = 'Dynamic', ## Overwrite/recreate existing disk image [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) begin { if ((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)) { throw ($localized.ImageAlreadyExistsError -f $Path); } elseif ((Test-Path -Path $Path -PathType Leaf) -and ($Force)) { Hyper-V\Dismount-VHD -Path $Path -ErrorAction Stop; Write-Verbose -Message ($localized.RemovingDiskImage -f $Path); Remove-Item -Path $Path -Force -ErrorAction Stop; } } #end begin process { $newVhdParams = @{ Path = $Path; SizeBytes = $Size; $Type = $true; } Write-Verbose -Message ($localized.CreatingDiskImageType -f $Type.ToLower(), $Path, ($Size/1MB)); [ref] $null = Hyper-V\New-Vhd @newVhdParams; } #end process } #end function function New-LabBootStrap { <# .SYNOPSIS Creates a lab DSC BootStrap script block. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.String])] param ( [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $CoreCLR, ## Custom default shell [Parameter(ValueFromPipelineByPropertyName)] [System.String] $DefaultShell, ## WSMan maximum envelope size [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $MaxEnvelopeSizeKb = 1024 ) process { $coreCLRScriptBlock = { ## Lability CoreCLR DSC Bootstrap $VerbosePreference = 'Continue'; Import-Certificate -FilePath "$env:SYSTEMDRIVE\BootStrap\LabRoot.cer" -CertStoreLocation 'Cert:\LocalMachine\Root\' -Verbose; ## Import the .PFX certificate with a blank password Import-PfxCertificate -FilePath "$env:SYSTEMDRIVE\BootStrap\LabClient.pfx" -CertStoreLocation 'Cert:\LocalMachine\My\' -Verbose; <#CustomBootStrapInjectionPoint#> if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.meta.mof") { Set-DscLocalConfigurationManager -Path "$env:SystemDrive\BootStrap\" -Verbose; } if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.mof") { Start-DscConfiguration -Path "$env:SystemDrive\Bootstrap\" -Force -Wait -Verbose -ErrorAction Stop; } #end if localhost.mof } #end CoreCLR bootstrap scriptblock $scriptBlock = { ## Lability DSC Bootstrap $VerbosePreference = 'Continue'; $DebugPreference = 'Continue'; $transcriptPath = '{0}\BootStrap\Bootstrap-{1}.log' -f $env:SystemDrive, (Get-Date).ToString('yyyyMMdd-hhmmss'); Start-Transcript -Path $transcriptPath -Force; certutil.exe -addstore -f "Root" "$env:SYSTEMDRIVE\BootStrap\LabRoot.cer"; ## Import the .PFX certificate with a blank password "" | certutil.exe -f -importpfx "$env:SYSTEMDRIVE\BootStrap\LabClient.pfx"; <#CustomBootStrapInjectionPoint#> ## Account for large configurations being "pushed" and increase the default from 500KB to <#MaxEnvelopeSizeKb#>KB (#306) Set-ItemProperty -LiteralPath HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client -Name maxEnvelopeSize -Value <#MaxEnvelopeSizeKb#> -Force -Verbose ## Disable network location wizard pop-up $null = New-Item -Path HKLM:\SYSTEM\CurrentControlSet\Control\Network -Name NewNetworkWindowOff -ItemType Container -Force -Verbose if (Test-Path -Path "$env:SystemDrive\BootStrap\localhost.meta.mof") { Set-DscLocalConfigurationManager -Path "$env:SystemDrive\BootStrap\" -Verbose; } $localhostMofPath = "$env:SystemDrive\BootStrap\localhost.mof"; if (Test-Path -Path $localhostMofPath) { if ($PSVersionTable.PSVersion.Major -eq 4) { ## Convert the .mof to v4 compatible - credit to Mike Robbins ## http://mikefrobbins.com/2014/10/30/powershell-desired-state-configuration-error-undefined-property-configurationname/ $mof = Get-Content -Path $localhostMofPath; $mof -replace '^\sName=.*;$|^\sConfigurationName\s=.*;$' | Set-Content -Path $localhostMofPath -Encoding Unicode -Force; } while ($true) { ## Replay the configuration until the LCM bloody-well takes it (more of a WMF 4 thing)! try { if (Test-Path -Path "$env:SystemRoot\System32\Configuration\Pending.mof") { Start-DscConfiguration -UseExisting -Force -Wait -Verbose -ErrorAction Stop; break; } else { Start-DscConfiguration -Path "$env:SystemDrive\Bootstrap\" -Force -Wait -Verbose -ErrorAction Stop; break; } } catch { Write-Error -Message $_; ## SIGH. Try restarting WMI.. if (-not ($interation % 10)) { ## SIGH. Try removing the configuration and restarting WMI.. Remove-DscConfigurationDocument -Stage Current,Pending,Previous -Force; Restart-Service -Name Winmgmt -Force; } Start-Sleep -Seconds 5; $interation++; } } #end while } #end if localhost.mof Stop-Transcript; } #end bootstrap scriptblock if ($CoreCLR) { $bootstrap = $coreCLRScriptBlock.ToString(); } else { $bootstrap = $scriptBlock.ToString(); } if ($PSBoundParameters.ContainsKey('DefaultShell')) { $shellScriptBlock = { Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\' -Name Shell -Value '{0}' -Force; <#CustomBootStrapInjectionPoint#> } $shellScriptBlockString = $shellScriptBlock.ToString() -f $DefaultShell; $bootstrap = $bootStrap.Replac('<#CustomBootStrapInjectionPoint#>', $shellScriptBlockString); } $bootstrap = $bootstrap -replace '<#MaxEnvelopeSizeKb#>', $MaxEnvelopeSizeKb; return $bootstrap; } #end process } #end function function New-LabMedia { <# .SYNOPSIS Creates a new lab media object. .DESCRIPTION Permits validation of custom NonNodeData\Lability\Media entries. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Id = $(throw ($localized.MissingParameterError -f 'Id')), [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Filename = $(throw ($localized.MissingParameterError -f 'Filename')), [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Description = '', [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('x86','x64')] [System.String] $Architecture = $(throw ($localized.MissingParameterError -f 'Architecture')), [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ImageName = '', [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('ISO','VHD','WIM','NULL')] [System.String] $MediaType = $(throw ($localized.MissingParameterError -f 'MediaType')), [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Uri = $(throw ($localized.MissingParameterError -f 'Uri')), [Parameter(ValueFromPipelineByPropertyName)] [System.String] $Checksum = '', [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ProductKey = '', [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('Windows','Linux')] [System.String] $OperatingSystem = 'Windows', [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Collections.Hashtable] $CustomData = @{}, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Array] $Hotfixes ) begin { ## Confirm we have a valid Uri try { if ($MediaType -ne 'NULL') { $resolvedUri = New-Object -TypeName 'System.Uri' -ArgumentList $Uri; if ($resolvedUri.Scheme -notin 'http','https','file') { throw ($localized.UnsupportedUriSchemeError -f $resolvedUri.Scheme); } } } catch { throw $_; } } process { $labMedia = [PSCustomObject] @{ Id = $Id; Filename = $Filename; Description = $Description; Architecture = $Architecture; ImageName = $ImageName; MediaType = $MediaType; OperatingSystem = $OperatingSystem; Uri = [System.Uri] $Uri; Checksum = $Checksum; CustomData = $CustomData; Hotfixes = $Hotfixes; } ## Ensure any explicit product key overrides the CustomData value if ($ProductKey) { $CustomData['ProductKey'] = $ProductKey; } return $labMedia; } #end process } #end function function New-LabSwitch { <# .SYNOPSIS Creates a new Lability network switch object. .DESCRIPTION Permits validation of custom NonNodeData\Lability\Network entries. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.Collections.Hashtable])] param ( ## Virtual switch name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Virtual switch type [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('Internal','External','Private')] [System.String] $Type, ## Physical network adapter name (for external switches) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $NetAdapterName, ## Share host access (for external virtual switches) [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Boolean] $AllowManagementOS = $false, ## Virtual switch availability [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('Present','Absent')] [System.String] $Ensure = 'Present' ) begin { if (($Type -eq 'External') -and (-not $NetAdapterName)) { throw ($localized.MissingParameterError -f 'NetAdapterName'); } } #end begin process { $newLabSwitch = @{ Name = $Name; Type = $Type; NetAdapterName = $NetAdapterName; AllowManagementOS = $AllowManagementOS; Ensure = $Ensure; } if ($Type -ne 'External') { [ref] $null = $newLabSwitch.Remove('NetAdapterName'); [ref] $null = $newLabSwitch.Remove('AllowManagementOS'); } return $newLabSwitch; } #end process } #end function function New-LabVirtualMachine { <# .SYNOPSIS Creates and configures a new lab virtual machine. .DESCRIPTION Creates an new VM, creating the switch if required, injecting all resources and snapshotting as required. #> [CmdletBinding(DefaultParameterSetName = 'PSCredential')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] 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 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, ## Virtual machine DSC .mof and .meta.mof location [Parameter(ValueFromPipelineByPropertyName)] [System.String] $Path = (Get-LabHostDscConfigurationPath), ## Skip creating baseline snapshots [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoSnapshot, ## Is a quick VM, e.g. created via the New-LabVM cmdlet [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $IsQuickVM, ## 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'); } } process { $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -ErrorAction Stop; $nodeName = $node.NodeName; ## Display name includes any environment prefix/suffix $displayName = $node.NodeDisplayName; if (-not (Test-ComputerName -ComputerName $node.NodeName.Split('.')[0])) { throw ($localized.InvalidComputerNameError -f $node.NodeName); } ## Don't attempt to check certificates for 'Quick VMs' if (-not $IsQuickVM) { ## Check for certificate before we (re)create the VM if (-not [System.String]::IsNullOrWhitespace($node.ClientCertificatePath)) { $expandedClientCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.ClientCertificatePath); if (-not (Test-Path -Path $expandedClientCertificatePath -PathType Leaf)) { throw ($localized.CannotFindCertificateError -f 'Client', $node.ClientCertificatePath); } } else { Write-Warning -Message ($localized.NoCertificateFoundWarning -f 'Client'); } if (-not [System.String]::IsNullOrWhitespace($node.RootCertificatePath)) { $expandedRootCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.RootCertificatePath); if (-not (Test-Path -Path $expandedRootCertificatePath -PathType Leaf)) { throw ($localized.CannotFindCertificateError -f 'Root', $node.RootCertificatePath); } } else { Write-Warning -Message ($localized.NoCertificateFoundWarning -f 'Root'); } } #end if not quick VM $environmentSwitchNames = @(); foreach ($switchName in $node.SwitchName) { ## Retrieve prefixed switch names for VM creation (if necessary) $resolveLabSwitchParams = @{ Name = $switchName; ConfigurationData = $ConfigurationData; WarningAction = 'SilentlyContinue'; } $networkSwitch = Resolve-LabSwitch @resolveLabSwitchParams; Write-Verbose -Message ($localized.SettingVMConfiguration -f 'Virtual Switch', $networkSwitch.Name); $environmentSwitchNames += $networkSwitch.Name; ## Set-LabSwitch also resolves/prefixes the switch name, so pass the naked name (#251) Set-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData; } if (-not (Test-LabImage -Id $node.Media -ConfigurationData $ConfigurationData)) { [ref] $null = New-LabImage -Id $node.Media -ConfigurationData $ConfigurationData; } Write-Verbose -Message ($localized.ResettingVMConfiguration -f 'VHDX', "$displayName.vhdx"); $resetLabVMDiskParams = @{ Name = $displayName; NodeName = $nodeName; Media = $node.Media; ConfigurationData = $ConfigurationData; } Reset-LabVMDisk @resetLabVMDiskParams -ErrorAction Stop; Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM', $displayName); $setLabVirtualMachineParams = @{ Name = $DisplayName; SwitchName = $environmentSwitchNames; Media = $node.Media; StartupMemory = $node.StartupMemory; MinimumMemory = $node.MinimumMemory; MaximumMemory = $node.MaximumMemory; ProcessorCount = $node.ProcessorCount; MACAddress = $node.MACAddress; SecureBoot = $node.SecureBoot; GuestIntegrationServices = $node.GuestIntegrationServices; AutomaticCheckPoints = $node.AutomaticCheckpoints; ConfigurationData = $ConfigurationData; } ## Add VMProcessor, Dvd Drive and additional HDD options foreach ($additionalProperty in 'DvdDrive','ProcessorOption','HardDiskDrive') { if ($node.ContainsKey($additionalProperty)) { $setLabVirtualMachineParams[$additionalProperty] = $node[$additionalProperty]; } } Set-LabVirtualMachine @setLabVirtualMachineParams; $media = Resolve-LabMedia -Id $node.Media -ConfigurationData $ConfigurationData; if (($media.OperatingSystem -eq 'Linux') -or ($media.MediaType -eq 'NULL')) { ## Skip injecting files for Linux VMs.. } else { Write-Verbose -Message ($localized.AddingVMCustomization -f 'VM'); $setLabVMDiskFileParams = @{ NodeName = $nodeName; ConfigurationData = $ConfigurationData; Path = $Path; Credential = $Credential; CoreCLR = $media.CustomData.SetupComplete -eq 'CoreCLR'; MaxEnvelopeSizeKb = $node.MaxEnvelopeSizeKb; } if (-not [System.String]::IsNullOrEmpty($media.CustomData.DefaultShell)) { $setLabVMDiskFileParams['DefaultShell'] = $media.CustomData.DefaultShell; } $resolveCustomBootStrapParams = @{ CustomBootstrapOrder = $node.CustomBootstrapOrder; ConfigurationCustomBootstrap = $node.CustomBootstrap; MediaCustomBootStrap = $media.CustomData.CustomBootstrap; } $customBootstrap = Resolve-LabCustomBootStrap @resolveCustomBootStrapParams; if ($customBootstrap) { $setLabVMDiskFileParams['CustomBootstrap'] = $customBootstrap; } if (-not [System.String]::IsNullOrEmpty($media.CustomData.ProductKey)) { $setLabVMDiskFileParams['ProductKey'] = $media.CustomData.ProductKey; } Set-LabVMDiskFile @setLabVMDiskFileParams -FeedCredential $feedCredential; } #end Windows VMs if (-not $NoSnapshot) { $snapshotName = $localized.BaselineSnapshotName -f $labDefaults.ModuleName; Write-Verbose -Message ($localized.CreatingBaselineSnapshot -f $snapshotName); Hyper-V\Checkpoint-VM -Name $displayName -SnapshotName $snapshotName -Confirm:$false; } if ($node.WarningMessage) { if ($node.WarningMessage -is [System.String]) { Write-Warning -Message ($localized.NodeCustomMessageWarning -f $nodeName, $node.WarningMessage.Trim("`n")); } else { Write-Warning -Message ($localized.IncorrectPropertyTypeError -f 'WarningMessage', '[System.String]') } } Write-Output -InputObject (Hyper-V\Get-VM -Name $displayName); } #end process } #end function function New-LabVMSnapshot { <# .SYNOPSIS Creates a snapshot of all virtual machines with the specified snapshot name. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $SnapshotName ) process { foreach ($vmName in $Name) { Write-Verbose -Message ($localized.CreatingVirtualMachineSnapshot -f $vmName, $SnapshotName); Hyper-V\Checkpoint-VM -VMName $vmName -SnapshotName $SnapshotName; } #end foreach VM } #end process } #end function function New-UnattendXml { <# .SYNOPSIS Creates a Windows unattended installation file. .DESCRIPTION Creates an unattended Windows 8/2012 installation file that will configure an operating system deployed from a WIM file, deploy the operating system and ensure that Powershell's desired state configuration (DSC) is configured to pull its configuration from the specified pull server. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.Xml.XmlDocument])] param ( # Local Administrator Password [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, # Computer name [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ComputerName, # Product Key [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}$')] [System.String] $ProductKey, # Input Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $InputLocale = 'en-US', # System Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $SystemLocale = 'en-US', # User Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $UserLocale = 'en-US', # UI Language [Parameter(ValueFromPipelineByPropertyName)] [System.String] $UILanguage = 'en-US', # Timezone [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Timezone, ##TODO: Validate timezones? # Registered Owner [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $RegisteredOwner = 'Virtual Engine', # Registered Organization [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $RegisteredOrganization = 'Virtual Engine', # TODO: Execute synchronous commands during OOBE pass as they only currently run during the Specialize pass ## Array of hashtables with Description, Order and Path keys [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $ExecuteCommand ) begin { $templateUnattendXml = [System.Xml.XmlDocument] @' <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="specialize"> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> <component name="Microsoft-Windows-Deployment" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>en-US</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>en-US</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>en-US</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>en-US</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <NetworkLocation>Work</NetworkLocation> <ProtectYourPC>3</ProtectYourPC> <SkipUserOOBE>true</SkipUserOOBE> <SkipMachineOOBE>true</SkipMachineOOBE> </OOBE> <ShowWindowsLive>false</ShowWindowsLive> <TimeZone>GMT Standard Time</TimeZone> <UserAccounts> <AdministratorPassword> <Value></Value> <PlainText>false</PlainText> </AdministratorPassword> </UserAccounts> <RegisteredOrganization>Virtual Engine</RegisteredOrganization> <RegisteredOwner>Virtual Engine</RegisteredOwner> <BluetoothTaskbarIconEnabled>true</BluetoothTaskbarIconEnabled> <DoNotCleanTaskBar>false</DoNotCleanTaskBar> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <NetworkLocation>Work</NetworkLocation> <ProtectYourPC>3</ProtectYourPC> <SkipUserOOBE>true</SkipUserOOBE> <SkipMachineOOBE>true</SkipMachineOOBE> </OOBE> <ShowWindowsLive>false</ShowWindowsLive> <TimeZone>GMT Standard Time</TimeZone> <UserAccounts> <AdministratorPassword> <Value></Value> <PlainText>false</PlainText> </AdministratorPassword> </UserAccounts> <RegisteredOrganization>Virtual Engine</RegisteredOrganization> <RegisteredOwner>Virtual Engine</RegisteredOwner> <BluetoothTaskbarIconEnabled>true</BluetoothTaskbarIconEnabled> <DoNotCleanTaskBar>false</DoNotCleanTaskBar> </component> </settings> </unattend> '@ [xml] $unattendXml = $templateUnattendXml; } process { foreach ($setting in $unattendXml.Unattend.Settings) { foreach($component in $setting.Component) { if ($setting.'Pass' -eq 'specialize' -and $component.'Name' -eq 'Microsoft-Windows-Deployment') { if (($null -ne $ExecuteCommand) -or ($ExecuteCommand.Length -gt 0)) { $commandOrder = 1; foreach ($synchronousCommand in $ExecuteCommand) { $runSynchronousElement = $component.AppendChild($unattendXml.CreateElement('RunSynchronous','urn:schemas-microsoft-com:unattend')); $syncCommandElement = $runSynchronousElement.AppendChild($unattendXml.CreateElement('RunSynchronousCommand','urn:schemas-microsoft-com:unattend')); [ref] $null = $syncCommandElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add'); $syncCommandDescriptionElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Description','urn:schemas-microsoft-com:unattend')); [ref] $null = $syncCommandDescriptionElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Description'])); $syncCommandOrderElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Order','urn:schemas-microsoft-com:unattend')); [ref] $null = $syncCommandOrderElement.AppendChild($unattendXml.CreateTextNode($commandOrder)); $syncCommandPathElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Path','urn:schemas-microsoft-com:unattend')); [ref] $null = $syncCommandPathElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Path'])); $commandOrder++; } } } if (($setting.'Pass' -eq 'specialize') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) { if ($ComputerName) { $computerNameElement = $component.AppendChild($unattendXml.CreateElement('ComputerName','urn:schemas-microsoft-com:unattend')); [ref] $null = $computerNameElement.AppendChild($unattendXml.CreateTextNode($ComputerName)); } if ($ProductKey) { $productKeyElement = $component.AppendChild($unattendXml.CreateElement('ProductKey','urn:schemas-microsoft-com:unattend')); [ref] $null = $productKeyElement.AppendChild($unattendXml.CreateTextNode($ProductKey.ToUpper())); } } if ((($setting.'Pass' -eq 'specialize') -or ($setting.'Pass' -eq 'oobeSystem')) -and ($component.'Name' -eq 'Microsoft-Windows-International-Core')) { $component.InputLocale = $InputLocale; $component.SystemLocale = $SystemLocale; $component.UILanguage = $UILanguage; $component.UserLocale = $UserLocale; } if (($setting.'Pass' -eq 'oobeSystem') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) { $component.TimeZone = $Timezone; $concatenatedPassword = '{0}AdministratorPassword' -f $Credential.GetNetworkCredential().Password; $component.UserAccounts.AdministratorPassword.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($concatenatedPassword)); $component.RegisteredOrganization = $RegisteredOrganization; $component.RegisteredOwner = $RegisteredOwner; } } #end foreach setting.Component } #end foreach unattendXml.Unattend.Settings Write-Output -InputObject $unattendXml; } #end process } #end function function Remove-ConfigurationData { <# .SYNOPSIS Removes custom lab configuration data file. #> [CmdletBinding(SupportsShouldProcess)] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( [Parameter(Mandatory)] [ValidateSet('Host','VM','Media','CustomMedia')] [System.String] $Configuration ) process { $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration; if (Test-Path -Path $configurationPath) { Write-Verbose -Message ($localized.ResettingConfigurationDefaults -f $Configuration); Remove-Item -Path $configurationPath -Force; } } #end process } # end function Remove-ConfigurationData function Remove-LabSwitch { <# .SYNOPSIS Removes a virtual network switch configuration. .DESCRIPTION Deletes a virtual network switch configuration using the xVMSwitch DSC resource. #> [CmdletBinding(SupportsShouldProcess)] param ( ## Switch Id/Name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $networkSwitch = Resolve-LabSwitch @PSBoundParameters; if (($null -ne $networkSwitch.IsExisting) -and ($networkSwitch.IsExisting -eq $true)) { if ($PSCmdlet.ShouldProcess($Name)) { $networkSwitch['Ensure'] = 'Absent'; [ref] $null = $networkSwitch.Remove('IsExisting'); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch; [ref] $null = Invoke-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch; } } } #end process } #end function function Remove-LabVirtualMachine { <# .SYNOPSIS Deletes a lab virtual machine. #> [CmdletBinding(SupportsShouldProcess)] 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, ## Include removal of virtual switch(es). By default virtual switches are not removed. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $RemoveSwitch ) process { if (-not (Test-LabNode -Name $Name -ConfigurationData $ConfigurationData)) { throw ($localized.CannotLocateNodeError -f $Name); } $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData; $nodeDisplayName = $node.NodeDisplayName; # Revert to oldest snapshot prior to VM removal to speed things up Hyper-V\Get-VMSnapshot -VMName $nodeDisplayName -ErrorAction SilentlyContinue | Sort-Object -Property CreationTime | Select-Object -First 1 | Hyper-V\Restore-VMSnapshot -Confirm:$false; Remove-LabVMSnapshot -Name $nodeDisplayName; $environmentSwitchNames = @(); foreach ($switchName in $node.SwitchName) { $environmentSwitchNames += Resolve-LabEnvironmentName -Name $switchName -ConfigurationData $ConfigurationData; } Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'VM', $nodeDisplayName); $clearLabVirtualMachineParams = @{ Name = $nodeDisplayName; SwitchName = $environmentSwitchNames; Media = $node.Media; StartupMemory = $node.StartupMemory; MinimumMemory = $node.MinimumMemory; MaximumMemory = $node.MaximumMemory; MACAddress = $node.MACAddress; ProcessorCount = $node.ProcessorCount; ConfigurationData = $ConfigurationData; } Clear-LabVirtualMachine @clearLabVirtualMachineParams; ## Remove the OS disk Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'VHD/X', "$($nodeDisplayName).vhd/vhdx"); $removeLabVMDiskParams = @{ Name = $nodeDisplayName; NodeName = $Name; Media = $node.Media; ConfigurationData = $ConfigurationData; } Remove-LabVMDisk @removeLabVMDiskParams -ErrorAction Stop; if ($RemoveSwitch) { foreach ($switchName in $node.SwitchName) { $environmentSwitchName = Resolve-LabEnvironmentName -Name $switchName -ConfigurationData $ConfigurationData; Write-Verbose -Message ($localized.RemovingNodeConfiguration -f 'Virtual Switch', $environmentSwitchName); Remove-LabSwitch -Name $switchName -ConfigurationData $ConfigurationData; } } } #end process } #end function function Remove-LabVirtualMachineHardDiskDrive { <# .SYNOPSIS Removes a virtual machine's additional hard disk drive(s). .DESCRIPTION Removes one or more additional hard disks. No need to detach the disks as the VM is deleted. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $NodeName, ## Collection of additional hard disk drive configurations [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $HardDiskDrive, ## Configuration environment name [Parameter()] [AllowNull()] [System.String] $EnvironmentName ) process { $vmHardDiskPath = Resolve-LabVMDiskPath -Name $NodeName -EnvironmentName $EnvironmentName -Parent; for ($i = 0; $i -lt $HardDiskDrive.Count; $i++) { $diskDrive = $HardDiskDrive[$i]; $controllerLocation = $i + 1; if ($diskDrive.ContainsKey('VhdPath')) { ## Do not remove VHD/Xs created externally! } else { ## Remove the VHD file $vhdName = '{0}-{1}' -f $NodeName, $controllerLocation; $vhdParams = @{ Name = $vhdName; Path = $vmHardDiskPath; MaximumSizeBytes = $diskDrive.MaximumSizeBytes; Generation = $diskDrive.Generation; Ensure = 'Absent'; } $vhdFilename = '{0}.{1}' -f $vhdName, $diskDrive.Generation.ToLower(); $vhdPath = Join-Path -Path $vmHardDiskPath -ChildPath $vhdFilename; Write-Verbose -Message ($localized.RemovingVhdFile -f $vhdPath); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVhd -Prefix Vhd; Invoke-LabDscResource -ResourceName Vhd -Parameters $vhdParams; } } #end for } #end process } #end function function Remove-LabVMDisk { <# .SYNOPSIS Removes lab VM disk file (VHDX) configuration. .DESCRIPTION Configures a VM disk configuration using the xVHD DSC resource. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( ## VM/node display name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## VM/node name [Parameter(ValueFromPipelineByPropertyName)] [System.String] $NodeName = $Name, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData -ErrorAction Stop; } else { $image = Get-LabImage -Id $Media -ErrorAction Stop; } $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; ## If the parent image isn't there, the differencing VHD won't be either! if ($image) { ## Ensure we look for the correct file extension (#182) $vhdPath = Resolve-LabVMDiskPath -Name $Name -Generation $image.Generation -EnvironmentName $environmentName; if (Test-Path -Path $vhdPath) { ## Only attempt to remove the differencing disk if it's there (and xVHD will throw) $vhd = @{ Name = $Name; Path = Split-Path -Path $vhdPath -Parent; ParentPath = $image.ImagePath; Generation = $image.Generation; Type = 'Differencing'; Ensure = 'Absent'; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD; [ref] $null = Invoke-LabDscResource -ResourceName VHD -Parameters $vhd; } } if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $resolveNodePropertyValueParams = @{ NodeName = $NodeName; ConfigurationData = $ConfigurationData; NoEnumerateWildcardNode = $true; ErrorAction = 'Stop'; } $node = Resolve-NodePropertyValue @resolveNodePropertyValueParams; if ($node.ContainsKey('HardDiskDrive')) { ## Remove additional HDDs $removeLabVirtualMachineHardDiskDriveParams = @{ NodeName = $node.NodeDisplayName; HardDiskDrive = $node.HardDiskDrive; EnvironmentName = $environmentName; } $null = Remove-LabVirtualMachineHardDiskDrive @removeLabVirtualMachineHardDiskDriveParams; } } } #end process } #end function function Remove-LabVMSnapshot { <# .SYNOPSIS Removes a VM snapshot. #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String[]] $Name, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $SnapshotName = '*' ) process { <## TODO: Add the ability to force/wait for the snapshots to be removed. When removing snapshots it take a minute or two before the files are actually removed. This causes issues when performing a lab reset #> foreach ($vmName in $Name) { # Sort by descending CreationTime to ensure we will not have to commit changes from one snapshot to another Hyper-V\Get-VMSnapshot -VMName $vmName -ErrorAction SilentlyContinue | Where-Object Name -like $SnapshotName | Sort-Object -Property CreationTime -Descending | ForEach-Object { Write-Verbose -Message ($localized.RemovingSnapshot -f $vmName, $_.Name); Hyper-V\Remove-VMSnapshot -VMName $_.VMName -Name $_.Name -Confirm:$false; } } #end foreach VM } #end process } #end function function Rename-LabModuleCacheVersion { <# .SYNOPSIS Renames a cached module zip file with its version number. #> [CmdletBinding(DefaultParameterSetName = 'PSGallery')] [OutputType([System.IO.FileInfo])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Name, ## Destination directory path to download the PowerShell module/DSC resource module to [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Path, ## GitHub module repository owner [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'GitHub')] [System.String] $Owner, ## GitHub module branch [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'GitHub')] [System.String] $Branch, ## The minimum version of the module required [Parameter(ValueFromPipelineByPropertyName)] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(ValueFromPipelineByPropertyName)] [System.Version] $RequiredVersion ) process { if ($PSCmdlet.ParameterSetName -eq 'GitHub') { $moduleManifest = Get-LabModuleCacheManifest -Path $Path -Provider 'GitHub'; $moduleVersion = $moduleManifest.ModuleVersion; $versionedModuleFilename = '{0}-v{1}_{2}_{3}.zip' -f $Name, $moduleVersion, $Owner, $Branch; } else { $moduleManifest = Get-LabModuleCacheManifest -Path $Path; $moduleVersion = $moduleManifest.ModuleVersion; $versionedModuleFilename = '{0}-v{1}.zip' -f $Name, $moduleVersion; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { if ($moduleVersion -ne $RequiredVersion) { throw ($localized.ModuleVersionMismatchError -f $Name, $moduleVersion, $RequiredVersion); } } elseif ($PSBoundParameters.ContainsKey('MinimumVersion')) { if ($moduleVersion -lt $MinimumVersion) { throw ($localized.ModuleVersionMismatchError -f $Name, $moduleVersion, $MinimumVersion); } } $versionedModulePath = Join-Path -Path (Split-Path -Path $Path -Parent) -ChildPath $versionedModuleFilename; if (Test-Path -Path $versionedModulePath -PathType Leaf) { ## Remove existing version module Remove-Item -Path $versionedModulePath -Force -Confirm:$false; } Rename-Item -Path $Path -NewName $versionedModuleFilename; return (Get-Item -Path $versionedModulePath); } #end process } #end function function Reset-LabVMDisk { <# .SYNOPSIS Removes and resets lab VM disk file (VHDX) configuration. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## VM/node display name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## VM/node name [Parameter(ValueFromPipelineByPropertyName)] [System.String] $NodeName = $Name, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $null = $PSBoundParameters.Remove('NodeName'); Remove-LabVMSnapshot -Name $Name; Remove-LabVMDisk -NodeName $NodeName @PSBoundParameters; Set-LabVMDisk @PSBoundParameters; } #end process } #end function function Resolve-AzDoModuleUri { <# .SYNOPSIS Returns the direct download Uri for a PowerShell module hosted on the Azure DevOps Artifacts. #> [CmdletBinding()] [OutputType([System.String])] param ( ## PowerShell DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## The minimum version of the DSC module required [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the DSC module required [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## Direct download Uri [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential, ## Catch all, for splatting $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { if ($PSBoundParameters.ContainsKey('Uri')) { $psRepositoryUri = $Uri; } else { $psRepositoryUri = (Get-ConfigurationData -Configuration Host).RepositoryUri; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { ## Download the specific version ## you would expect to be able to use $RequiredVersion as the version ## However this fails if a 4 part version is used as Azure Artifcates always uses 3 part versions return ('{0}?id={1}&version={2}' -f $psRepositoryUri, $Name.ToLower(), "$($RequiredVersion.Major).$($RequiredVersion.Minor).$($RequiredVersion.Build)") } else { ## Find the latest package versions $invokeRestMethodParams = @{ Uri = "{0}/FindPackagesById()?id='{1}'" -f $psRepositoryUri, $Name.ToLower() } if ($PSBoundParameters.ContainsKey('FeedCredential')) { $invokeRestMethodParams['Credential'] = $FeedCredential } $azDoPackages = Invoke-RestMethod @invokeRestMethodParams ## Find and return the latest version $lastestDoPackageVersion = $azDoPackages | ForEach-Object { $_.properties.NormalizedVersion -as [System.Version] } | Sort-Object | Select-Object -Last 1 return ('{0}?id={1}&version={2}' -f $psRepositoryUri, $Name.ToLower(), $lastestDoPackageVersion.ToString()) } } #end process } #end function Resolve-AzDoModuleUri.ps1 function Resolve-ConfigurationDataPath { <# .SYNOPSIS Resolves the lab configuration data path. .NOTES When -IncludeDefaultPath is specified, if the configuration data file is not found, the default module configuration path is returned. #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter(Mandatory)] [ValidateSet('Host', 'VM', 'Media', 'CustomMedia','LegacyMedia')] [System.String] $Configuration, [Parameter(ParameterSetName = 'Default')] [System.Management.Automation.SwitchParameter] $IncludeDefaultPath, [Parameter(Mandatory, ParameterSetName = 'IsDefaultPath')] [System.Management.Automation.SwitchParameter] $IsDefaultPath ) process { switch ($Configuration) { 'Host' { $configPath = $labDefaults.HostConfigFilename; } 'VM' { $configPath = $labDefaults.VMConfigFilename; } 'Media' { $configPath = $labDefaults.MediaConfigFilename; } 'CustomMedia' { $configPath = $labDefaults.CustomMediaConfigFilename; } 'LegacyMedia' { $configPath = $labDefaults.LegacyMediaPath; } } $configPath = Join-Path -Path $labDefaults.ConfigurationData -ChildPath $configPath; $resolvedPath = Join-Path -Path "$env:ALLUSERSPROFILE\$($labDefaults.ModuleName)" -ChildPath $configPath; if ($IsDefaultPath) { $resolvedPath = Join-Path -Path $labDefaults.ModuleRoot -ChildPath $configPath; } elseif ($IncludeDefaultPath) { if (-not (Test-Path -Path $resolvedPath)) { $resolvedPath = Join-Path -Path $labDefaults.ModuleRoot -ChildPath $configPath; } } $resolvedPath = Resolve-PathEx -Path $resolvedPath; Write-Debug -Message ('Resolved ''{0}'' configuration path to ''{1}''.' -f $Configuration, $resolvedPath); return $resolvedPath; } #end process } #end function ReolveConfigurationPath function Resolve-ConfigurationPath { <# .SYNOPSIS Resolves a node's .mof configuration file path. .DESCRIPTION Searches the current working directory and host configuration data path for a node's .mof files, searching an environment name subdirectory if environment name is defined. #> [CmdletBinding()] param ( ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Lab vm/node name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## Defined .mof path [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Path, ## Do not throw and return default configuration path [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $UseDefaultPath ) process { Write-Verbose -Message ($localized.SearchingConfigurationPaths); try { ## Do we have an environment name? $configurationName = $ConfigurationData.NonNodeData[$labDefaults.moduleName].EnvironmentName; } catch { Write-Debug -Message 'No environment name defined'; } if (-not [System.String]::IsNullOrEmpty($Path)) { ## Search the Specified path $resolvedPath = Resolve-PathEx -Path $Path; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } elseif ($configurationName) { ## Search the Specified\ConfigurationName path $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } } } ## Search the ConfigurationPath path $configurationPath = Get-LabHostDscConfigurationPath; $resolvedPath = Resolve-PathEx -Path $configurationPath; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } elseif ($configurationName) { ## Search the ConfigurationPath\ConfigurationName path $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } } ## Search the Current path $currentPath = (Get-Location -PSProvider FileSystem).Path; $resolvedPath = Resolve-PathEx -Path $currentPath; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } elseif ($configurationName) { ## Search the Current\ConfigurationName path $resolvedPath = Join-Path -Path $resolvedPath -ChildPath $configurationName; if (Test-ConfigurationPath -Name $Name -Path $resolvedPath) { return $resolvedPath; } } if ($UseDefaultPath) { if (-not [System.String]::IsNullOrEmpty($Path)) { ## Return the specified path return (Resolve-PathEx -Path $Path); } else { ## Return the default configuration path return Get-LabHostDscConfigurationPath; } } else { ## We cannot resolve/locate the mof files.. throw ($localized.CannotLocateMofFileError -f $Name); } } #end process } #end function Resolve-ConfigurationPath function Resolve-DismPath { <# .SYNOPSIS Resolves the specified path to a path to DISM dll. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Path ) process { if (-not (Test-Path -Path $Path)) { ## Path doesn't exist throw ($localized.InvalidPathError -f 'Directory', $DismPath); } else { $dismItem = Get-Item -Path $Path; $dismDllName = 'Microsoft.Dism.Powershell.dll'; if ($dismItem.Name -ne $dismDllName) { if ($dismItem -is [System.IO.DirectoryInfo]) { $dismItemPath = Join-Path -Path $DismPath -ChildPath $dismDllName; if (-not (Test-Path -Path $dismItemPath)) { throw ($localized.CannotLocateDismDllError -f $Path); } else { $dismItem = Get-Item -Path $dismItemPath; } } else { throw ($localized.InvalidPathError -f 'File', $DismPath); } } } return $dismItem.FullName; } #end process } #end function function Resolve-GitHubModuleUri { <# .SYNOPSIS Resolves the correct GitHub URI for the specified Owner, Repository and Branch. #> [CmdletBinding()] [OutputType([System.Uri])] param ( ## GitHub repository owner [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Owner, ## GitHub repository name [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Repository, ## GitHub repository branch [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Branch = 'master', ## Catch all to be able to pass parameter via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $uri = 'https://github.com/{0}/{1}/archive/{2}.zip' -f $Owner, $Repository, $Branch; return New-Object -TypeName System.Uri -ArgumentList $uri; } #end process } #end function function Resolve-LabConfigurationModule { <# .SYNOPSIS Resolves a lab module definition by its name from Lability configuration data. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, [Parameter(Mandatory)] [ValidateSet('Module','DscResource')] [System.String] $ModuleType, ## Lab module name/ID [Parameter(ValueFromPipelineByPropertyName)] [System.String[]] $Name, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $ThrowIfNotFound ) process { $modules = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).$ModuleType; if (($PSBoundParameters.ContainsKey('Name')) -and ($Name -notcontains '*')) { ## Check we have them all first.. foreach ($moduleName in $Name) { if ($modules.Name -notcontains $moduleName) { if ($ThrowIfNotFound) { throw ($localized.CannotResolveModuleNameError -f $ModuleType, $moduleName); } else { Write-Warning -Message ($localized.CannotResolveModuleNameError -f $ModuleType, $moduleName); } } } $modules = $modules | Where-Object { $_.Name -in $Name }; } return $modules; } #end process } #end function function Resolve-LabCustomBootStrap { <# .SYNOPSIS Resolves the media and node custom bootstrap, using the specified CustomBootstrapOrder #> [CmdletBinding()] [OutputType([System.String])] param ( ## Custom bootstrap order [Parameter(Mandatory, ValueFromPipeline)] [ValidateSet('ConfigurationFirst','ConfigurationOnly','Disabled','MediaFirst','MediaOnly')] [System.String] $CustomBootstrapOrder, ## Node/configuration custom bootstrap script [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $ConfigurationCustomBootStrap, ## Media custom bootstrap script [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String[]] $MediaCustomBootStrap ) begin { if ([System.String]::IsNullOrWhiteSpace($ConfigurationCustomBootStrap)) { $ConfigurationCustomBootStrap = ""; } ## Convert the string[] into a multi-line string if ($MediaCustomBootstrap) { $mediaBootstrap = [System.String]::Join("`r`n", $MediaCustomBootStrap); } else { $mediaBootstrap = ""; } } #end begin process { switch ($CustomBootstrapOrder) { 'ConfigurationFirst' { $bootStrap = "{0}`r`n{1}" -f $ConfigurationCustomBootStrap, $mediaBootstrap; } 'ConfigurationOnly' { $bootStrap = $ConfigurationCustomBootStrap; } 'MediaFirst' { $bootStrap = "{0}`r`n{1}" -f $mediaBootstrap, $ConfigurationCustomBootStrap; } 'MediaOnly' { $bootStrap = $mediaBootstrap; } Default { #Disabled } } #end switch return $bootStrap; } #end process } #end function function Resolve-LabEnvironmentName { <# .SYNOPSIS Resolves a name with an environment prefix and suffix. #> [CmdletBinding()] [OutputType([System.String])] param ( ## Switch Id/Name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## PowerShell DSC configuration document (.psd1) containing lab metadata. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { ## Add a prefix if defined if ($ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentPrefix) { $Name = '{1}{0}' -f $Name, $ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentPrefix; } if ($ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentSuffix) { $Name = '{0}{1}' -f $Name, $ConfigurationData.NonNodeData.($labDefaults.ModuleName).EnvironmentSuffix; } return $Name; } #end process } #end function function Resolve-LabImage { <# .SYNOPSIS Resolves a Lability image by its path. .DESCRIPTION When running Remove-LabVM there is not always a configuration document supplied. This causes issues removing a VMs VHD/X file. The ResolveLabImage function locates the image by its physical path. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Path ) process { $vhdParentPaths = Resolve-VhdHierarchy -VhdPath $Path; Write-Output (Get-LabImage | Where-Object ImagePath -in $vhdParentPaths); } #end process } #end function Resolve-LabImage function Resolve-LabMedia { <# .SYNOPSIS Resolves the specified media using the registered media and configuration data. .DESCRIPTION Resolves the specified lab media from the registered media, but permitting the defaults to be overridden by configuration data. This also permits specifying of media within Configuration Data and not having to be registered on the lab host. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')] param ( ## Media ID or alias [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Id, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { ## Avoid any $media variable scoping issues $media = $null ## If we have configuration data specific instance, return that if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $customMedia = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Media.Where({ $_.Id -eq $Id -or $_.Alias -eq $Id }) if ($customMedia) { $newLabMediaParams = @{ } foreach ($key in $customMedia.Keys) { $newLabMediaParams[$key] = $customMedia.$key } $media = New-LabMedia @newLabMediaParams } } ## If we have custom media, return that if (-not $media) { $media = Get-ConfigurationData -Configuration CustomMedia $media = $media | Where-Object { $_.Id -eq $Id -or $_.Alias -eq $Id } } ## If we still don't have a media image, return the built-in object if (-not $media) { $media = Get-LabMedia -Id $Id } ## We don't have any defined, custom or built-in media if (-not $media) { throw ($localized.CannotLocateMediaError -f $Id) } return $media } #end process } #end function function Resolve-LabModule { <# .SYNOPSIS Returns the Node\DSCResource or Node\Module definitions from the NonNodeData\Lability\DSCResource or NonNodeData\Lability\Module node. .DESCRIPTION Resolves lab modules/DSC resources names defined at the Node\DSCResource or Node\Module node, and returns a collection of hashtables where the names match the definition in the NonNodeData\Lability\DSCResource or \NonNodeData\Lability\Module nodes. If resources are defined at the \NonNodeData\Lability\DSCResource or NonNodeData\Lability\Module nodes, but there are no VM references, then all the associated resources are returned. .NOTES If no NonNodeData\Lability\DscResource collection is defined, all locally installed DSC resource modules are returned. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Module type to enumerate [Parameter(Mandatory)] [ValidateSet('Module','DscResource')] [System.String] $ModuleType ) process { $resolveNodePropertyValueParams = @{ NodeName = $NodeName; ConfigurationData = $ConfigurationData; } $nodeProperties = Resolve-NodePropertyValue @resolveNodePropertyValueParams; $resolveModuleParams = @{ ConfigurationData = $ConfigurationData; ModuleType = $ModuleType; } if ($nodeProperties.ContainsKey($ModuleType)) { $resolveModuleParams['Name'] = $nodeProperties[$ModuleType]; } $modules = Resolve-LabConfigurationModule @resolveModuleParams; ## Only copy all existing DSC resources if there is no node or configuration DscResource ## defined. This allows suppressing local DSC resource module deploys if (($ModuleType -eq 'DscResource') -and (-not $nodeProperties.ContainsKey('DscResource')) -and ($null -eq $ConfigurationData.NonNodeData.($labDefaults.ModuleName).DscResource)) { <# There is no DSCResource = @() node defined either at the node or the configuration level. Therefore, we need to copy all the existing DSC resources on from the host by returning a load of FileSystem provider resources.. #> Write-Warning -Message ($localized.DscResourcesNotDefinedWarning); $modules = Get-DscResourceModule -Path "$env:ProgramFiles\WindowsPowershell\Modules" | ForEach-Object { ## Create a new hashtable Write-Output -InputObject @{ Name = $_.ModuleName; Version = $_.ModuleVersion; Provider = 'FileSystem'; Path = $_.Path; } }; } return $modules; } } #end function Resolve-LabModule function Resolve-LabResource { <# .SYNOPSIS Resolves a lab resource by its ID #> param ( ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Lab resource ID [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourceId ) process { $resource = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource | Where-Object Id -eq $ResourceId; if ($resource) { return $resource; } else { throw ($localized.CannotResolveResourceIdError -f $resourceId); } } #end process } #end function function Resolve-LabSwitch { <# .SYNOPSIS Resolves the specified switch using configuration data. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Switch Id/Name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## PowerShell DSC configuration document (.psd1) containing lab metadata. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $networkSwitch = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Network.Where({ $_.Name -eq $Name }); if ($hostDefaults.DisableSwitchEnvironmentName -eq $false) { ## Prefix/suffix switch name $Name = Resolve-LabEnvironmentName -Name $Name -ConfigurationData $ConfigurationData; } if ($networkSwitch) { $networkHashtable = @{}; foreach ($key in $networkSwitch.Keys) { [ref] $null = $networkHashtable.Add($key, $networkSwitch.$Key); } $networkSwitch = New-LabSwitch @networkHashtable; } elseif (Hyper-V\Get-VMSwitch -Name $Name -ErrorAction SilentlyContinue) { $existingSwitch = Hyper-V\Get-VMSwitch -Name $Name; ## Ensure that we only resolve a single switch as Hyper-V will allow (#326) if ($existingSwitch -is [System.Array]) { throw ($localized.AmbiguousSwitchNameError -f $Name); } Write-Warning -Message ($localized.UsingExistingSwitchWarning -f $Name); ## Use an existing virtual switch with a matching name if one exists $networkSwitch = @{ Name = $existingSwitch.Name; Type = $existingSwitch.SwitchType; AllowManagementOS = $existingSwitch.AllowManagementOS; IsExisting = $true; } if (($existingSwitch.NetAdapterInterfaceDescription).Name) { $existingSwitchAdapter = Get-NetAdapter -InterfaceDescription $existingSwitch.NetAdapterInterfaceDescription; $networkSwitch['NetAdapterName'] = $existingSwitchAdapter.Name; } } else { ## Create an internal switch $networkSwitch = @{ Name = $Name; Type = 'Internal'; } } return $networkSwitch; } #end process } #end function function Resolve-LabVMGenerationDiskPath { <# .SYNOPSIS Resolves the specified VM name's target VHD/X path. #> [CmdletBinding()] param ( ## VM/node name. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData; $resolveLabVMDiskPathParams = @{ Name = $Name; Generation = $image.Generation; EnvironmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; } $vhdPath = Resolve-LabVMDiskPath @resolveLabVMDiskPathParams return $vhdPath; } #end process } #end function function Resolve-LabVMDiskPath { <# .SYNOPSIS Resolves the specified VM name to it's target VHDX path. #> param ( ## VM/node name. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter()] [ValidateSet('VHD','VHDX')] [System.String] $Generation = 'VHDX', ## Configuration environment name [Parameter()] [AllowNull()] [System.String] $EnvironmentName, ## Return the parent/folder path [Parameter()] [System.Management.Automation.SwitchParameter] $Parent ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $differencingVhdPath = $hostDefaults.DifferencingVhdPath; if ((-not $hostDefaults.DisableVhdEnvironmentName) -and (-not [System.String]::IsNullOrEmpty($EnvironmentName))) { $differencingVhdPath = Join-Path -Path $differencingVhdPath -ChildPath $EnvironmentName; } if ($Parent) { $vhdPath = $differencingVhdPath; } else { $vhdName = '{0}.{1}' -f $Name, $Generation.ToLower(); $vhdPath = Join-Path -Path $differencingVhdPath -ChildPath $vhdName; } return $vhdPath; } #end process } #end function function Resolve-LabVMImage { <# .SYNOPSIS Resolves a virtual machine's Lability image. #> [CmdletBinding()] param ( ## VM name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $Name ) process { Hyper-V\Get-VM -Name $Name | Hyper-V\Get-VMHardDiskDrive | Select-Object -First 1 -ExpandProperty $Path | Resolve-LabImage; } #'end process } #end function Resolve-LabVMImage function Resolve-NodePropertyValue { <# .SYNOPSIS Resolves a node's properites. .DESCRIPTION Resolves a lab virtual machine properties from the lab defaults, Node\* node and Node\NodeName node. Properties defined on the wildcard node override the lab defaults. Properties defined at the node override the wildcard node settings. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Do not enumerate the AllNodes.'*' node [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoEnumerateWildcardNode ) process { $node = @{ }; if (-not $NoEnumerateWildcardNode) { ## Retrieve the AllNodes.* properties $ConfigurationData.AllNodes.Where({ $_.NodeName -eq '*' }) | ForEach-Object { foreach ($key in $_.Keys) { $node[$key] = $_.$key; } } } ## Retrieve the AllNodes.$NodeName properties $ConfigurationData.AllNodes.Where({ $_.NodeName -eq $NodeName }) | ForEach-Object { foreach ($key in $_.Keys) { $node[$key] = $_.$key; } } ## Check VM defaults $labDefaultProperties = Get-ConfigurationData -Configuration VM; $properties = Get-Member -InputObject $labDefaultProperties -MemberType NoteProperty; foreach ($propertyName in $properties.Name) { ## Int32 values of 0 get coerced into $false! if (($node.$propertyName -isnot [System.Int32]) -and (-not $node.ContainsKey($propertyName))) { $node[$propertyName] = $labDefaultProperties.$propertyName; } } $resolveLabEnvironmentNameParams = @{ Name = $NodeName; ConfigurationData = $ConfigurationData; } ## Set the node's friendly/display name with any prefix/suffix $node['NodeDisplayName'] = Resolve-LabEnvironmentName @resolveLabEnvironmentNameParams; ## Use the prefixed/suffixed NetBIOSName is specified if ($node.UseNetBIOSName -eq $true) { $node['NodeDisplayName'] = $node['NodeDisplayName'].Split('.')[0]; } $moduleName = $labDefaults.ModuleName; ## Rename/overwrite existing parameter values where $moduleName-specific parameters exist foreach ($key in @($node.Keys)) { if ($key.StartsWith("$($moduleName)_")) { $node[($key.Replace("$($moduleName)_",''))] = $node.$key; $node.Remove($key); } } return $node; } #end process } #end function ResolveLabVMProperties function Resolve-PathEx { <# .SYNOPSIS Resolves the wildcard characters in a path, and displays the path contents, ignoring non-existent paths. .DESCRIPTION The Resolve-Path cmdlet interprets the wildcard characters in a path and displays the items and containers at the location specified by the path, such as the files and folders or registry keys and subkeys. #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory)] [System.String] $Path ) process { try { $expandedPath = [System.Environment]::ExpandEnvironmentVariables($Path); $resolvedPath = Resolve-Path -Path $expandedPath -ErrorAction Stop; $Path = $resolvedPath.ProviderPath; } catch [System.Management.Automation.ItemNotFoundException] { $Path = [System.Environment]::ExpandEnvironmentVariables($_.TargetObject); $Error.Remove($Error[-1]); } return $Path; } #end process } #end function function Resolve-ProgramFilesFolder { <# .SYNOPSIS Resolves known localized %ProgramFiles% directories. .LINK https://en.wikipedia.org/wiki/Program_Files #> [CmdletBinding(DefaultParameterSetName = 'Path')] [OutputType([System.IO.DirectoryInfo])] param ( ## Root path to check [Parameter(Mandatory, ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Drive letter [Parameter(Mandatory, ParameterSetName = 'Drive')] [ValidateLength(1,1)] [System.String] $Drive ) begin { if ($PSCmdlet.ParameterSetName -eq 'Drive') { $Path = '{0}:\' -f $Drive; } } process { $knownFolderNames = @( "Program Files", "Programmes", "Archivos de programa", "Programme", "Programf�jlok", "Programmi", "Programmer", "Program", "Programfiler", "Arquivos de Programas", "Programas" "???e?a ?fa?�????" ) Get-ChildItem -Path $Path -Directory | Where-Object Name -in $knownFolderNames | Select-Object -First 1; } #end process } #end function function Resolve-PSGalleryModuleUri { <# .SYNOPSIS Returns the direct download Uri for a PowerShell module hosted on the PowerShell Gallery. #> [CmdletBinding()] [OutputType([System.String])] param ( ## PowerShell DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## The minimum version of the DSC module required [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the DSC module required [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## Direct download Uri [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Uri, ## Catch all, for splatting $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { if ($PSBoundParameters.ContainsKey('Uri')) { $psRepositoryUri = $Uri; } else { $psRepositoryUri = (Get-ConfigurationData -Configuration Host).RepositoryUri; } if ($PSBoundParameters.ContainsKey('RequiredVersion')) { ## Download the specific version return ('{0}/{1}/{2}' -f $psRepositoryUri, $Name, $RequiredVersion); } else { ## Download the latest version return ('{0}/{1}' -f $psRepositoryUri, $Name); } } #end process } #end function Resolve-PSGalleryModuleUri <# The MIT License (MIT) Copyright (c) 2015 Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #> function Resolve-VhdHierarchy { <# .SYNOPSIS Returns VM VHDs, including snapshots and differencing disks #> [CmdletBinding()] param ( ## Path to current virtual machine VHD/X file [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $VhdPath ) process { $vmVhdPath = Hyper-V\Get-VHD -Path $VhdPath; Write-Output -InputObject $vmVhdPath.Path; while (-not [System.String]::IsNullOrEmpty($vmVhdPath.ParentPath)) { $vmVhdPath.ParentPath; $vmVhdPath = (Hyper-V\Get-VHD -Path $vmVhdPath.ParentPath); } } #end process } #end function Resolve-VhdHierarchy function Set-ConfigurationData { <# .SYNOPSIS Saves lab configuration data. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.PSCustomObject])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')] param ( [Parameter(Mandatory)] [ValidateSet('Host','VM','Media','CustomMedia')] [System.String] $Configuration, [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $InputObject ) process { $configurationPath = Resolve-ConfigurationDataPath -Configuration $Configuration; [ref] $null = New-Directory -Path (Split-Path -Path $configurationPath -Parent) -Verbose:$false; Set-Content -Path $configurationPath -Value (ConvertTo-Json -InputObject $InputObject -Depth 5) -Force -Confirm:$false; } #end process } #end function Set-ConfigurationData function Set-DiskImageBootVolume { <# .SYNOPSIS Sets the boot volume of a mounted disk image. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk ## Disk image partition scheme [Parameter(Mandatory)] [ValidateSet('MBR','GPT')] [System.String] $PartitionStyle ) process { switch ($PartitionStyle) { 'MBR' { Set-DiskImageBootVolumeMbr -Vhd $Vhd; break; } 'GPT' { Set-DiskImageBootVolumeGpt -Vhd $Vhd; break; } } #end switch } #end process } #end function function Set-DiskImageBootVolumeGpt { <# .SYNOPSIS Configure/repair MBR boot volume #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk ) process { $bcdBootExe = 'bcdboot.exe'; $imageName = [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path); $systemPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'System'; $osPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'Basic'; Write-Verbose -Message ($localized.RepairingBootVolume -f $osPartitionDriveLetter); $bcdBootArgs = @( ('{0}:\Windows' -f $osPartitionDriveLetter), # Path to source Windows boot files ('/s {0}:\' -f $systemPartitionDriveLetter), # Specifies the volume letter of the drive to create the \BOOT folder on. '/v' # Enabled verbose logging. '/f UEFI' # Specifies the firmware type of the target system partition ) Invoke-Executable -Path $bcdBootExe -Arguments $bcdBootArgs -LogName ('{0}-BootEdit.log' -f $imageName); ## Clean up and remove drive access path Remove-PSDrive -Name $osPartitionDriveLetter -PSProvider FileSystem -ErrorAction Ignore; [ref] $null = Get-PSDrive; } #end process } #end function function Set-DiskImageBootVolumeMbr { <# .SYNOPSIS Configure/repair MBR boot volume #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD(X) Operating System disk image [Parameter(Mandatory)] [System.Object] $Vhd # Microsoft.Vhd.PowerShell.VirtualHardDisk ) process { $bcdBootExe = 'bcdboot.exe'; $bcdEditExe = 'bcdedit.exe'; $imageName = [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path); $osPartitionDriveLetter = Get-DiskImageDriveLetter -DiskImage $Vhd -PartitionType 'IFS'; Write-Verbose -Message ($localized.RepairingBootVolume -f $osPartitionDriveLetter); $bcdBootArgs = @( ('{0}:\Windows' -f $osPartitionDriveLetter), # Path to source Windows boot files ('/s {0}:\' -f $osPartitionDriveLetter), # Volume to create the \BOOT folder on. '/v' # Enable verbose logging. '/f BIOS' # Firmware type of the target system partition ) Invoke-Executable -Path $bcdBootExe -Arguments $bcdBootArgs -LogName ('{0}-BootEdit.log' -f $imageName); $bootmgrDeviceArgs = @( ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter), '/set {bootmgr} device locate' ); Invoke-Executable -Path $bcdEditExe -Arguments $bootmgrDeviceArgs -LogName ('{0}-BootmgrDevice.log' -f $imageName); $defaultDeviceArgs = @( ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter), '/set {default} device locate' ); Invoke-Executable -Path $bcdEditExe -Arguments $defaultDeviceArgs -LogName ('{0}-DefaultDevice.log' -f $imageName); $defaultOsDeviceArgs = @( ('/store {0}:\boot\bcd' -f $osPartitionDriveLetter), '/set {default} osdevice locate' ); Invoke-Executable -Path $bcdEditExe -Arguments $defaultOsDeviceArgs -LogName ('{0}-DefaultOsDevice.log' -f $imageName); } #end process } #end function function Set-LabBootStrap { <# .SYNOPSIS Writes the lab BootStrap.ps1 file to the target directory. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Destination Bootstrap directory path. [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Path, ## Custom bootstrap script [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $CustomBootStrap, ## Is a CoreCLR VM. The PowerShell switches are different in the CoreCLR, i.e. Nano Server [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $CoreCLR, ## Custom shell [Parameter(ValueFromPipelineByPropertyName)] [System.String] $DefaultShell, ## WSMan maximum envelope size [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $MaxEnvelopeSizeKb = 1024 ) process { $newBootStrapParams = @{ CoreCLR = $CoreCLR; MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb; } if (-not [System.String]::IsNullOrEmpty($DefaultShell)) { $newBootStrapParams['DefaultShell'] = $DefaultShell; } $bootStrap = New-LabBootStrap @newBootStrapParams; if ($CustomBootStrap) { $bootStrap = $bootStrap.Replace('<#CustomBootStrapInjectionPoint#>', $CustomBootStrap); } [ref] $null = New-Directory -Path $Path -Confirm:$false; $bootStrapPath = Join-Path -Path $Path -ChildPath 'BootStrap.ps1'; Set-Content -Path $bootStrapPath -Value $bootStrap -Encoding UTF8 -Force -Confirm:$false; } #end process } #end function function Set-LabDscResource { <# .SYNOPSIS Runs the ResourceName DSC resource ensuring it's in the desired state. .DESCRIPTION The Set-LabDscResource cmdlet invokes the target $ResourceName\Set-TargetResource function using the supplied $Parameters hastable. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Name of the DSC resource to invoke [Parameter(Mandatory, ValueFromPipeline)] [System.String] $ResourceName, ## The DSC resource's Set-TargetResource parameter hashtable [Parameter(Mandatory)] [System.Collections.Hashtable] $Parameters ) process { $setTargetResourceCommand = 'Set-{0}TargetResource' -f $ResourceName; Write-Debug ($localized.InvokingCommand -f $setTargetResourceCommand); $Parameters.Keys | ForEach-Object { Write-Debug -Message ($localized.CommandParameter -f $_, $Parameters.$_); } try { $setDscResourceResult = & $setTargetResourceCommand @Parameters; } catch { Write-Warning -Message ($localized.DscResourceFailedError -f $setTargetResourceCommand, $_); } return $setDscResourceResult; } #end process } #end function function Set-LabSetupCompleteCmd { <# .SYNOPSIS Creates a lab BootStrap script block. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.Management.Automation.ScriptBlock])] param ( ## Destination SetupComplete.cmd directory path. [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Is a CoreCLR VM. The bootstrapping via Powershell.exe in the CoreCLR doesn't work in its current format, i.e. with Nano Server [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $CoreCLR ) process { [ref] $null = New-Directory -Path $Path -Confirm:$false; $setupCompletePath = Join-Path -Path $Path -ChildPath 'SetupComplete.cmd'; if ($CoreCLR) { Write-Verbose -Message $localized.UsingCoreCLRSetupComplete; $setupCompleteCmd = @" schtasks /create /tn "BootStrap" /tr "cmd.exe /c """Powershell.exe -Command %SYSTEMDRIVE%\BootStrap\BootStrap.ps1""" > %SYSTEMDRIVE%\BootStrap\BootStrap.log" /sc "Once" /sd "01/01/2099" /st "00:00" /ru "System" schtasks /run /tn "BootStrap" "@ } else { Write-Verbose -Message $localized.UsingDefaultSetupComplete; $setupCompleteCmd = 'Powershell.exe -NoProfile -ExecutionPolicy Bypass -NonInteractive -File "%SYSTEMDRIVE%\BootStrap\BootStrap.ps1"'; } Set-Content -Path $setupCompletePath -Value $setupCompleteCmd -Encoding Ascii -Force -Confirm:$false; } #end process } #end function function Set-LabSwitch { <# .SYNOPSIS Sets/invokes a virtual network switch configuration. .DESCRIPTION Sets/invokes a virtual network switch configuration using the xVMSwitch DSC resource. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Switch Id/Name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## PowerShell DSC configuration document (.psd1) containing lab metadata. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $networkSwitch = Resolve-LabSwitch @PSBoundParameters; if (($null -eq $networkSwitch.IsExisting) -or ($networkSwitch.IsExisting -eq $false)) { Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch; [ref] $null = Invoke-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch; } } #end process } #end function function Set-LabVirtualMachine { <# .SYNOPSIS Invokes the current configuration a virtual machine. .DESCRIPTION Invokes/sets a virtual machine configuration using the xVMHyperV DSC resource. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String[]] $SwitchName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Media, [Parameter(Mandatory)] [System.UInt64] $StartupMemory, [Parameter(Mandatory)] [System.UInt64] $MinimumMemory, [Parameter(Mandatory)] [System.UInt64] $MaximumMemory, [Parameter(Mandatory)] [System.Int32] $ProcessorCount, [Parameter()] [AllowNull()] [System.String[]] $MACAddress, [Parameter()] [System.Boolean] $SecureBoot, [Parameter()] [System.Boolean] $GuestIntegrationServices, [Parameter()] [System.Boolean] $AutomaticCheckpoints, ## xVMProcessor options [Parameter()] [System.Collections.Hashtable] $ProcessorOption, ## xVMHardDiskDrive options [Parameter()] [System.Collections.Hashtable[]] $HardDiskDrive, ## xVMDvdDrive options [Parameter()] [System.Collections.Hashtable] $DvdDrive, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { ## Store additional options for when we have a VM $vmProcessorParams = $PSBoundParameters['ProcessorOption']; $vmDvdDriveParams = $PSBoundParameters['DvdDrive']; $vmHardDiskDriveParams = $PSBoundParameters['HardDiskDrive']; [ref] $null = $PSBoundParameters.Remove('ProcessorOption'); [ref] $null = $PSBoundParameters.Remove('DvdDrive'); [ref] $null = $PSBoundParameters.Remove('HardDiskDrive'); ## Resolve the xVMHyperV resource parameters $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters; Write-Verbose -Message ($localized.CreatingVMGeneration -f $vmHyperVParams.Generation); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM; Invoke-LabDscResource -ResourceName VM -Parameters $vmHyperVParams; if ($null -ne $vmProcessorParams) { ## Ensure we have the node's name $vmProcessorParams['VMName'] = $Name; Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM processor', $Name); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMProcessor -Prefix VMProcessor; Invoke-LabDscResource -ResourceName VMProcessor -Parameters $vmProcessorParams; } if ($null -ne $vmDvdDriveParams) { ## Ensure we have the node's name $vmDvdDriveParams['VMName'] = $Name; Write-Verbose -Message ($localized.SettingVMConfiguration -f 'VM DVD drive', $Name); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMDvdDrive -Prefix VMDvdDrive; Invoke-LabDscResource -ResourceName VMDvdDrive -Parameters $vmDvdDriveParams; } if ($null -ne $vmHardDiskDriveParams) { $setLabVirtualMachineHardDiskDriveParams = @{ NodeName = $Name; VMGeneration = $vmHyperVParams.Generation; HardDiskDrive = $vmHardDiskDriveParams; } Set-LabVirtualMachineHardDiskDrive @setLabVirtualMachineHardDiskDriveParams; } } #end process } #end function function Set-LabVirtualMachineHardDiskDrive { <# .SYNOPSIS Sets a virtual machine's additional hard disk drive(s). .DESCRIPTION Adds one or more additional hard disks to a VM. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.String] $NodeName, ## Collection of additional hard disk drive configurations [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $HardDiskDrive, ## Virtual machine generation [Parameter()] [System.Int32] $VMGeneration ) process { $vmHardDiskPath = (Get-ConfigurationData -Configuration Host).DifferencingVhdPath; for ($i = 0; $i -lt $HardDiskDrive.Count; $i++) { $diskDrive = $HardDiskDrive[$i]; $controllerLocation = $i + 1; Assert-VirtualMachineHardDiskDriveParameter @diskDrive -VMGeneration $VMGeneration; if ($diskDrive.ContainsKey('VhdPath')) { $vhdPath = $diskDrive.VhdPath; } else { ## Create the VHD file $vhdName = '{0}-{1}' -f $NodeName, $controllerLocation; $vhdParams = @{ Name = $vhdName; Path = $vmHardDiskPath; MaximumSizeBytes = $diskDrive.MaximumSizeBytes; Generation = $diskDrive.Generation; Ensure = 'Present'; } if ($null -ne $diskDrive.Type) { $vhdParams['Type'] = $diskDrive.Type; } $vhdFilename = '{0}.{1}' -f $vhdName, $diskDrive.Generation.ToLower(); $vhdPath = Join-Path -Path $vmHardDiskPath -ChildPath $vhdFilename; Write-Verbose -Message ($localized.CreatingAdditionalVhdFile -f $vhdPath); Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVhd -Prefix Vhd; Invoke-LabDscResource -ResourceName Vhd -Parameters $vhdParams; } ## Now add the VHD Write-Verbose -Message ($localized.AddingAdditionalVhdFile -f $vhdPath, "0:$controllerLocation"); $vmHardDiskDriveParams = @{ VMName = $NodeName; ControllerLocation = $controllerLocation; Path = $VhdPath; Ensure = 'Present'; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHardDiskDrive -Prefix HardDiskDrive; Invoke-LabDscResource -ResourceName HardDiskDrive -Parameters $vmHardDiskDriveParams; } #end for } #end process } #end function function Set-LabVMDisk { <# .SYNOPSIS Sets a lab VM disk file (VHDX) configuration. .DESCRIPTION Configures a VM disk configuration using the xVHD DSC resource. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( ## VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData -ErrorAction Stop; } else { $image = Get-LabImage -Id $Media -ErrorAction Stop; } $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; $vhd = @{ Name = $Name; Path = Resolve-LabVMDiskPath -Name $Name -EnvironmentName $environmentName -Parent; ParentPath = $image.ImagePath; Generation = $image.Generation; Type = 'Differencing'; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD; [ref] $null = Invoke-LabDscResource -ResourceName VHD -Parameters $vhd; } #end process } #end function function Set-LabVMDiskFile { <# .SYNOPSIS Copies Lability files to a node's VHD(X) file. .DESCRIPTION Copies the Lability bootstrap file, SetupComplete.cmd, unattend.xml, mof files, certificates and PowerShell/DSC resource modules to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Local administrator password of the VM [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, ## Lab VM/Node DSC .mof and .meta.mof configuration files [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Path, ## Custom bootstrap script [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $CustomBootstrap, ## CoreCLR [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $CoreCLR, ## Custom/replacement Shell [Parameter(ValueFromPipelineByPropertyName)] [System.String] $DefaultShell, ## WSMan maximum envelope size [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $MaxEnvelopeSizeKb = 1024, ## Media-defined product key [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ProductKey, ## Credentials to access the a private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ) process { ## Temporarily disable Windows Explorer popup disk initialization and format notifications ## http://blogs.technet.com/b/heyscriptingguy/archive/2013/05/29/use-powershell-to-initialize-raw-disks-and-partition-and-format-volumes.aspx Stop-ShellHWDetectionService; $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop; $resolveLabVMGenerationDiskPathParams = @{ Name = $node.NodeDisplayName; Media = $node.Media; ConfigurationData = $ConfigurationData; } $vhdPath = Resolve-LabVMGenerationDiskPath @resolveLabVMGenerationDiskPathParams; ## Disable BitLocker fixed drive write protection (if enabled) Disable-BitLockerFDV; Write-Verbose -Message ($localized.MountingDiskImage -f $VhdPath); $vhd = Hyper-V\Mount-Vhd -Path $vhdPath -Passthru -Confirm:$false; $driveList = Get-PSDrive -PSProvider FileSystem $vhdDriveLetter = Storage\Get-Partition -DiskNumber $vhd.DiskNumber | Where-Object DriveLetter | Select-Object -Last 1 -ExpandProperty DriveLetter; ## If no drive letter is automagically assigned, assign one. if ($null -eq $vhdDriveLetter) { $driveListNames = $driveList.Name foreach ($driveLetter in [char[]](67..90)) { if ($driveListNames -notcontains $driveLetter) { $vhdDriveLetter = $driveLetter break } } Get-Partition | Where-Object { ($_.DiskNumber -eq $vhd.DiskNumber) -and ($_.Type -eq 'Basic') } | Set-Partition -NewDriveLetter $vhdDriveLetter } Start-ShellHWDetectionService; try { Set-LabVMDiskFileResource @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; Set-LabVMDiskFileBootstrap @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; Set-LabVMDiskFileUnattendXml @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; Set-LabVMDiskFileMof @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; Set-LabVMDiskFileCertificate @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; Set-LabVMDiskFileModule @PSBoundParameters -VhdDriveLetter $vhdDriveLetter; # FeedCredential passed in bound parameters } catch { ## Bubble up the error to the caller throw $_; } finally { ## Ensure the VHD is dismounted (#185) Write-Verbose -Message ($localized.DismountingDiskImage -f $VhdPath); $null = Hyper-V\Dismount-Vhd -Path $VhdPath -Confirm:$false; ## Enable BitLocker (if required) Assert-BitLockerFDV; } } #end process } #end function Set-LabVMDiskFile function Set-LabVMDiskFileBootstrap { <# .SYNOPSIS Copies a the Lability bootstrap file to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Custom bootstrap script [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $CustomBootstrap, ## CoreCLR [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $CoreCLR, ## Custom/replacement shell [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $DefaultShell, ## WSMan maximum envelope size [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] $MaxEnvelopeSizeKb = 1024, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter; Write-Verbose -Message ($localized.AddingBootStrapFile -f $bootStrapPath); $setBootStrapParams = @{ Path = $bootStrapPath; CoreCLR = $CoreCLR; MaxEnvelopeSizeKb = $MaxEnvelopeSizeKb; } if ($CustomBootStrap) { $setBootStrapParams['CustomBootStrap'] = $CustomBootStrap; } if ($PSBoundParameters.ContainsKey('DefaultShell')) { Write-Verbose -Message ($localized.SettingCustomShell -f $DefaultShell); $setBootStrapParams['DefaultShell'] = $DefaultShell; } Set-LabBootStrap @setBootStrapParams; $setupCompleteCmdPath = '{0}:\Windows\Setup\Scripts' -f $vhdDriveLetter; Write-Verbose -Message ($localized.AddingSetupCompleteCmdFile -f $setupCompleteCmdPath); Set-LabSetupCompleteCmd -Path $setupCompleteCmdPath -CoreCLR:$CoreCLR; } #end process } #end function function Set-LabVMDiskFileCertificate { <# .SYNOPSIS Copies a node's certificate(s) to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop; $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter; if (-not [System.String]::IsNullOrWhitespace($node.ClientCertificatePath)) { [ref] $null = New-Item -Path $bootStrapPath -ItemType File -Name 'LabClient.pfx' -Force; $destinationCertificatePath = Join-Path -Path $bootStrapPath -ChildPath 'LabClient.pfx'; $expandedClientCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.ClientCertificatePath); Write-Verbose -Message ($localized.AddingCertificate -f 'Client', $destinationCertificatePath); Copy-Item -Path $expandedClientCertificatePath -Destination $destinationCertificatePath -Force -Confirm:$false; } if (-not [System.String]::IsNullOrWhitespace($node.RootCertificatePath)) { [ref] $null = New-Item -Path $bootStrapPath -ItemType File -Name 'LabRoot.cer' -Force; $destinationCertificatePath = Join-Path -Path $bootStrapPath -ChildPath 'LabRoot.cer'; $expandedRootCertificatePath = [System.Environment]::ExpandEnvironmentVariables($node.RootCertificatePath); Write-Verbose -Message ($localized.AddingCertificate -f 'Root', $destinationCertificatePath); Copy-Item -Path $expandedRootCertificatePath -Destination $destinationCertificatePath -Force -Confirm:$false; } } #end process } #end function function Set-LabVMDiskFileModule { <# .SYNOPSIS Copies a node's PowerShell and DSC resource modules to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Node DSC .mof and .meta.mof configuration file path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Path, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments, ## Credentials to access the a private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ) process { ## Resolve the localized %ProgramFiles% directory $programFilesPath = '{0}\WindowsPowershell\Modules' -f (Resolve-ProgramFilesFolder -Drive $VhdDriveLetter).FullName ## Add the DSC resource modules $resolveLabModuleParams = @{ ConfigurationData = $ConfigurationData; NodeName = $NodeName; ModuleType = 'DscResource'; } $setLabVMDiskDscModuleParams = @{ Module = Resolve-LabModule @resolveLabModuleParams; DestinationPath = $programFilesPath; } if ($null -ne $setLabVMDiskDscModuleParams['Module']) { ## Check that we have cache copies of all modules used in the .mof file. $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $NodeName); if (Test-Path -Path $mofPath -PathType Leaf) { $testLabMofModuleParams = @{ Module = $setLabVMDiskDscModuleParams.Module; MofModule = Get-LabMofModule -Path $mofPath; } if ($null -ne $testLabMofModuleParams['MofModule']) { ## TODO: Add automatic download of missing resources from the PSGallery. [ref] $null = Test-LabMofModule @testLabMofModuleParams; } } Write-Verbose -Message ($localized.AddingDSCResourceModules -f $programFilesPath); Set-LabVMDiskModule @setLabVMDiskDscModuleParams -FeedCredential $FeedCredential; } ## Add the PowerShell resource modules $resolveLabModuleParams =@{ ConfigurationData = $ConfigurationData; NodeName = $NodeName; ModuleType = 'Module'; } $setLabVMDiskPowerShellModuleParams = @{ Module = Resolve-LabModule @resolveLabModuleParams; DestinationPath = $programFilesPath; } if ($null -ne $setLabVMDiskPowerShellModuleParams['Module']) { Write-Verbose -Message ($localized.AddingPowerShellModules -f $programFilesPath); Set-LabVMDiskModule @setLabVMDiskPowerShellModuleParams; } } #end process } #end function function Set-LabVMDiskFileMof { <# .SYNOPSIS Copies a node's mof files to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab VM/Node DSC .mof and .meta.mof configuration files [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Path, ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $bootStrapPath = '{0}:\BootStrap' -f $VhdDriveLetter; $mofPath = Join-Path -Path $Path -ChildPath ('{0}.mof' -f $NodeName); if (-not (Test-Path -Path $mofPath)) { Write-Warning -Message ($localized.CannotLocateMofFileError -f $mofPath); } else { $destinationMofPath = Join-Path -Path $bootStrapPath -ChildPath 'localhost.mof'; Write-Verbose -Message ($localized.AddingDscConfiguration -f $destinationMofPath); Copy-Item -Path $mofPath -Destination $destinationMofPath -Force -ErrorAction Stop -Confirm:$false; } $metaMofPath = Join-Path -Path $Path -ChildPath ('{0}.meta.mof' -f $NodeName); if (Test-Path -Path $metaMofPath -PathType Leaf) { $destinationMetaMofPath = Join-Path -Path $bootStrapPath -ChildPath 'localhost.meta.mof'; Write-Verbose -Message ($localized.AddingDscConfiguration -f $destinationMetaMofPath); Copy-Item -Path $metaMofPath -Destination $destinationMetaMofPath -Force -Confirm:$false; } } #end process } #end function function Set-LabVMDiskFileResource { <# .SYNOPSIS Copies a node's defined resources to VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $hostDefaults = Get-ConfigurationData -Configuration Host; $resourceDestinationPath = '{0}:\{1}' -f $vhdDriveLetter, $hostDefaults.ResourceShareName; $expandLabResourceParams = @{ ConfigurationData = $ConfigurationData; Name = $NodeName; DestinationPath = $resourceDestinationPath; } Write-Verbose -Message ($localized.AddingVMResource -f 'VM'); Expand-LabResource @expandLabResourceParams; } #end process } #end function function Set-LabVMDiskFileUnattendXml { <# .SYNOPSIS Copies a node's unattent.xml to a VHD(X) file. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lab VM/Node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $NodeName, ## Lab DSC configuration data [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, ## Mounted VHD path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $VhdDriveLetter, ## Local administrator password of the VM [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, ## Media-defined product key [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ProductKey, ## Catch all to enable splatting @PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $node = Resolve-NodePropertyValue -NodeName $NodeName -ConfigurationData $ConfigurationData -ErrorAction Stop; ## Create Unattend.xml $newUnattendXmlParams = @{ ComputerName = $node.NodeName.Split('.')[0]; # Convert any FQDN to NetBIOS (#335) Credential = $Credential; InputLocale = $node.InputLocale; SystemLocale = $node.SystemLocale; UserLocale = $node.UserLocale; UILanguage = 'en-US'; Timezone = $node.Timezone; RegisteredOwner = $node.RegisteredOwner; RegisteredOrganization = $node.RegisteredOrganization; } Write-Verbose -Message $localized.SettingAdministratorPassword; ## Node defined Product Key takes preference over key defined in the media definition if ($node.CustomData.ProductKey) { $newUnattendXmlParams['ProductKey'] = $node.CustomData.ProductKey; } elseif ($PSBoundParameters.ContainsKey('ProductKey')) { $newUnattendXmlParams['ProductKey'] = $ProductKey; } ## TODO: We probably need to be localise the \Windows\ (%ProgramFiles% has been done) directory? $unattendXmlPath = '{0}:\Windows\System32\Sysprep\Unattend.xml' -f $VhdDriveLetter; Write-Verbose -Message ($localized.AddingUnattendXmlFile -f $unattendXmlPath); [ref] $null = Set-UnattendXml @newUnattendXmlParams -Path $unattendXmlPath; } #end process } #end function function Set-LabVMDiskModule { <# .SYNOPSIS Downloads (if required) PowerShell/DSC modules and expands them to the destination path specified. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ## Lability PowerShell modules/DSC resource hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $Module, ## The target VHDX modules path [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, ## Force a download of the module(s) even if they already exist in the cache. [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Force, ## Removes existing target module directory (if present) [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Clean, ## Credentials to access the a private feed [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ) process { ## Invokes the module download if not cached, and returns the source [ref] $null = Invoke-LabModuleCacheDownload -Module $Module -Force:$Force -FeedCredential $FeedCredential; ## Expand the modules into the VHDX file [ref] $null = Expand-LabModuleCache -Module $Module -DestinationPath $DestinationPath -Clean:$Clean; } #end process } #end function function Set-ResourceChecksum { <# .SYNOPSIS Creates a resource's checksum file. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms','')] param ( ## Path of file to create the checksum of [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Path ) process { $checksumPath = '{0}.checksum' -f $Path; ## As it can take a long time to calculate the checksum, write it out to disk for future reference Write-Verbose -Message ($localized.CalculatingResourceChecksum -f $checksumPath); $fileHash = Get-FileHash -Path $Path -Algorithm MD5 -ErrorAction Stop | Select-Object -ExpandProperty Hash; Write-Verbose -Message ($localized.WritingResourceChecksum -f $fileHash, $checksumPath); $fileHash | Set-Content -Path $checksumPath -Force; } #end process } #end function function Set-ResourceDownload { <# .SYNOPSIS Downloads a (web) resource and creates a MD5 checksum. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Checksum, [Parameter(ValueFromPipelineByPropertyName)] [System.UInt32] $BufferSize = 64KB, [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoChecksum, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $FeedCredential ##TODO: Support Headers and UserAgent ) begin { $parentDestinationPath = Split-Path -Path $DestinationPath -Parent; [ref] $null = New-Directory -Path $parentDestinationPath; } process { if (-not $PSBoundParameters.ContainsKey('BufferSize')) { $systemUri = New-Object -TypeName System.Uri -ArgumentList @($uri); if ($systemUri.IsFile) { $BufferSize = 1MB; } } Write-Verbose -Message ($localized.DownloadingResource -f $Uri, $DestinationPath); Invoke-WebClientDownload -DestinationPath $DestinationPath -Uri $Uri -BufferSize $BufferSize -Credential $FeedCredential; if ($NoChecksum -eq $false) { ## Create the checksum file for future reference [ref] $null = Set-ResourceChecksum -Path $DestinationPath; } } #end process } #end function function Set-UnattendXml { <# .SYNOPSIS Creates a Windows unattended installation file and saves to disk. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [OutputType([System.Xml.XmlDocument])] param ( # Filename/path to save the unattend file as [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Path, # Local Administrator Password [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, # Computer name [Parameter(ValueFromPipelineByPropertyName)] [System.String] $ComputerName, # Product Key [Parameter(ValueFromPipelineByPropertyName)] [ValidatePattern('^[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}$')] [System.String] $ProductKey, # Input Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $InputLocale = 'en-US', # System Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $SystemLocale = 'en-US', # User Locale [Parameter(ValueFromPipelineByPropertyName)] [System.String] $UserLocale = 'en-US', # UI Language [Parameter(ValueFromPipelineByPropertyName)] [System.String] $UILanguage = 'en-US', # Timezone [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Timezone, ##TODO: Validate timezones? # Registered Owner [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $RegisteredOwner = 'Virtual Engine', # Registered Organization [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $RegisteredOrganization = 'Virtual Engine', # TODO: Execute synchronous commands during OOBE pass as they only currently run during the Specialize pass ## Array of hashtables with Description, Order and Path keys [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable[]] $ExecuteCommand ) process { [ref] $null = $PSBoundParameters.Remove('Path'); $unattendXml = New-UnattendXml @PSBoundParameters; ## Ensure the parent Sysprep directory exists (#232) [ref] $null = New-Directory -Path (Split-Path -Path $Path -Parent); $resolvedPath = Resolve-PathEx -Path $Path; return $unattendXml.Save($resolvedPath); } #end process } #end function function Start-DscConfigurationCompilation { <# .SYNOPSIS Compiles an individual DSC configuration as a job. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidMultipleTypeAttributes','')] [OutputType([System.IO.FileInfo])] param ( ## DSC configuration file path, e.g. CONTROLLER.ps1 [Parameter(Mandatory)] [System.String] $Configuration, ## Parameters to pass into the DSC configuration [Parameter(Mandatory)] [System.Collections.Hashtable] $ConfigurationParameters, [Parameter()] [System.Management.Automation.SwitchParameter] $AsJob ) begin { ## Compilation scriptblock started as a job $compileConfigurationScriptBlock = { param ( ## DSC configuration path, e.g. CONTROLLER.ps1 [Parameter(Mandatory)] [System.String] $ConfigurationPath, ## Parameters to pass into the configuration [Parameter()] [System.Collections.Hashtable] [ref] $ConfigurationParameters, [Parameter()] [System.String] $VerbosePreference ) process { $ConfigurationPath = Resolve-Path -Path $ConfigurationPath; ## TODO Configuration name must currently match configuration file name $configurationName = (Get-Item -Path $ConfigurationPath).BaseName; ## Internal functions and localization strings are in a different PowerShell.exe ## process so we have to resort to hard coding (for the time being) :( Write-Verbose -Message ("Loading configuration '{0}'." -f $ConfigurationPath); $existingVerbosePreference = $VerbosePreference; ## Hide verbose output $VerbosePreference = 'SilentlyContinue'; ## Import the configuration . $ConfigurationPath; $VerbosePreference = $existingVerbosePreference; Write-Verbose -Message ("Compiling configuration '{0}'." -f $ConfigurationPath); $VerbosePreference = 'SilentlyContinue'; if ($ConfigurationParameters) { ## Call the configuration (complile it) with supplied parameters & $configurationName @ConfigurationParameters; } else { ## Just call the configuration (compile it) & $configurationName; } ## Restore verbose preference $VerbosePreference = $existingVerbosePreference; } #end process } #end $compileConfigurationScriptBlock } process { $configurationPath = Resolve-Path -Path $Configuration; $startJobParams = @{ Name = $configurationPath; ScriptBlock = $compileConfigurationScriptBlock; } if ($PSBoundParameters.ContainsKey('ConfigurationParameters')) { $startJobParams['ArgumentList'] = @($configurationPath, $ConfigurationParameters, $VerbosePreference); } $job = Start-Job @startJobParams; $activity = $localized.CompilingConfiguration; if (-not $AsJob) { while ($Job.HasMoreData -or $Job.State -eq 'Running') { $percentComplete++; if ($percentComplete -gt 100) { $percentComplete = 0; } Write-Progress -Id $job.Id -Activity $activity -Status $configurationPath -PercentComplete $percentComplete; Receive-Job -Job $Job Start-Sleep -Milliseconds 500; } Write-Progress -Id $job.Id -Activity $activity -Completed; $job | Receive-Job; } else { return $job; } } #end process } #end function function Start-ShellHWDetectionService { <# .SYNOPSIS Starts the ShellHWDetectionService - if present! #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ) process { if (Get-Service -Name 'ShellHWDetection' -ErrorAction SilentlyContinue) { Start-Service -Name 'ShellHWDetection' -ErrorAction Ignore -Confirm:$false; } } #end process } #end function Start-ShellHWDetectionService function Stop-ShellHWDetectionService { <# .SYNOPSIS Stops the ShellHWDetectionService - if present! #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')] param ( ) process { if (Get-Service -Name 'ShellHWDetection' -ErrorAction SilentlyContinue) { Stop-Service -Name 'ShellHWDetection' -Force -ErrorAction Ignore -Confirm:$false; } } #end process } #end function Stop-ShellHWDetectionService function Test-ComputerName { <# .SYNOPSIS Validates a computer name is valid. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline)] [ValidateLength(1, 15)] [System.String] $ComputerName ) process { $invalidMatch = '[~!@#\$%\^&\*\(\)=\+_\[\]{}\\\|;:.''",<>\/\?\s]'; return ($ComputerName -inotmatch $invalidMatch); } } #end function function Test-ConfigurationPath { <# .SYNOPSIS Tests the specified path for a computer's .mof file. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Lab vm/node name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## Defined .mof path [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowEmptyString()] [System.String] $Path ) process { $searchString = '{0}.*mof' -f $Name; $searchPath = Join-Path -Path $Path -ChildPath $searchString; Write-Debug -Message ("Searching configuration path '{0}'." -f $searchPath); if (Get-ChildItem -Path $searchPath -ErrorAction SilentlyContinue) { return $true; } return $false; } #end process } #end function Test-ConfigurationPath function Test-DscResourceModule { <# .SYNOPSIS Tests whether the specified PowerShell module directory is a DSC resource. .DESCRIPTION The Test-DscResourceModule determines whether the specified path is a PowerShell DSC resource module. This is used to only copy DSC resources to a VM's VHD(X) file - not ALL modules! .NOTES THIS METHOD IS DEPRECATED IN FAVOUR OF THE NEW MODULE CACHE FUNCTIONALITY #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Path, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $ModuleName ) process { ## This module contains a \DSCResources folder, but we don't want to enumerate this! if ($Path -notmatch "\\$($labDefaults.ModuleName)$") { Write-Debug -Message ('Testing for MOF-based DSC Resource ''{0}'' directory.' -f "$Path\DSCResources"); if (Test-Path -Path "$Path\DSCResources" -PathType Container) { ## We have a WMF 4.0/MOF DSC resource module Write-Debug -Message ('Found MOF-based DSC resource ''{0}''.' -f $Path); return $true; } Write-Debug -Message ('Testing for Class-based DSC resource definition ''{0}''.' -f "$Path\$ModuleName.psm1"); if (Test-Path -Path "$Path\$ModuleName.psm1") { $psm1Content = Get-Content -Path "$Path\$ModuleName.psm1"; ## If there's a .psm1 file, check if it's a class-based DSC resource if ($psm1Content -imatch '^(\s*)\[DscResource\(\)\](\s*)$') { ## File has a [DscResource()] declaration Write-Debug -Message ('Found Class-based DSC resource ''{0}''.' -f $Path); return $true; } } } #end if this module return $false; } #end process } #end function function Test-LabDscModule { <# .SYNOPSIS Tests whether the ResourceName of the specified ModuleName can be located on the system. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory)] [System.String] $ModuleName, [Parameter()] [System.String] $ResourceName, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $MinimumVersion ) process { if (Get-LabDscModule @PSBoundParameters -ErrorAction SilentlyContinue) { return $true; } else { return $false; } } #end process } #end function function Test-LabDscResource { <# .SYNOPSIS Tests the ResourceName DSC resource to determine if it's in the desired state. .DESCRIPTION The Test-LabDscResource cmdlet invokes the target $ResourceName\Test-TargetResource function using the supplied $Parameters hastable. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Name of the DSC resource to test [Parameter(Mandatory, ValueFromPipeline)] [System.String] $ResourceName, ## The DSC resource's Test-TargetResource parameter hashtable [Parameter(Mandatory)] [System.Collections.Hashtable] $Parameters ) process { $testTargetResourceCommand = 'Test-{0}TargetResource' -f $ResourceName; Write-Debug ($localized.InvokingCommand -f $testTargetResourceCommand); $Parameters.Keys | ForEach-Object { Write-Debug -Message ($localized.CommandParameter -f $_, $Parameters.$_); } try { $testDscResourceResult = & $testTargetResourceCommand @Parameters; } catch { ## No point writing warnings as failures will occur, i.e. "VHD not found" ## when a VM does not yet exist. Write-Warning -Message ($localized.DscResourceFailedError -f $testTargetResourceCommand, $_); $testDscResourceResult = $false; } if (-not $testDscResourceResult) { Write-Verbose -Message ($localized.TestFailed -f $testTargetResourceCommand); } return $testDscResourceResult; } #end process } #end function function TestModule { <# .SYNOPSIS Tests whether an exising PowerShell module meets the minimum or required version #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Name, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## Catch all to be able to pass parameters via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { $module = Get-LabModule -Name $Name; if ($module) { $testLabModuleVersionParams = @{ ModulePath = $module.Path; } if ($MinimumVersion) { $testLabModuleVersionParams['MinimumVersion'] = $MinimumVersion; } if ($RequiredVersion) { $testLabModuleVersionParams['RequiredVersion'] = $RequiredVersion; } return (Test-LabModuleVersion @testLabModuleVersionParams); } else { return $false; } } #end process } #end function function Test-LabModuleCache { <# .SYNOPSIS Tests whether the requested PowerShell module is cached. #> [CmdletBinding(DefaultParameterSetName = 'Name')] [OutputType([System.Boolean])] param ( ## PowerShell module/DSC resource module name [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Name, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## GitHub repository owner [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Owner, ## GitHub repository branch [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Branch, ## Source Filesystem module path [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateNotNullOrEmpty()] [System.String] $Path, ## Provider used to download the module [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameMinimum')] [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'NameRequired')] [ValidateSet('PSGallery','GitHub','AzDo', 'FileSystem')] [System.String] $Provider, ## Lability PowerShell module info hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Module')] [ValidateNotNullOrEmpty()] [System.Collections.Hashtable] $Module, ## Catch all to be able to pass parameter via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) begin { ## Remove -RemainingArguments to stop it being passed on. [ref] $null = $PSBoundParameters.Remove('RemainingArguments'); } process { $moduleFileInfo = Get-LabModuleCache @PSBoundParameters; return ($null -ne $moduleFileInfo); } #end process } #end function function Test-LabModuleVersion { <# .SYNOPSIS Tests whether an exising PowerShell module meets the minimum or required version #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Path to the module's manifest file [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ModulePath, ## The minimum version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'MinimumVersion')] [ValidateNotNullOrEmpty()] [ValidateNotNullOrEmpty()] [System.Version] $MinimumVersion, ## The exact version of the module required [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RequiredVersion')] [ValidateNotNullOrEmpty()] [System.Version] $RequiredVersion, ## Catch all to be able to pass parameters via $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) process { try { Write-Verbose -Message ($localized.QueryingModuleVersion -f [System.IO.Path]::GetFileNameWithoutExtension($ModulePath)); $moduleManifest = ConvertTo-ConfigurationData -ConfigurationData $ModulePath; Write-Verbose -Message ($localized.ExistingModuleVersion -f $moduleManifest.ModuleVersion); } catch { Write-Error "Oops $ModulePath" } if ($PSCmdlet.ParameterSetName -eq 'MinimumVersion') { return (($moduleManifest.ModuleVersion -as [System.Version]) -ge $MinimumVersion); } elseif ($PSCmdlet.ParameterSetName -eq 'RequiredVersion') { return (($moduleManifest.ModuleVersion -as [System.Version]) -eq $RequiredVersion); } } #end process } #end function Test-LabModuleVersion function Test-LabMofModule { <# .SYNOPSIS Tests whether the modules defined in a .mof file match the modules defined in the Lability configuration document. #> [CmdletBinding()] param ( ## List of resource modules defined in the Lability configuration file [Parameter(Mandatory)] [System.Collections.Hashtable[]] $Module, ## List of resource modules defined in the MOF file [Parameter(Mandatory)] [System.Collections.Hashtable[]] $MofModule ) process { $isCompliant = $true; foreach ($mof in $mofModule) { foreach ($labModule in $Module) { if ($labModule.Name -eq $mof.Name) { $isModuleDefined = $true; $isVersionMismatch = $false; if ($labModule.MinimumVersion) { Write-Warning -Message ($localized.ModuleUsingMinimumVersionWarning -f $labModule.Name); if ($labModule.MinimumVersion -ne $mof.RequiredVersion) { $isVersionMismatch = $true; $version = $labModule.MinimumVersion; } } elseif ($labModule.RequiredVersion) { if ($labModule.RequiredVersion -ne $mof.RequiredVersion) { $isVersionMismatch = $true; $version = $labModule.RequiredVersion; } } else { ## We have no way of knowing whether we have the right version :() Write-Warning -Message ($locaized.ModuleMissingRequiredVerWarning -f $labModule.Name); } if ($isVersionMismatch) { $isCompliant = $false; Write-Warning -Message ($localized.MofModuleVersionMismatchWarning -f $labModule.Name, $mof.RequiredVersion, $version); } } } #end foreach configuration module if (-not $isModuleDefined) { $isCompliant = $false; Write-Warning -Message ($localized.ModuleMissingDefinitionWarning -f $mof.Name); } } #end foreach Mof module return $isCompliant; } #end process } #end function function Test-LabNode { <# .SYNOPSIS Tests whether the node name is defined in a configuration document. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## PowerShell module hashtable [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [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 { $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -NoEnumerateWildcardNode; return ($null -ne $node.NodeName); } #end process } #end function function Test-LabResourceIsLocal { <# .SYNOPSIS Test whether a lab resource is available locally #> [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(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $ResourceId, ## Node's target resource folder [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $LocalResourcePath ) process { $resource = Resolve-LabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId; if (($resource.Expand) -and ($resource.Expand -eq $true)) { ## Check the ResourceId folder is present $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resourceId; $resourceExtension = [System.IO.Path]::GetExtension($resource.Filename); switch ($resourceExtension) { '.iso' { $isPresent = Test-Path -Path $resourcePath -PathType Container; } '.zip' { $isPresent = Test-Path -Path $resourcePath -PathType Container; } default { throw ($localized.ExpandNotSupportedError -f $resourceExtension); } } } else { $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resource.Filename; $isPresent = Test-Path -Path $resourcePath -PathType Leaf; } if ($isPresent) { Write-Verbose -Message ($localized.ResourceFound -f $resourcePath); return $true; } else { Write-Verbose -Message ($localized.ResourceNotFound -f $resourcePath); return $false; } } #end process } #end function function Test-LabSwitch { <# .SYNOPSIS Tests the current configuration a virtual network switch. .DESCRIPTION Tests a virtual network switch configuration using the xVMSwitch DSC resource. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Switch Id/Name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## PowerShell DSC configuration document (.psd1) containing lab metadata. [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $networkSwitch = Resolve-LabSwitch @PSBoundParameters; if (($null -ne $networkSwitch.IsExisting) -and ($networkSwitch.IsExisting -eq $true)) { ## The existing virtual switch may be of a type not supported by the DSC resource. return $true; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMSwitch -Prefix VMSwitch; return Test-LabDscResource -ResourceName VMSwitch -Parameters $networkSwitch; } #end process } #end function function Test-LabVirtualMachine { <# .SYNOPSIS Tests the current configuration a virtual machine. .DESCRIPTION Tests the current configuration a virtual machine using the xVMHyperV DSC resource. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String[]] $SwitchName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Media, [Parameter(Mandatory)] [System.UInt64] $StartupMemory, [Parameter(Mandatory)] [System.UInt64] $MinimumMemory, [Parameter(Mandatory)] [System.UInt64] $MaximumMemory, [Parameter(Mandatory)] [System.Int32] $ProcessorCount, [Parameter()] [AllowNull()] [System.String[]] $MACAddress, [Parameter()] [System.Boolean] $SecureBoot, [Parameter()] [System.Boolean] $GuestIntegrationServices, [Parameter()] [System.Boolean] $AutomaticCheckpoints, ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration. [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData ) process { $vmHyperVParams = Get-LabVirtualMachineProperty @PSBoundParameters; Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVMHyperV -Prefix VM; try { ## xVMHyperV\Test-TargetResource throws if the VHD doesn't exist? return (Test-LabDscResource -ResourceName VM -Parameters $vmHyperVParams -ErrorAction SilentlyContinue); } catch { return $false; } } #end process } #end function function Test-LabVMDisk { <# .SYNOPSIS Checks whether the lab virtual machine disk (VHDX) is present. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## VM/node name [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Name, ## Media Id [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Media, ## Lab DSC configuration data [Parameter(ValueFromPipelineByPropertyName)] [System.Collections.Hashtable] [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()] $ConfigurationData, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present' ) process { if ($PSBoundParameters.ContainsKey('ConfigurationData')) { $image = Get-LabImage -Id $Media -ConfigurationData $ConfigurationData; } else { $image = Get-LabImage -Id $Media; } $environmentName = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).EnvironmentName; $vhd = @{ Name = $Name; Path = Resolve-LabVMDiskPath -Name $Name -EnvironmentName $environmentName -Parent; ParentPath = $image.ImagePath; Generation = $image.Generation; Type = 'Differencing'; } if (-not $image) { ## This only occurs when a parent image is not available (#104). $vhd['MaximumSize'] = 136365211648; #127GB $vhd['Generation'] = 'VHDX'; } Import-LabDscResource -ModuleName xHyper-V -ResourceName MSFT_xVHD -Prefix VHD; $testDscResourceResult = Test-LabDscResource -ResourceName VHD -Parameters $vhd; if ($Ensure -eq 'Absent') { return -not $testDscResourceResult; } else { return $testDscResourceResult; } } #end process } #end function function Test-ResourceDownload { <# .SYNOPSIS Tests if a web resource has been downloaded and whether the MD5 checksum is correct. .NOTES Based upon https://github.com/iainbrighton/cRemoteFile/blob/master/DSCResources/VE_RemoteFile/VE_RemoteFile.ps1 #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.String] $DestinationPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Uri, [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Checksum, [Parameter(ValueFromPipelineByPropertyName)] [System.UInt32] $BufferSize = 64KB, ## Enables mocking terminating calls from Pester [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $ThrowOnError ##TODO: Support Headers and UserAgent ) process { [ref] $null = $PSBoundParameters.Remove('ThrowOnError'); $resource = Get-ResourceDownload @PSBoundParameters; $isCompliant = $true; if (-not (Test-Path -Path $DestinationPath -PathType Leaf)) { ## If the actual file doesn't exist return a failure! (#205) $isCompliant = $false; } elseif ([System.String]::IsNullOrEmpty($Checksum)) { Write-Verbose -Message ($localized.ResourceChecksumNotSpecified -f $DestinationPath); $isCompliant = $true; } elseif ($Checksum -eq $resource.Checksum) { Write-Verbose -Message ($localized.ResourceChecksumMatch -f $DestinationPath, $Checksum); $isCompliant = $true; } else { Write-Verbose -Message ($localized.ResourceChecksumMismatch -f $DestinationPath, $Checksum); $isCompliant = $false; } if ($ThrowOnError -and (-not $isCompliant)) { throw ($localized.ResourceChecksumMismatchError -f $DestinationPath, $Checksum); } else { return $isCompliant; } } #end process } #end function function Test-WindowsBuildNumber { <# .SYNOPSIS Validates the host build meets the specified requirements. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Minimum Windows build number required [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.Int32] $MinimumVersion ) begin { if (($null -ne $PSVersion.PSEdition) -and ($PSVersion.PSEdition -eq 'Core')) { # New-NotSupportedException } } process { $buildNumber = $PSVersionTable.BuildVersion.Build; return $buildNumber -ge $MinimumVersion; } #end process } #end function function Write-Verbose { <# .SYNOPSIS Proxy function for Write-Verbose that adds a timestamp and/or call stack information to the output. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets','')] param ( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Message ) process { if (-not [System.String]::IsNullOrEmpty($Message)) { $verboseMessage = Get-FormattedMessage -Message $Message Microsoft.PowerShell.Utility\Write-Verbose -Message $verboseMessage } } } function Write-Warning { <# .SYNOPSIS Proxy function for Write-Warning that adds a timestamp and/or call stack information to the output. #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets','')] param ( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Message ) process { if (-not [System.String]::IsNullOrEmpty($Message)) { $warningMessage = Get-FormattedMessage -Message $Message Microsoft.PowerShell.Utility\Write-Warning -Message $warningMessage } } } # SIG # Begin signature block # MIIuugYJKoZIhvcNAQcCoIIuqzCCLqcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAUsv/rU8jslgsr # fQ2F5dLDvVidQp2wM4bh3FoImvzfVKCCE6QwggWQMIIDeKADAgECAhAFmxtXno4h # 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 # DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgJ1MnsiUml0MYeKU2x4f1LCah # 2RlhmnMHik5x3n21ujIwDQYJKoZIhvcNAQEBBQAEggIATWv5r5Bh8q9HQh5Hropm # WOCu2axcd2COCCMdoXeRPnStk51E/xroNXWWkxBQR/KmIOg/Ags9ks0EVyPB4Cav # q46j7qI8VwHRdoK6xNvwgWd5CdmKwhf05GPNNqymr4Wx/5bhSf70F3yjBSJQ/axS # EmwMPglFtDvIZmHXKPT/krW4/09+9P1++2JA2CSEn36JjJ9vYh/CPFz2eScJ/ifJ # G2mUiH/6WGm4zFSCc0yD9Bl9DP6blUM5wmfBvtO7hlq0hi7I0H5SUVmXYgGq1ysw # fJKrUTmxQ1d4iXKnsu740NDlWhvY4KSBk+uXMt2RsK+O4oQ2vryhaf4eeFN7Th6L # 86gfbclqhWzLrHOYVpoyho5yKPM4N94eU9qAdmLSvjApob573bVXS1W15orERlrz # jsJ2p/QXoeqE45Qo6TKs3FOP66T+KjxCo7XvG0SFvpHVpz8Z2BIXP/yQbbyDk/po # 5KBXEVcjm3MuSwnnggKCRW+5HZwwUNR+yeSAzBEy0W9htJ7l9Ht6A7loKcsnaDWC # A4Jtv/sfd5iyjh3IUFBKlRl0t7aHbc5nhtRbqcJ2v/lLGQj+7i7fEx6RvHjD4fSq # BYSNKiTVECfDjsz0QNo1zoYArU7dUbrXhUYm2LYPsiI1bZmbrLcrsUqj5AIDNj3N # XQqaGd+cDT9eydaGsJszGDehghc5MIIXNQYKKwYBBAGCNwMDATGCFyUwghchBgkq # hkiG9w0BBwKgghcSMIIXDgIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ # EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQg9dWq1U0G # XYz1QR2AxCp3pjTeAaZsKEiVrrY8igg7Y5oCEBYMj68Nkyg2lXtW6GX+wyQYDzIw # MjUwNDA4MTYyNjEyWqCCEwMwgga8MIIEpKADAgECAhALrma8Wrp/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 # NjEyWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZb # RTAvBgkqhkiG9w0BCQQxIgQgbheeXUcGZKNCwxehXtAFYgHG0YOTNNAGgcYfOK5O # wxswNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13Pb # BdZluQWTmEOPmtswDQYJKoZIhvcNAQEBBQAEggIAqkuEmgOeTRUgswqjBDkMyLx5 # h5Qa3fyYaxLFE1tXnMCcKEvjyNzmZq4PTEI7nMpSaIeKv/8ZVFOJhlh+51bbueNK # VEP4wHlyHQzvt0oY32iRlUT7yxisZ1cvWMNymTj5pqibAYEo1XpcHEvCR8aU95YU # yvdVFaMb6KePaZxFtU0sCzKMhCzlQagSKUiN554J8oqBAZ5PVAeQzzu9OwOuksCc # +x7DQNcsTVHJMUFpAQFknHVo7QunI/QUKmTU3c5uochBHyPj3iXBCo5m/V5kpZMo # FyIE4IzvqTUVCV1daf54dlldhwJxZfF+GN663YiQo0gu1biQAnQd/ylqjxnWlCEv # MR84DJSdH7EfySereS56z9OMXa7/YM1zdt9agPgXVvA9tANSH0pzwTJQcIXdb98g # NfTlezRpoWlFbguRrLzDR5D2/9Q060vdEutpsn5l8EH3mEBsFkNgKOmAulds89wJ # rFgaWUHexhCXyh7LK73nbF0X2vByXDYdErjlskXDiX4rp/54yiCe3Y92tN3aXkz4 # 7/rid6KRnnwUE5EdnJEbnyL8e5le1qnkQdhzyV4enaFcBAJyA0FJ3/RrgMJ86yLN # 3IH8nhPycRX4+IUldaDFufh44v7VmknAjvhoB02QYr+NVzb/urBKkhCtq0hIPUGI # TVJd5C2B0WcNGxbftuE= # SIG # End signature block |