lib/private/dsc.ps1
<#
.SYNOPSIS Get a list of all Resources imported in a DSC Config .DESCRIPTION Uses RegEx to pull a list of Resources that are imported in a DSC Configuration using the Import-DSCResource cmdlet. If The -ModuleVersion parameter is included then the ModuleVersion property in the returned LabDSCModule object will be set, otherwise it will be null. .PARAMETER DSCConfigFile Contains the path to the DSC Config file to extract resource module names from. .PARAMETER DSCConfigContent Contains the content of the DSC Config to extract resource module names from. .EXAMPLE GetModulesInDSCConfig -DSCConfigFile c:\mydsc\Server01.ps1 Return the DSC Resource module list from file c:\mydsc\server01.ps1 .EXAMPLE GetModulesInDSCConfig -DSCConfigContent $DSCConfig Return the DSC Resource module list from the DSC Config in $DSCConfig. .OUTPUTS An array of LabDSCModule objects containing the DSC Resource modules required by this DSC configuration file. #> function GetModulesInDSCConfig() { [CmdLetBinding(DefaultParameterSetName="Content")] [OutputType([Object[]])] Param ( [parameter( Position=1, ParameterSetName="Content", Mandatory=$true)] [String] $DSCConfigContent, [parameter( Position=2, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [String] $DSCConfigFile ) [LabDSCModule[]] $Modules = $Null if ($PSCmdlet.ParameterSetName -eq 'File') { [String] $DSCConfigContent = Get-Content -Path $DSCConfigFile -RAW } # if $Regex = "[ \t]*?Import\-DscResource[ \t]+(?:\-ModuleName[ \t])?'?`"?([A-Za-z0-9._-]+)`"?'?(([ \t]+-ModuleVersion)?[ \t]+'?`"?([0-9.]+)`"?`?)?[ \t]*?[\r\n]+?" $Matches = [regex]::matches($DSCConfigContent, $Regex, 'IgnoreCase') foreach ($Match in $Matches) { $ModuleName = $Match.Groups[1].Value $ModuleVersion = $Match.Groups[4].Value # Make sure this module isn't already in the list if ($ModuleName -notin $Modules.ModuleName) { $Module = [LabDSCModule]::New($ModuleName) if (-not [String]::IsNullOrWhitespace($ModuleVersion)) { $Module.ModuleVersion = [Version] $ModuleVersion } # if $Modules += @( $Module ) } # if } # foreach Return $Modules } # GetModulesInDSCConfig <# .SYNOPSIS Sets the Modules Resources that should be imported in a DSC Config. .DESCRIPTION It will completely replace the list of Imported DSCResources with this new list. .PARAMETER DSCConfigFile Contains the path to the DSC Config file to set resource module names in. .PARAMETER DSCConfigContent Contains the content of the DSC Config to set resource module names in. .PARAMETER Modules Contains an array of LabDSCModule objects to replace set in the Configuration. .EXAMPLE SetModulesInDSCConfig -DSCConfigFile c:\mydsc\Server01.ps1 -Modules $Modules Set the DSC Resource module in the content from file c:\mydsc\server01.ps1 .EXAMPLE SetModulesInDSCConfig -DSCConfigContent $DSCConfig -Modules $Modules Set the DSC Resource module in the content $DSCConfig .OUTPUTS A string containing the content of the DSC Config file with the updated module names in it. #> function SetModulesInDSCConfig() { [CmdLetBinding(DefaultParameterSetName="Content")] [OutputType([String])] Param ( [parameter( Position=1, ParameterSetName="Content", Mandatory=$true)] [String] $DSCConfigContent, [parameter( Position=2, ParameterSetName="File", Mandatory=$true)] [ValidateNotNullOrEmpty()] [String] $DSCConfigFile, [parameter( Position=3, Mandatory=$true)] [ValidateNotNullOrEmpty()] [LabDSCModule[]] $Modules ) if ($PSCmdlet.ParameterSetName -eq 'File') { [String] $DSCConfigContent = Get-Content -Path $DSCConfigFile -RAW } # if $Regex = "[ \t]*?Import\-DscResource[ \t]+(?:\-ModuleName[ \t]+)?'?`"?([A-Za-z0-9._-]+)`"?'?([ \t]+(-ModuleVersion[ \t]+)?'?`"?([0-9.]+)`"?'?)?[ \t]*[\r\n]+" $Matches = [regex]::matches($DSCConfigContent, $Regex, 'IgnoreCase') foreach ($Module in $Modules) { $ImportCommand = "Import-DscResource -ModuleName '$($Module.ModuleName)'" if ($Module.ModuleVersion) { $ImportCommand = "$ImportCommand -ModuleVersion '$($Module.ModuleVersion)'" } # if $ImportCommand = " $ImportCommand`r`n" # is this module already in there? [Boolean] $Found = $False foreach ($Match in $Matches) { if ($Match.Groups[1].Value -eq $Module.ModuleName) { # Found the module - so replace it $DSCConfigContent = ("{0}{1}{2}") ` -f $DSCConfigContent.Substring(0,$Match.Index),` $ImportCommand,` $DSCConfigContent.Substring($Match.Index+$Match.Length) $Matches = [regex]::matches($DSCConfigContent, $Regex, 'IgnoreCase') $Found = $True break } # if } # foreach if (-not $Found) { if ($Matches.Count -gt 0) { # Add this to the end of the existing Import-DSCResource lines $Match = $Matches[$Matches.count-1] } else { # There are no existing DSC Resource lines, so add it after # Configuration ... { line $Match = [regex]::matches($DSCConfigContent, "[ \t]*?Configuration[ \t]+?'?`"?[A-Za-z0-9._-]+`"?'?[ \t]*?[\r\n]*?{[\r\n]*?", 'IgnoreCase') if (-not $Match) { $ExceptionParameters = @{ errorId = 'DSCConfiguartionMissingError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfiguartionMissingError) } ThrowException @ExceptionParameters } } # if $DSCConfigContent = ("{0}{1}{2}") ` -f $DSCConfigContent.Substring(0,$Match.Index+$Match.Length),` $ImportCommand,` $DSCConfigContent.Substring($Match.Index+$Match.Length) $Matches = [regex]::matches($DSCConfigContent, $Regex, 'IgnoreCase') } # Module not found so add it to the end } # foreach Return $DSCConfigContent } # SetModulesInDSCConfig <# .SYNOPSIS This function prepares all the files and modules necessary for a VM to be configured using Desired State Configuration (DSC). .DESCRIPTION This funcion performs the following tasks in preparation for starting Desired State Configuration on a Virtual Machine: 1. Ensures the folder structure for the Virtual Machine DSC files is available. 2. Gets a list of all Modules required by the DSC configuration to be applied. 3. Download and Install any missing DSC modules required for the DSC configuration. 4. Copy all modules required for the DSC configuration to the VM folder. 5. Cause a self-signed cetficiate to be created and downloaded on the Lab VM. 6. Create a Networking DSC configuration file and ensure the DSC config file calss it. 7. Create the MOF file from the config and an LCM config. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab CreateDSCMOFFiless -Lab $Lab -VM $VMs[0] Prepare the first VM in the Lab c:\mylab\config.xml for DSC configuration. .OUTPUTS None. #> function CreateDSCMOFFiles { [CmdLetBinding()] param ( [Parameter(Mandatory)] $Lab, [Parameter(Mandatory)] [LabVM] $VM ) [String] $DSCMOFFile = '' [String] $DSCMOFMetaFile = '' # Get the root path of the VM [String] $VMRootPath = $VM.VMRootPath # Get Path to LabBuilder files [String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath if (-not $VM.DSC.ConfigFile) { # This VM doesn't have a DSC Configuration return } # Make sure all the modules required to create the MOF file are installed $InstalledModules = Get-Module -ListAvailable WriteMessage -Message $($LocalizedData.DSCConfigIdentifyModulesMessage ` -f $VM.DSC.ConfigFile,$VM.Name) [String] $DSCConfigContent = Get-Content ` -Path $($VM.DSC.ConfigFile) ` -RAW [LabDSCModule[]] $DSCModules = GetModulesInDSCConfig ` -DSCConfigContent $DSCConfigContent # Add the xNetworking DSC Resource because it is always used $Module = [LabDSCModule]::New('xNetworking') # It must be 3.0.0.0 or greater $Module.MinimumVersion = [Version] '3.0.0.0' $DSCModules += @( $Module ) foreach ($DSCModule in $DSCModules) { $ModuleName = $DSCModule.ModuleName $ModuleSplat = @{ Name = $ModuleName } $ModuleVersion = $DSCModule.ModuleVersion $MinimumVersion = $DSCModule.MinimumVersion if ($ModuleVersion) { $FilterScript = { ($_.Name -eq $ModuleName) -and ($ModuleVersion -eq $_.Version) } $ModuleSplat += @{ RequiredVersion = $ModuleVersion } } elseif ($MinimumVersion) { $FilterScript = { ($_.Name -eq $ModuleName) -and ($_.Version -ge $MinimumVersion) } $ModuleSplat += @{ MinimumVersion = $MinimumVersion } } else { $FilterScript = { ($_.Name -eq $ModuleName) } } $Module = ($InstalledModules | Where-Object -FilterScript $FilterScript | Sort-Object -Property Version -Descending | Select-Object -First 1) if ($Module) { # The module already exists, load the version number into the Module # to force the version number to be set in the DSC Config file $DSCModule.ModuleVersion = $Module.Version } else { # The Module isn't available on this computer, so try and install it WriteMessage -Message $($LocalizedData.DSCConfigSearchingForModuleMessage ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) $NewModule = Find-Module ` @ModuleSplat if ($NewModule) { WriteMessage -Message $($LocalizedData.DSCConfigInstallingModuleMessage ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) try { $NewModule | Install-Module } catch { $ExceptionParameters = @{ errorId = 'DSCModuleDownloadError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleDownloadError ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) } ThrowException @ExceptionParameters } } else { $ExceptionParameters = @{ errorId = 'DSCModuleDownloadError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleDownloadError ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) } ThrowException @ExceptionParameters } $DSCModule.ModuleVersion = $NewModule.Version } # if WriteMessage -Message $($LocalizedData.DSCConfigSavingModuleMessage ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) # Find where the module is actually stored [String] $ModulePath = '' foreach ($Path in $ENV:PSModulePath.Split(';')) { $ModulePath = Join-Path ` -Path $Path ` -ChildPath $ModuleName if (Test-Path -Path $ModulePath) { break } # If } # Foreach if (-not (Test-Path -Path $ModulePath)) { $ExceptionParameters = @{ errorId = 'DSCModuleNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCModuleNotFoundError ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName) } ThrowException @ExceptionParameters } $DestinationPath = Join-Path -Path $VMLabBuilderFiles -ChildPath 'DSC Modules\' if (-not (Test-Path -Path $DestinationPath)) { # Create the DSC Modules folder if it doesn't exist. New-Item -Path $DestinationPath -ItemType Directory -Force } # if WriteMessage -Message $($LocalizedData.DSCConfigCopyingModuleMessage ` -f $VM.DSC.ConfigFile,$VM.Name,$ModuleName,$ModulePath,$DestinationPath) Copy-Item ` -Path $ModulePath ` -Destination $DestinationPath ` -Recurse ` -Force } # Foreach if ($VM.CertificateSource -eq [LabCertificateSource]::Guest) { # Recreate the certificate if it the source is the Guest if (-not (RecreateSelfSignedCertificate -Lab $Lab -VM $VM)) { $ExceptionParameters = @{ errorId = 'CertificateCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.CertificateCreateError ` -f $VM.Name) } ThrowException @ExceptionParameters } # Remove any old self-signed certifcates for this VM Get-ChildItem -Path cert:\LocalMachine\My ` | Where-Object { $_.FriendlyName -eq $Script:DSCCertificateFriendlyName } ` | Remove-Item } # if # Add the VM Self-Signed Certificate to the Local Machine store and get the Thumbprint [String] $CertificateFile = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath $Script:DSCEncryptionCert $Certificate = Import-Certificate ` -FilePath $CertificateFile ` -CertStoreLocation 'Cert:LocalMachine\My' [String] $CertificateThumbprint = $Certificate.Thumbprint # Set the predicted MOF File name $DSCMOFFile = Join-Path ` -Path $ENV:Temp ` -ChildPath "$($VM.ComputerName).mof" $DSCMOFMetaFile = ([System.IO.Path]::ChangeExtension($DSCMOFFile,'meta.mof')) # Generate the LCM MOF File WriteMessage -Message $($LocalizedData.DSCConfigCreatingLCMMOFMessage ` -f $DSCMOFMetaFile,$VM.Name) $null = ConfigLCM ` -OutputPath $($ENV:Temp) ` -ComputerName $($VM.ComputerName) ` -Thumbprint $CertificateThumbprint if (-not (Test-Path -Path $DSCMOFMetaFile)) { $ExceptionParameters = @{ errorId = 'DSCConfigMetaMOFCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMetaMOFCreateError ` -f $VM.Name) } ThrowException @ExceptionParameters } # If # A DSC Config File was provided so create a MOF File out of it. WriteMessage -Message $($LocalizedData.DSCConfigCreatingMOFMessage ` -f $VM.DSC.ConfigFile,$VM.Name) # Now create the Networking DSC Config file [String] $DSCNetworkingConfig = GetDSCNetworkingConfig ` -Lab $Lab -VM $VM [String] $NetworkingDSCFile = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'DSCNetworking.ps1' $null = Set-Content ` -Path $NetworkingDSCFile ` -Value $DSCNetworkingConfig . $NetworkingDSCFile [String] $DSCFile = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'DSC.ps1' # Set the Modules List in the DSC Configuration $DSCConfigContent = SetModulesInDSCConfig ` -DSCConfigContent $DSCConfigContent ` -Modules $DSCModules if (-not ($DSCConfigContent -match 'Networking Network {}')) { # Add the Networking Configuration item to the base DSC Config File # Find the location of the line containing "Node $AllNodes.NodeName {" [String] $Regex = '\s*Node\s.*{.*' $Matches = [regex]::matches($DSCConfigContent, $Regex, 'IgnoreCase') if ($Matches.Count -eq 1) { $DSCConfigContent = $DSCConfigContent.` Insert($Matches[0].Index+$Matches[0].Length,"`r`nNetworking Network {}`r`n") } Else { $ExceptionParameters = @{ errorId = 'DSCConfigMoreThanOneNodeError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMoreThanOneNodeError ` -f $VM.DSC.ConfigFile,$VM.Name) } ThrowException @ExceptionParameters } # If } # If # Save the DSC Content $null = Set-Content ` -Path $DSCFile ` -Value $DSCConfigContent ` -Force # Hook the Networking DSC File into the main DSC File . $DSCFile [String] $DSCConfigName = $VM.DSC.ConfigName WriteMessage -Message $($LocalizedData.DSCConfigPrepareMessage ` -f $DSCConfigname,$VM.Name) # Generate the Configuration Nodes data that always gets passed to the DSC configuration. [String] $ConfigData = @" @{ AllNodes = @( @{ NodeName = '$($VM.ComputerName)' CertificateFile = '$CertificateFile' Thumbprint = '$CertificateThumbprint' LocalAdminPassword = '$($VM.administratorpassword)' $($VM.DSC.Parameters) } ) } "@ # Write it to a temp file [String] $ConfigFile = Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'DSCConfigData.psd1' if (Test-Path -Path $ConfigFile) { $null = Remove-Item ` -Path $ConfigFile ` -Force } Set-Content -Path $ConfigFile -Value $ConfigData # Generate the MOF file from the configuration $null = & "$DSCConfigName" -OutputPath $($ENV:Temp) -ConfigurationData $ConfigFile if (-not (Test-Path -Path $DSCMOFFile)) { $ExceptionParameters = @{ errorId = 'DSCConfigMOFCreateError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.DSCConfigMOFCreateError ` -f $VM.DSC.ConfigFile,$VM.Name) } ThrowException @ExceptionParameters } # If # Remove the VM Self-Signed Certificate from the Local Machine Store $null = Remove-Item ` -Path "Cert:LocalMachine\My\$CertificateThumbprint" ` -Force WriteMessage -Message $($LocalizedData.DSCConfigMOFCreatedMessage ` -f $VM.DSC.ConfigFile,$VM.Name) # Copy the files to the LabBuilder Files folder $null = Copy-Item ` -Path $DSCMOFFile ` -Destination (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath "$($VM.ComputerName).mof") ` -Force if (-not $VM.DSC.MOFFile) { # Remove Temporary files created by DSC $null = Remove-Item ` -Path $DSCMOFFile ` -Force } if (Test-Path -Path $DSCMOFMetaFile) { $null = Copy-Item ` -Path $DSCMOFMetaFile ` -Destination (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath "$($VM.ComputerName).meta.mof") ` -Force if (-not $VM.DSC.MOFFile) { # Remove Temporary files created by DSC $null = Remove-Item ` -Path $DSCMOFMetaFile ` -Force } } # If } # CreateDSCMOFFiles <# .SYNOPSIS This function prepares the PowerShell scripts used for starting up DSC on a VM. .DESCRIPTION Two PowerShell scripts will be created by this function in the LabBuilder Files folder of the VM: 1. StartDSC.ps1 - the script that is called automatically to start up DSC. 2. StartDSCDebug.ps1 - a debug script that will start up DSC in debug mode. These scripts will contain code to perform the following operations: 1. Configure the names of the Network Adapters so that they will match the names in the DSC Configuration files. 2. Enable/Disable DSC Event Logging. 3. Apply Configuration to the Local Configuration Manager. 4. Start DSC. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab SetDSCStartFile -Lab $Lab -VM $VMs[0] Prepare the first VM in the Lab c:\mylab\config.xml for DSC start up. .OUTPUTS None. #> function SetDSCStartFile { [CmdLetBinding()] param ( [Parameter(Mandatory)] $Lab, [Parameter(Mandatory)] [LabVM] $VM ) [String] $DSCStartPs = '' # Get Path to LabBuilder files [String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath # Relabel the Network Adapters so that they match what the DSC Networking config will use # This is because unfortunately the Hyper-V Device Naming feature doesn't work. [String] $ManagementSwitchName = GetManagementSwitchName ` -Lab $Lab $Adapters = [String[]] ($VM.Adapters).Name $Adapters += @($ManagementSwitchName) foreach ($Adapter in $Adapters) { $NetAdapter = Get-VMNetworkAdapter -VMName $($VM.Name) -Name $Adapter if (-not $NetAdapter) { $ExceptionParameters = @{ errorId = 'NetworkAdapterNotFoundError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NetworkAdapterNotFoundError ` -f $Adapter,$VM.Name) } ThrowException @ExceptionParameters } # If $MacAddress = $NetAdapter.MacAddress if (-not $MacAddress) { $ExceptionParameters = @{ errorId = 'NetworkAdapterBlankMacError' errorCategory = 'InvalidArgument' errorMessage = $($LocalizedData.NetworkAdapterBlankMacError ` -f $Adapter,$VM.Name) } ThrowException @ExceptionParameters } # If $DSCStartPs += @" Get-NetAdapter `` | Where-Object { `$_.MacAddress.Replace('-','') -eq '$MacAddress' } `` | Rename-NetAdapter -NewName '$($Adapter)' "@ } # Foreach # Enable DSC logging (as long as it hasn't been already) # Nano Server doesn't have the Microsoft-Windows-Dsc/Analytic channels so # Logging can't be enabled. if ($VM.OSType -ne [LabOSType]::Nano) { [String] $Logging = ($VM.DSC.Logging).ToString() $DSCStartPs += @" `$Result = & "wevtutil.exe" get-log "Microsoft-Windows-Dsc/Analytic" if (-not (`$Result -like '*enabled: true*')) { & "wevtutil.exe" set-log "Microsoft-Windows-Dsc/Analytic" /q:true /e:$Logging } `$Result = & "wevtutil.exe" get-log "Microsoft-Windows-Dsc/Debug" if (-not (`$Result -like '*enabled: true*')) { & "wevtutil.exe" set-log "Microsoft-Windows-Dsc/Debug" /q:true /e:$Logging } "@ } # if # Start the actual DSC Configuration $DSCStartPs += @" Set-DscLocalConfigurationManager `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Verbose *>> `"`$(`$ENV:SystemRoot)\Setup\Scripts\DSC.log`" Start-DSCConfiguration `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Force `` -Verbose *>> `"`$(`$ENV:SystemRoot)\Setup\Scripts\DSC.log`" "@ $null = Set-Content ` -Path (Join-Path -Path $VMLabBuilderFiles -ChildPath 'StartDSC.ps1') ` -Value $DSCStartPs -Force $DSCStartPsDebug = @" param ( [boolean] `$WaitForDebugger ) Set-DscLocalConfigurationManager `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Verbose if (`$WaitForDebugger) { Enable-DscDebug `` -BreakAll } Start-DSCConfiguration `` -Path `"`$(`$ENV:SystemRoot)\Setup\Scripts\`" `` -Force `` -Debug `` -Wait `` -Verbose if (`$WaitForDebugger) { Disable-DscDebug } "@ $null = Set-Content ` -Path (Join-Path -Path $VMLabBuilderFiles -ChildPath 'StartDSCDebug.ps1') ` -Value $DSCStartPsDebug -Force } # SetDSCStartFile <# .SYNOPSIS This function prepares all files require to configure a VM using Desired State Configuration (DSC). .DESCRIPTION Calling this function will cause the LabBuilder folder to be populated/updated with all files required to configure a Virtual Machine with DSC. This includes: 1. Required DSC Resouce Modules. 2. DSC Credential Encryption certificate. 3. DSC Configuration files. 4. DSC MOF Files for general config and for LCM config. 5. Start up scripts. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab InitializeDSC -Lab $Lab -VM $VMs[0] Prepares all files required to start up Desired State Configuration for the first VM in the Lab c:\mylab\config.xml for DSC start up. .OUTPUTS None. #> function InitializeDSC { [CmdLetBinding()] param ( [Parameter(Mandatory)] $Lab, [Parameter(Mandatory)] [LabVM] $VM ) # Are there any DSC Settings to manage? CreateDSCMOFFiles -Lab $Lab -VM $VM # Generate the DSC Start up Script file SetDSCStartFile -Lab $Lab -VM $VM } # InitializeDSC <# .SYNOPSIS Uploads prepared Modules and MOF files to a VM and starts up Desired State Configuration (DSC) on it. .DESCRIPTION This function will perform the following tasks: 1. Connect to the VM via remoting. 2. Upload the DSC and LCM MOF files to the c:\windows\setup\scripts folder of the VM. 3. Upload DSC Start up scripts to the c:\windows\setup\scripts folder of the VM. 4. Upload all required modules to the c:\program files\WindowsPowerShell\Modules\ folder of the VM. 5. Invoke the StartDSC.ps1 script on the VM to start DSC processing. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .PARAMETER Timeout The maximum amount of time that this function can take to perform DSC start-up. If the timeout is reached before the process is complete an error will be thrown. The timeout defaults to 300 seconds. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab StartDSC -Lab $Lab -VM $VMs[0] Starts up Desired State Configuration for the first VM in the Lab c:\mylab\config.xml. .OUTPUTS None. #> function StartDSC { [CmdLetBinding()] param ( [Parameter(Mandatory)] $Lab, [Parameter(Mandatory)] [LabVM] $VM, [Int] $Timeout = 300 ) [DateTime] $StartTime = Get-Date [System.Management.Automation.Runspaces.PSSession] $Session = $null [Boolean] $Complete = $False [Boolean] $ConfigCopyComplete = $False [Boolean] $ModuleCopyComplete = $False # Get Path to LabBuilder files [String] $VMLabBuilderFiles = $VM.LabBuilderFilesPath While ((-not $Complete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -lt $TimeOut) { # Connect to the VM $Session = Connect-LabVM ` -VM $VM ` -ErrorAction Continue # Failed to connnect to the VM if (-not $Session) { $ExceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } ThrowException @ExceptionParameters return } if (($Session) ` -and ($Session.State -eq 'Opened') ` -and (-not $ConfigCopyComplete)) { $CopyParameters = @{ Destination = 'c:\Windows\Setup\Scripts' ToSession = $Session Force = $True ErrorAction = 'Stop' } # Connection has been made OK, upload the DSC files While ((-not $ConfigCopyComplete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -lt $TimeOut) { Try { WriteMessage -Message $($LocalizedData.CopyingFilesToVMMessage ` -f $VM.Name,'DSC') $null = Copy-Item ` @CopyParameters ` -Path (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath "$($VM.ComputerName).mof") if (Test-Path ` -Path "$VMLabBuilderFiles\$($VM.ComputerName).meta.mof") { $null = Copy-Item ` @CopyParameters ` -Path (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath "$($VM.ComputerName).meta.mof") } # If $null = Copy-Item ` @CopyParameters ` -Path (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'StartDSC.ps1') $null = Copy-Item ` @CopyParameters ` -Path (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath 'StartDSCDebug.ps1') $ConfigCopyComplete = $True } Catch { WriteMessage -Message $($LocalizedData.CopyingFilesToVMFailedMessage ` -f $VM.Name,'DSC',$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # while } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $ConfigCopyComplete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $ExceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } ThrowException @ExceptionParameters } # if # Upload any required modules to the VM if (($Session) ` -and ($Session.State -eq 'Opened') ` -and (-not $ModuleCopyComplete)) { [String] $DSCContent = Get-Content ` -Path $($VM.DSC.ConfigFile) ` -RAW [LabDSCModule[]] $DSCModules = GetModulesInDSCConfig ` -DSCConfigContent $DSCContent # Add the xNetworking DSC Resource because it is always used $Module = [LabDSCModule]::New('xNetworking') $DSCModules += @( $Module ) foreach ($DSCModule in $DSCModules) { $ModuleName = $DSCModule.ModuleName # Upload all but PSDesiredStateConfiguration because it # should always exist on client node. if ($ModuleName -ne 'PSDesiredStateConfiguration') { $ModuleVersion = $DSCModule.Version try { WriteMessage -Message $($LocalizedData.CopyingFilesToVMMessage ` -f $VM.Name,"DSC Module $ModuleName") $null = Copy-Item ` -Path (Join-Path ` -Path $VMLabBuilderFiles ` -ChildPath "DSC Modules\$ModuleName\") ` -Destination "$($env:ProgramFiles)\WindowsPowerShell\Modules\" ` -ToSession $Session ` -Force ` -Recurse ` -ErrorAction Stop } catch { WriteMessage -Message $($LocalizedData.CopyingFilesToVMFailedMessage ` -f $VM.Name,"DSC Module $ModuleName",$Script:RetryConnectSeconds) Start-Sleep -Seconds $Script:RetryConnectSeconds } # try } # if } # foreach $ModuleCopyComplete = $True } # if # If the copy didn't complete and we're out of time throw an exception if ((-not $ModuleCopyComplete) ` -and (((Get-Date) - $StartTime).TotalSeconds) -ge $TimeOut) { # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $ExceptionParameters = @{ errorId = 'DSCInitializationError' errorCategory = 'OperationTimeout' errorMessage = $($LocalizedData.DSCInitializationError ` -f $VM.Name) } ThrowException @ExceptionParameters } # if # Finally, Start DSC up! if (($Session) ` -and ($Session.State -eq 'Opened') ` -and ($ConfigCopyComplete) ` -and ($ModuleCopyComplete)) { WriteMessage -Message $($LocalizedData.StartingDSCMessage ` -f $VM.Name) Invoke-Command -Session $Session { c:\windows\setup\scripts\StartDSC.ps1 } # Disconnect from the VM Disconnect-LabVM ` -VM $VM ` -ErrorAction Continue $Complete = $True } # if } # while } # StartDSC <# .SYNOPSIS Assemble the content of the Networking DSC config file. .DESCRIPTION This function creates the content that will be written to the Networking DSC Config file from the networking details stored in the VM object. .EXAMPLE $Lab = Get-Lab -ConfigPath c:\mylab\config.xml $VMs = Get-LabVM -Lab $Lab $NetworkingDSC = GetDSCNetworkingConfig -Lab $Lab -VM $VMs[0] Return the Networking DSC for the first VM in the Lab c:\mylab\config.xml for DSC configuration. .PARAMETER Lab Contains the Lab object that was produced by the Get-Lab cmdlet. .PARAMETER VM A LabVM object pulled from the Lab Configuration file using Get-LabVM .OUTPUTS A string containing the DSC Networking config. #> function GetDSCNetworkingConfig { [CmdLetBinding()] [OutputType([String])] param ( [Parameter(Mandatory)] $Lab, [Parameter(Mandatory)] [LabVM] $VM ) $xNetworkingVersion = (` Get-Module xNetworking -ListAvailable ` | Sort-Object version -Descending ` | Select-Object -First 1 ` ).Version.ToString() [String] $DSCNetworkingConfig = @" Configuration Networking { Import-DscResource -ModuleName xNetworking -ModuleVersion $xNetworkingVersion "@ [Int] $AdapterCount = 0 foreach ($Adapter in $VM.Adapters) { $AdapterCount++ if ($Adapter.IPv4) { if (-not [String]::IsNullOrWhitespace($Adapter.IPv4.Address)) { $DSCNetworkingConfig += @" xIPAddress IPv4_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv4' IPAddress = '$($Adapter.IPv4.Address.Replace(',',"','"))' PrefixLength = '$($Adapter.IPv4.SubnetMask)' } "@ if (-not [String]::IsNullOrWhitespace($Adapter.IPv4.DefaultGateway)) { $DSCNetworkingConfig += @" xDefaultGatewayAddress IPv4G_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv4' Address = '$($Adapter.IPv4.DefaultGateway)' } "@ } Else { $DSCNetworkingConfig += @" xDefaultGatewayAddress IPv4G_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv4' } "@ } # If } Else { $DSCNetworkingConfig += @" xDhcpClient IPv4DHCP_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv4' State = 'Enabled' } "@ } # If if (-not [String]::IsNullOrWhitespace($Adapter.IPv4.DNSServer)) { $DSCNetworkingConfig += @" xDnsServerAddress IPv4D_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv4' Address = '$($Adapter.IPv4.DNSServer.Replace(',',"','"))' } "@ } # If } # If if ($Adapter.IPv6) { if (-not [String]::IsNullOrWhitespace($Adapter.IPv6.Address)) { $DSCNetworkingConfig += @" xIPAddress IPv6_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv6' IPAddress = '$($Adapter.IPv6.Address.Replace(',',"','"))' PrefixLength = '$($Adapter.IPv6.SubnetMask)' } "@ if (-not [String]::IsNullOrWhitespace($Adapter.IPv6.DefaultGateway)) { $DSCNetworkingConfig += @" xDefaultGatewayAddress IPv6G_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv6' Address = '$($Adapter.IPv6.DefaultGateway)' } "@ } Else { $DSCNetworkingConfig += @" xDefaultGatewayAddress IPv6G_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv6' } "@ } # If } Else { $DSCNetworkingConfig += @" xDhcpClient IPv6DHCP_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv6' State = 'Enabled' } "@ } # If if (-not [String]::IsNullOrWhitespace($Adapter.IPv6.DNSServer)) { $DSCNetworkingConfig += @" xDnsServerAddress IPv6D_$AdapterCount { InterfaceAlias = '$($Adapter.Name)' AddressFamily = 'IPv6' Address = '$($Adapter.IPv6.DNSServer.Replace(',',"','"))' } "@ } # If } # If } # Endfor $DSCNetworkingConfig += @" } "@ Return $DSCNetworkingConfig } # GetDSCNetworkingConfig [DSCLocalConfigurationManager()] Configuration ConfigLCM { Param ( [String] $ComputerName, [String] $Thumbprint ) Node $ComputerName { Settings { RefreshMode = 'Push' ConfigurationMode = 'ApplyAndAutoCorrect' CertificateId = $Thumbprint ConfigurationModeFrequencyMins = 15 RefreshFrequencyMins = 30 RebootNodeIfNeeded = $True ActionAfterReboot = 'ContinueConfiguration' } } } |