diff --git a/template/customscripts/common.psm1 b/template/customscripts/common.psm1 index 2979c4d..170675e 100644 --- a/template/customscripts/common.psm1 +++ b/template/customscripts/common.psm1 @@ -76,45 +76,52 @@ function New-LogFileName [string] $FileName ) - return '{0:yyyyMMdd-HHmmss}_{1}_{2}.txt' -f [DateTime]::Now, $env:ComputerName, $FileName + return '{0:yyyyMMdd-HHmmss}_{1}_{2}.log' -f [DateTime]::Now, $env:ComputerName, $FileName } -function Write-ScriptLog +# The script log default context. +$script:scriptLogDefaultConext = '' + +function Set-ScriptLogDefaultContext { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, Position = 0)] - [string] $Context, + [Parameter(Mandatory = $true)] + [string] $LogContext + ) + + $script:scriptLogDefaultConext = $LogContext +} +function Write-ScriptLog +{ + [CmdletBinding()] + param ( [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] [AllowEmptyString()] [string] $Message, [Parameter(Mandatory = $false)] - [ValidateSet('Verbose', 'Warning', 'Error', 'Debug', 'Otput', 'Host')] - [string] $Type = 'Verbose', + [ValidateSet('Info', 'Warning', 'Error')] + [string] $Level = 'Info', [Parameter(Mandatory = $false)] - [switch] $UseInScriptBlock + [string] $LogContext ) - $builtMessage = '[{0:yyyy-MM-dd HH:mm:ss}] [{1}] {2}' -f [DateTime]::Now, $Context, $Message - switch ($Type) { - 'Warning' { Write-Warning -Message $builtMessage } - 'Error' { Write-Error -Message $builtMessage } - 'Debug' { Write-Debug -Message $builtMessage } - 'Otput' { Write-Output -InputObject $builtMessage } - 'Host' { Write-Host -Object $builtMessage } - default { - if ($UseInScriptBlock) { - # NOTE: Redirecting a verbose message because verbose messages are not showing it come from script blocks. - Write-Verbose -Message ('VERBOSE: ' + $builtMessage) 4>&1 - } - else { - Write-Verbose -Message $builtMessage - } - } + $timestamp = '{0:yyyy-MM-dd HH:mm:ss}' -f [DateTime]::Now + $computerName = $env:ComputerName.ToLower() + $context = if ($PSBoundParameters.ContainsKey('LogContext')) { + '[{0}][{1}]' -f $computerName, $LogContext + } + elseif (-not [string]::IsNullOrEmpty($script:scriptLogDefaultConext)) { + '[{0}][{1}]' -f $computerName, $script:scriptLogDefaultConext } + else { + '[{0}]' -f $computerName + } + $logRecord = '{0} {1,-7} {2} {3}' -f $timestamp, $Level.ToUpper(), $context, $Message + Write-Host -Object $logRecord -ForegroundColor Cyan } function Get-LabDeploymentConfig @@ -225,19 +232,20 @@ function Invoke-FileDownload for ($retryCount = 0; $retryCount -lt $MaxRetryCount; $retryCount++) { try { - 'Downloading the file from "{0}" to "{1}".' -f $SourceUri, $destinationFilePath | Write-ScriptLog -Context $env:ComputerName + 'Donwload the file to "{0}" from "{1}".' -f $destinationFilePath, $SourceUri | Write-ScriptLog Start-BitsTransfer -Source $SourceUri -Destination $destinationFilePath Get-Item -LiteralPath $destinationFilePath return } catch { - ( - 'Will retry the download... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $env:ComputerName - + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Will retry the download...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning Remove-Item -LiteralPath $destinationFilePath -Force -ErrorAction Continue } Start-Sleep -Seconds $RetryIntervalSeconds @@ -397,10 +405,9 @@ function WaitingForVhdDismount ) while((Get-WindowsImage -Mounted | Where-Object -Property 'ImagePath' -EQ -Value $VhdPath) -ne $null) { - 'Waiting for VHD dismount completion...' | Write-ScriptLog -Context $VhdPath + 'Wait for the VHD dismount completion...' | Write-ScriptLog -LogContext $VhdPath Start-Sleep -Seconds $ProbeIntervalSeconds } - 'The VHD dismount completed.' | Write-ScriptLog -Context $VhdPath } function Set-UnattendAnswerFileToVhd @@ -421,32 +428,37 @@ function Set-UnattendAnswerFileToVhd $baseFolderName = [IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetDirectoryName($VhdPath)) + '-' + (New-Guid).Guid.Substring(0, 4) - 'Mouting the VHD...' | Write-ScriptLog -Context $VhdPath + 'Mount the VHD.' | Write-ScriptLog -LogContext $VhdPath $vhdMountPath = [IO.Path]::Combine('C:\', $baseFolderName + '-mount') - 'vhdMountPath: {0}' -f $vhdMountPath | Write-ScriptLog -Context $VhdPath - New-Item -ItemType Directory -Path $vhdMountPath -Force | Out-String | Write-ScriptLog -Context $VhdPath + 'vhdMountPath: "{0}"' -f $vhdMountPath | Write-ScriptLog -LogContext $VhdPath + New-Item -ItemType Directory -Path $vhdMountPath -Force | Out-String | Write-ScriptLog -LogContext $VhdPath $scratchDirectory = [IO.Path]::Combine('C:\', $baseFolderName + '-scratch') - 'scratchDirectory: {0}' -f $scratchDirectory | Write-ScriptLog -Context $VhdPath - New-Item -ItemType Directory -Path $scratchDirectory -Force | Out-String | Write-ScriptLog -Context $VhdPath + 'scratchDirectory: "{0}"' -f $scratchDirectory | Write-ScriptLog -LogContext $VhdPath + New-Item -ItemType Directory -Path $scratchDirectory -Force | Out-String | Write-ScriptLog -LogContext $VhdPath $logPath = [IO.Path]::Combine($LogFolder, (New-LogFileName -FileName ('injectunattend-' + [IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetDirectoryName($VhdPath))))) - 'logPath: {0}' -f $logPath | Write-ScriptLog -Context $VhdPath - Mount-WindowsImage -Path $vhdMountPath -Index 1 -ImagePath $VhdPath -ScratchDirectory $scratchDirectory -LogPath $logPath | Out-String | Write-ScriptLog -Context $VhdPath + 'logPath: "{0}"' -f $logPath | Write-ScriptLog -LogContext $VhdPath + Mount-WindowsImage -Path $vhdMountPath -Index 1 -ImagePath $VhdPath -ScratchDirectory $scratchDirectory -LogPath $logPath | Out-String | Write-ScriptLog -LogContext $VhdPath - 'Create the unattend answer file in the VHD...' | Write-ScriptLog -Context $VhdPath + 'Create the unattend answer file in the VHD.' | Write-ScriptLog -LogContext $VhdPath $pantherPath = [IO.Path]::Combine($vhdMountPath, 'Windows', 'Panther') - New-Item -ItemType Directory -Path $pantherPath -Force | Out-String | Write-ScriptLog -Context $VhdPath + New-Item -ItemType Directory -Path $pantherPath -Force | Out-String | Write-ScriptLog -LogContext $VhdPath Set-Content -Path ([IO.Path]::Combine($pantherPath, 'unattend.xml')) -Value $UnattendAnswerFileContent -Force + 'Create the unattend answer file in the VHD completed.' | Write-ScriptLog -LogContext $VhdPath - 'Dismouting the VHD...' | Write-ScriptLog -Context $VhdPath - Dismount-WindowsImage -Path $vhdMountPath -Save -ScratchDirectory $scratchDirectory -LogPath $logPath | Out-String | Write-ScriptLog -Context $VhdPath + 'Dismount the VHD.' | Write-ScriptLog -LogContext $VhdPath + Dismount-WindowsImage -Path $vhdMountPath -Save -ScratchDirectory $scratchDirectory -LogPath $logPath | Out-String | Write-ScriptLog -LogContext $VhdPath - 'Waiting for VHD dismount completion (MountPath: "{0}")...' -f $vhdMountPath | Write-ScriptLog -Context $VhdPath + 'Wait for the VHD dismount (MountPath: "{0}").' -f $vhdMountPath | Write-ScriptLog -LogContext $VhdPath WaitingForVhdDismount -VhdPath $VhdPath + 'The VHD dismount completed.' | Write-ScriptLog -LogContext $VhdPath + 'Remove the VHD mount path.' | Write-ScriptLog -LogContext $VhdPath Remove-Item $vhdMountPath -Force + + 'Remove the scratch directory.' | Write-ScriptLog -LogContext $VhdPath Remove-Item $scratchDirectory -Force } @@ -496,18 +508,19 @@ function Install-WindowsFeatureToVhd ) $logPath = [IO.Path]::Combine($LogFolder, (New-LogFileName -FileName ('installwinfeature-' + [IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetDirectoryName($VhdPath))))) - 'logPath: {0}' -f $logPath | Write-ScriptLog -Context $VhdPath + 'logPath: "{0}"' -f $logPath | Write-ScriptLog -LogContext $VhdPath $startTime = Get-Date while ((Get-Date) -lt ($startTime + $RetyTimeout)) { # NOTE: Effort to prevent collision of concurrent DISM operations. $waitHandle = CreateWaitHandleForSerialization -SyncEventName 'Local\hcilab-install-windows-feature-to-vhd' - 'Waiting the turn to doing the Install-WindowsFeature cmdlet''s DISM operations...' | Write-ScriptLog -Context $VhdPath + 'Wait for the turn to doing the Install-WindowsFeature cmdlet''s DISM operations.' | Write-ScriptLog -LogContext $VhdPath $waitHandle.WaitOne() - 'Acquired the turn to doing the Install-WindowsFeature cmdlet''s DISM operation.' | Write-ScriptLog -Context $VhdPath + 'Acquired the turn to doing the Install-WindowsFeature cmdlet''s DISM operation.' | Write-ScriptLog -LogContext $VhdPath try { # NOTE: Install-WindowsFeature cmdlet will fail sometimes due to concurrent operations, etc. + 'Start Windows features installation to VHD.' | Write-ScriptLog -LogContext $VhdPath $params = @{ Vhd = $VhdPath Name = $FeatureName @@ -515,31 +528,37 @@ function Install-WindowsFeatureToVhd LogPath = $logPath ErrorAction = [Management.Automation.ActionPreference]::Stop } - Install-WindowsFeature @params | Out-String | Write-ScriptLog -Context $VhdPath + Install-WindowsFeature @params | Out-String | Write-ScriptLog -LogContext $VhdPath # NOTE: The DISM mount point is still remain after the Install-WindowsFeature cmdlet completed. - 'Waiting for VHD dismount completion by the Install-WindowsFeature cmdlet execution...' | Write-ScriptLog -Context $VhdPath + 'Wait for VHD dismount completion by the Install-WindowsFeature cmdlet execution.' | Write-ScriptLog -LogContext $VhdPath WaitingForVhdDismount -VhdPath $VhdPath + 'The VHD dismount completed.' | Write-ScriptLog -LogContext $VhdPath - 'Windows features installation to VHD was completed.' | Write-ScriptLog -Context $VhdPath + 'Windows features installation to VHD completed.' | Write-ScriptLog -LogContext $VhdPath return } catch { - ( - 'Thrown a exception by Install-WindowsFeature cmdlet execution. Will retry Install-WindowsFeature cmdlet... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $VhdPath + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Thrown a exception by Install-WindowsFeature cmdlet execution. Will retry Install-WindowsFeature cmdlet...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning -LogContext $VhdPath } finally { - 'Releasing the turn to doing the Install-WindowsFeature cmdlet''s DISM operation...' | Write-ScriptLog -Context $VhdPath + 'Releasing the turn to doing the Install-WindowsFeature cmdlet''s DISM operation.' | Write-ScriptLog -LogContext $VhdPath $waitHandle.Set() $waitHandle.Dispose() } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'The Install-WindowsFeature cmdlet execution for "{0}" was not succeeded in the acceptable time ({1}).' -f $VhdPath, $RetyTimeout.ToString() + + $exceptionMessage = 'The Install-WindowsFeature cmdlet execution for "{0}" was not succeeded in the acceptable time ({1}).' -f $VhdPath, $RetyTimeout.ToString() + $exceptionMessage | Write-ScriptLog -Level Error -LogContext $VhdPath + throw $exceptionMessage } function Start-VMWithRetry @@ -566,22 +585,27 @@ function Start-VMWithRetry ErrorAction = [Management.Automation.ActionPreference]::Stop } if ((Start-VM @params) -ne $null) { - 'The VM was started.' | Write-ScriptLog -Context $VMName + 'The VM was started.' | Write-ScriptLog return } } catch { # NOTE: In sometimes, we need retry to waiting for unmount the VHD. - ( - 'Will retry start the VM... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $VMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Will retry start the VM...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'The VM "{0}" was not start in the acceptable time ({1}).' -f $VMName, $RetyTimeout.ToString() + + $exceptionMessage = 'The VM "{0}" was not start in the acceptable time ({1}).' -f $VMName, $RetyTimeout.ToString() + $exceptionMessage | Write-ScriptLog -Level Error + throw $exceptionMessage } function Wait-PowerShellDirectReady @@ -612,21 +636,26 @@ function Wait-PowerShellDirectReady ErrorAction = [Management.Automation.ActionPreference]::Stop } if ((Invoke-Command @params) -eq 'ready') { - 'The VM is ready.' | Write-ScriptLog -Context $VMName + 'PowerShell Direct is ready on the VM.' | Write-ScriptLog return } } catch { - ( - 'Probing the VM ready state... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $VMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Probing the VM ready state...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'The VM "{0}" was not ready in the acceptable time ({1}).' -f $VMName, $RetyTimeout.ToString() + + $exceptionMessage = 'The VM "{0}" was not ready in the acceptable time ({1}).' -f $VMName, $RetyTimeout.ToString() + $exceptionMessage | Write-ScriptLog -Level Error + throw $exceptionMessage } # A sync event name for blocking the AD DS operations. @@ -638,7 +667,7 @@ function Block-AddsDomainOperation [CmdletBinding()] param () - 'Block the AD DS domain operations until the AD DS DC VM deployment is completed...' | Write-ScriptLog -Context $env:ComputerName + 'Block the AD DS domain operations until the AD DS DC VM deployment is completed.' | Write-ScriptLog $params = @{ TypeName = 'System.Threading.EventWaitHandle' ArgumentList = @( @@ -660,7 +689,7 @@ function Unblock-AddsDomainOperation throw 'The wait event handle for AD DS VM ready is not initialized.' } $script:addsDcDeploymentCompletionWaitHandle.Set() - 'Unblocked the AD DS domain operations. The AD DS DC VM has been deployed.' | Write-ScriptLog -Context $env:ComputerName + 'Unblocked the AD DS domain operations. The AD DS DC VM has been deployed.' | Write-ScriptLog } finally { $script:addsDcDeploymentCompletionWaitHandle.Dispose() @@ -675,16 +704,16 @@ function Wait-AddsDcDeploymentCompletion $waitHandle = $null if ([System.Threading.EventWaitHandle]::TryOpenExisting($script:addsDcDeploymentCompletionSyncEventName, [ref] $waitHandle)) { try { - 'Waiting for the AD DS DC deployment completion...' | Write-ScriptLog -Context $env:ComputerName + 'Wait for the AD DS DC deployment completion.' | Write-ScriptLog $waitHandle.WaitOne() - 'The AD DS DC has been deployed.' | Write-ScriptLog -Context $env:ComputerName + 'The AD DS DC has been deployed.' | Write-ScriptLog } finally { $waitHandle.Dispose() } } else { - 'The AD DS DC is already deployed. (The wait handle did not exist)' | Write-ScriptLog -Context $env:ComputerName + 'The AD DS DC is already deployed. (The wait handle did not exist)' | Write-ScriptLog } } @@ -723,7 +752,7 @@ function Wait-DomainControllerServiceReady ErrorAction = [Management.Automation.ActionPreference]::Stop } if ((Invoke-Command @params) -eq $true) { - 'The AD DS DC is ready.' | Write-ScriptLog -Context $AddsDcVMName + 'The AD DS DC is ready.' | Write-ScriptLog return } } @@ -733,51 +762,58 @@ function Wait-DomainControllerServiceReady # Exception: System.Management.Automation.Remoting.PSRemotingTransportException # FullyQualifiedErrorId: 2100,PSSessionStateBroken # The background process reported an error with the following message: "The Hyper-V socket target process has ended.". - ( - 'Restart the AD DS DC VM due to PowerShell Remoting transport exception. ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $AddsDcVMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Restart the AD DS DC VM due to PowerShell Remoting transport exception.', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning $waitHandle = CreateWaitHandleForSerialization -SyncEventName 'Local\hcilab-adds-dc-vm-reboot' - 'Waiting the turn to doing the AD DS DC VM reboot...' | Write-ScriptLog -Context $AddsDcVMName + 'Wait for the turn to doing the AD DS DC VM reboot.' | Write-ScriptLog $waitHandle.WaitOne() - 'Acquired the turn to doing the AD DS DC VM reboot.' | Write-ScriptLog -Context $AddsDcVMName + 'Acquired the turn to doing the AD DS DC VM reboot.' | Write-ScriptLog try { $uptimeThresholdMinutes = 15 $addsDcVM = Get-VM -Name $AddsDcVMName # NOTE: Skip rebooting if the VM is young because it means the VM already rebooted recently by other jobs. if ($addsDcVM.UpTime -gt (New-TimeSpan -Minutes $uptimeThresholdMinutes)) { - 'Stopping the AD DS DC VM due to PowerShell Direct exception...' | Write-ScriptLog -Context $AddsDcVMName + 'Stop the AD DS DC VM due to PowerShell Direct exception.' | Write-ScriptLog Stop-VM -Name $AddsDcVMName -ErrorAction Continue - 'Starting the AD DS DC VM due to PowerShell Direct exception...' | Write-ScriptLog -Context $AddsDcVMName + 'Start the AD DS DC VM due to PowerShell Direct exception.' | Write-ScriptLog Start-VM -Name $AddsDcVMName -ErrorAction Continue } else { - 'Skip the AD DS DC VM rebooting because the VM''s uptime is too short (less than {0} minutes).' -f $uptimeThresholdMinutes | Write-ScriptLog -Context $AddsDcVMName + 'Skip the AD DS DC VM rebooting because the VM''s uptime is too short (less than {0} minutes).' -f $uptimeThresholdMinutes | Write-ScriptLog } } finally { - 'Releasing the turn to doing the AD DS DC VM reboot...' | Write-ScriptLog -Context $AddsDcVMName + 'Release the turn to doing the AD DS DC VM reboot.' | Write-ScriptLog $waitHandle.Set() $waitHandle.Dispose() } } else { - ( - 'Probing AD DS DC ready state... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $AddsDcVMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Probing AD DS DC ready state...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning } } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'The AD DS DC "{0}" was not ready in the acceptable time ({1}).' -f $AddsDcVMName, $RetyTimeout.ToString() + + $exceptionMessage = 'The AD DS DC "{0}" was not ready in the acceptable time ({1}).' -f $AddsDcVMName, $RetyTimeout.ToString() + $exceptionMessage | Write-ScriptLog -Level Error + throw $exceptionMessage } function New-LogonCredential @@ -828,7 +864,7 @@ function Add-VMToADDomain [TimeSpan] $RetyTimeout = (New-TimeSpan -Minutes 30) ) - 'Joining the VM "{0}" to the AD domain "{1}"...' -f $VMName, $DomainFqdn | Write-ScriptLog -Context $VMName + 'Join the "{0}" VM to the AD domain "{1}".' -f $VMName, $DomainFqdn | Write-ScriptLog $startTime = Get-Date while ((Get-Date) -lt ($startTime + $RetyTimeout)) { @@ -846,20 +882,25 @@ function Add-VMToADDomain ErrorAction = [Management.Automation.ActionPreference]::Stop } Invoke-Command @params - 'The VM "{0}" was joined to the AD domain "{1}".' -f $VMName, $DomainFqdn | Write-ScriptLog -Context $VMName + 'Join the "{0}" VM to the AD domain "{1}" completed.' -f $VMName, $DomainFqdn | Write-ScriptLog return } catch { - ( - 'Will retry join the VM "{0}" to the AD domain "{1}"... ' + - '(ExceptionMessage: {2} | Exception: {3} | FullyQualifiedErrorId: {4} | CategoryInfo: {5} | ErrorDetailsMessage: {6})' - ) -f @( - $VMName, $DomainFqdn, $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $VMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + ('Will retry join the VM "{0}" to the AD domain "{1}"... ' -f $VMName, $DomainFqdn), + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'Domain join the VM "{0}" to the AD domain "{1}" was not complete in the acceptable time ({2}).' -f $VMName, $DomainFqdn, $RetyTimeout.ToString() + + $exceptionMessage = 'Domain join the "{0}" VM to the AD domain "{1}" was not complete in the acceptable time ({2}).' -f $VMName, $DomainFqdn, $RetyTimeout.ToString() + $exceptionMessage | Write-ScriptLog -Level Error + throw $exceptionMessage } function Copy-PSModuleIntoVM @@ -873,8 +914,10 @@ function Copy-PSModuleIntoVM [string] $ModuleFilePathToCopy ) + 'Copy the PowerShell module from "{0}" on the lab host into the VM "{1}".' -f $ModuleFilePathToCopy, $Session.VMName | Write-ScriptLog $commonModuleFilePathInVM = [IO.Path]::Combine('C:\Windows\Temp', [IO.Path]::GetFileName($ModuleFilePathToCopy)) Copy-Item -ToSession $Session -Path $ModuleFilePathToCopy -Destination $commonModuleFilePathInVM + 'Copy the PowerShell module from "{0}" on the lab host to "{1}" on the VM "{2}" completed.' -f $ModuleFilePathToCopy, $commonModuleFilePathInVM, $Session.VMName | Write-ScriptLog return $commonModuleFilePathInVM } @@ -930,11 +973,14 @@ function Invoke-PSDirectSessionCleanup [string] $CommonModuleFilePath ) - 'Deleting the common module file "{0}" within the VM...' -f $CommonModuleFilePath | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Delete the common module file "{0}" on the VM "{1}".' -f $CommonModuleFilePath, $env:ComputerName | Write-ScriptLog Remove-Item -LiteralPath $CommonModuleFilePath -Force - } | Out-String | Write-ScriptLog -Context $env:ComputerName + 'Delete the common module file "{0}" on the VM "{1}" completed.' -f $CommonModuleFilePath, $env:ComputerName | Write-ScriptLog + } | Out-String | Write-ScriptLog + 'Delete PowerShell Direct sessions.' | Write-ScriptLog $Session | Remove-PSSession + 'Delete PowerShell Direct sessions completed.' | Write-ScriptLog } function New-ShortcutFile @@ -1020,6 +1066,7 @@ function New-WacConnectionFileContent $exportFunctions = @( 'Start-ScriptLogging', 'Stop-ScriptLogging', + 'Set-ScriptLogDefaultContext', 'Write-ScriptLog', 'Get-LabDeploymentConfig', 'Get-Secret', diff --git a/template/customscripts/configure-lab-host.ps1 b/template/customscripts/configure-lab-host.ps1 index 04a3907..abd4954 100644 --- a/template/customscripts/configure-lab-host.ps1 +++ b/template/customscripts/configure-lab-host.ps1 @@ -15,31 +15,34 @@ function Invoke-WindowsTerminalInstallation [string] $DownloadFolderPath ) - 'Downloading the Windows 10 pre-install kit zip file...' | Write-ScriptLog -Context $env:ComputerName + 'Download the Windows 10 pre-install kit zip file.' | Write-ScriptLog $params = @{ SourceUri = 'https://github.com/microsoft/terminal/releases/download/v1.19.10821.0/Microsoft.WindowsTerminal_1.19.10821.0_8wekyb3d8bbwe.msixbundle_Windows10_PreinstallKit.zip' DownloadFolder = $DownloadFolderPath FileNameToSave = 'Microsoft.WindowsTerminal_Windows10_PreinstallKit.zip' } $zipFile = Invoke-FileDownload @params + 'Download the Windows 10 pre-install kit zip file completed.' | Write-ScriptLog - 'Expaneding the Windows 10 pre-install kit zip file...' | Write-ScriptLog -Context $env:ComputerName + 'Expaned the Windows 10 pre-install kit zip file.' | Write-ScriptLog $fileSourceFolder = [IO.Path]::Combine([IO.Path]::GetDirectoryName($zipFile.FullName), [IO.Path]::GetFileNameWithoutExtension($zipFile.FullName)) Expand-Archive -LiteralPath $zipFile.FullName -DestinationPath $fileSourceFolder -Force + 'Expaned the Windows 10 pre-install kit zip file completed.' | Write-ScriptLog # Retrieve the Windows Termainl intallation files. $uiXamlAppxFile = Get-ChildItem -LiteralPath $fileSourceFolder -Filter 'Microsoft.UI.Xaml.*_x64__*.appx' | Select-Object -First 1 $msixBundleFile = Get-ChildItem -LiteralPath $fileSourceFolder -Filter '*.msixbundle' | Select-Object -First 1 $licenseXmlFile = Get-ChildItem -LiteralPath $fileSourceFolder -Filter '*_License1.xml' | Select-Object -First 1 + 'Windows Terminal installation files:' | Write-ScriptLog + 'Microsoft.UI.Xaml: "{0}"' -f $uiXamlAppxFile.FullName | Write-ScriptLog + 'Microsoft.WindowsTerminal: "{0}"' -f $msixBundleFile.FullName | Write-ScriptLog + 'LicenseXml: "{0}"' -f $licenseXmlFile.FullName | Write-ScriptLog - 'Microsoft.UI.Xaml: "{0}"' -f $uiXamlAppxFile.FullName | Write-ScriptLog -Context $env:ComputerName - 'Microsoft.WindowsTerminal: "{0}"' -f $msixBundleFile.FullName | Write-ScriptLog -Context $env:ComputerName - 'LicenseXml: "{0}"' -f $licenseXmlFile.FullName | Write-ScriptLog -Context $env:ComputerName - - 'Installing the dependency packages for Windows Terminal...' | Write-ScriptLog -Context $env:ComputerName + 'Install the dependency packages for Windows Terminal.' | Write-ScriptLog Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath $uiXamlAppxFile.FullName + 'Install the dependency packages for Windows Terminal completed.' | Write-ScriptLog - 'Creating a script file for the Windows Terminal installation scheduled task...' | Write-ScriptLog -Context $env:ComputerName + 'Create a script file for the scheduled task to install Windows Terminal.' | Write-ScriptLog $scheduledTaskName = 'WindowsTermailInstallation' $scheduledTaskScriptFileContent = @" (Get-Host).UI.RawUI.WindowTitle = 'Windows Terminal installation' @@ -49,8 +52,9 @@ Disable-ScheduledTask -TaskName '{2}' -TaskPath '\' "@ -f $msixBundleFile.FullName, $licenseXmlFile.FullName, $scheduledTaskName $scheduledTaskScriptFilePath = [IO.Path]::Combine($DownloadFolderPath, $scheduledTaskName + 'Task.ps1') Set-Content -LiteralPath $scheduledTaskScriptFilePath -Value $scheduledTaskScriptFileContent -Force + 'Create a script file for the scheduled task to install Windows Terminal completed.' | Write-ScriptLog - 'Creating a scheduled task for Windows Terminal installation...' | Write-ScriptLog -Context $env:ComputerName + 'Create a new scheduled task to install Windows Terminal.' | Write-ScriptLog $adminUsername = Get-InstanceMetadata -FilterPath '/compute/osProfile/adminUsername' -LeafNode $params = @{ TaskName = $scheduledTaskName @@ -62,6 +66,7 @@ Disable-ScheduledTask -TaskName '{2}' -TaskPath '\' Force = $true } Register-ScheduledTask @params + 'Create a new scheduled task to install Windows Terminal completed.' | Write-ScriptLog } function Invoke-VSCodeInstallation @@ -73,15 +78,16 @@ function Invoke-VSCodeInstallation [string] $DownloadFolderPath ) - 'Downloading the Visual Studio Code system installer...' | Write-ScriptLog -Context $env:ComputerName + 'Download the Visual Studio Code system installer.' | Write-ScriptLog $params = @{ SourceUri = 'https://code.visualstudio.com/sha/download?build=stable&os=win32-x64' DownloadFolder = $DownloadFolderPath FileNameToSave = 'VSCodeSetup-x64.exe' } $installerFile = Invoke-FileDownload @params + 'Download the Visual Studio Code system installer completed.' | Write-ScriptLog - 'Installing the Visual Studio Code...' | Write-ScriptLog -Context $env:ComputerName + 'Install Visual Studio Code.' | Write-ScriptLog $mergeTasks = @( 'desktopicon', '!quicklaunchicon', @@ -97,18 +103,34 @@ function Invoke-VSCodeInstallation Wait = $true PassThru = $true } - Start-Process @params + $result = Start-Process @params + $result | Format-List -Property @( + @{ Label = 'FileName'; Expression = { $_.StartInfo.FileName } }, + @{ Label = 'Arguments'; Expression = { $_.StartInfo.Arguments } }, + @{ Label = 'WorkingDirectory'; Expression = { $_.StartInfo.WorkingDirectory } }, + 'Id', + 'HasExited', + 'ExitCode', + 'StartTime', + 'ExitTime', + 'TotalProcessorTime', + 'PrivilegedProcessorTime', + 'UserProcessorTime' + ) | Out-String | Write-ScriptLog + 'Install Visual Studio Code completed.' | Write-ScriptLog } Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'common.psm1')) -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log + +'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host # Volume -'Creating a storage pool...' | Write-ScriptLog -Context $env:ComputerName +'Create a new storage pool.' | Write-ScriptLog $params = @{ FriendlyName = $labConfig.labHost.storage.poolName StorageSubSystemFriendlyName = '*storage*' @@ -116,8 +138,9 @@ $params = @{ } $storagePool = New-StoragePool @params $storagePool | Format-List -Property '*' +'Create a new storage pool completed.' | Write-ScriptLog -'Creating a virtual disk...' | Write-ScriptLog -Context $env:ComputerName +'Create a new virtual disk.' | Write-ScriptLog $params = @{ StoragePoolFriendlyName = $labConfig.labHost.storage.poolName FriendlyName = $labConfig.labHost.storage.volumeLabel @@ -129,8 +152,9 @@ $params = @{ } $virtualDisk = New-VirtualDisk @params $virtualDisk | Format-List -Property '*' +'Create a new virtual disk completed.' | Write-ScriptLog -'Initializing the virtual disk...' | Write-ScriptLog -Context $env:ComputerName +'Initialize the virtual disk.' | Write-ScriptLog $params = @{ UniqueId = $virtualDisk.UniqueId PartitionStyle = 'GPT' @@ -138,16 +162,18 @@ $params = @{ } $disk = Initialize-Disk @params $disk | Format-List -Property '*' +'Initialize the virtual disk completed.' | Write-ScriptLog -'Creating a partition...' | Write-ScriptLog -Context $env:ComputerName +'Create a new partition.' | Write-ScriptLog $params = @{ DriveLetter = $labConfig.labHost.storage.driveLetter UseMaximumSize = $true } $partition = $disk | New-Partition @params $partition | Format-List -Property '*' +'Create a new partition completed.' | Write-ScriptLog -'Formatting the volume...' | Write-ScriptLog -Context $env:ComputerName +'Format the partition.' | Write-ScriptLog $params = @{ FileSystem = 'ReFS' AllocationUnitSize = 4KB @@ -155,40 +181,43 @@ $params = @{ } $volume = $partition | Format-Volume @params $volume | Format-List -Property '*' +'Format the partition completed.' | Write-ScriptLog -'Setting Defender exclusions...' | Write-ScriptLog -Context $env:ComputerName +'Set Defender exclusions.' | Write-ScriptLog $exclusionPath = $storageVolume.DriveLetter + ':\' Add-MpPreference -ExclusionPath $exclusionPath if ((Get-MpPreference).ExclusionPath -notcontains $exclusionPath) { throw 'Defender exclusion setting failed.' } +'Set Defender exclusions completed.' | Write-ScriptLog -'Creating the folder structure on the volume...' | Write-ScriptLog -Context $env:ComputerName +'Create the folder structure on the volume.' | Write-ScriptLog New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.temp -Force New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.updates -Force New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.vhd -Force New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.vm -Force - -'The volume creation has been completed.' | Write-ScriptLog -Context $env:ComputerName +'Create the folder structure on the volume completed.' | Write-ScriptLog # Hyper-V -'Configuring Hyper-V host settings...' | Write-ScriptLog -Context $env:ComputerName +'Configure Hyper-V host settings.' | Write-ScriptLog $params = @{ VirtualMachinePath = $labConfig.labHost.folderPath.vm VirtualHardDiskPath = $labConfig.labHost.folderPath.vhd EnableEnhancedSessionMode = $true } Set-VMHost @params +'Configure Hyper-V host settings completed.' | Write-ScriptLog -'Creating a NAT vSwitch...' | Write-ScriptLog -Context $env:ComputerName +'Create a new NAT vSwitch.' | Write-ScriptLog $params = @{ Name = $labConfig.labHost.vSwitch.nat.name SwitchType = 'Internal' } New-VMSwitch @params +'Create a new NAT vSwitch completed.' | Write-ScriptLog -'Enabling forwarding on the host''s NAT network interfaces...' | Write-ScriptLog -Context $env:ComputerName +'Enable forwarding on the lab host''s NAT network interface.' | Write-ScriptLog $paramsForGet = @{ InterfaceAlias = '*{0}*' -f $labConfig.labHost.vSwitch.nat.name } @@ -196,16 +225,18 @@ $paramsForSet = @{ Forwarding = 'Enabled' } Get-NetIPInterface @paramsForGet | Set-NetIPInterface @paramsForSet +'Enable forwarding on the lab host''s NAT network interface completed.' | Write-ScriptLog foreach ($netNat in $labConfig.labHost.netNat) { - 'Creating a network NAT "{0}"...' -f $netNat.name | Write-ScriptLog -Context $env:ComputerName + 'Create a new network NAT "{0}".' -f $netNat.name | Write-ScriptLog $params = @{ Name = $netNat.name InternalIPInterfaceAddressPrefix = $netNat.InternalAddressPrefix } New-NetNat @params + 'Create a new network NAT "{0}" completed.' -f $netNat.name | Write-ScriptLog - 'Assigning an internal IP configuration to the host''s NAT network interface...' | Write-ScriptLog -Context $env:ComputerName + 'Assign an internal IP configuration to the host''s NAT network interface.' | Write-ScriptLog $params= @{ InterfaceIndex = (Get-NetAdapter -Name ('*{0}*' -f $labConfig.labHost.vSwitch.nat.name)).ifIndex AddressFamily = 'IPv4' @@ -213,44 +244,46 @@ foreach ($netNat in $labConfig.labHost.netNat) { PrefixLength = $netNat.hostInternalPrefixLength } New-NetIPAddress @params + 'Assign an internal IP configuration to the host''s NAT network interface completed.' | Write-ScriptLog } -'The Hyper-V configuration has been completed.' | Write-ScriptLog -Context $env:ComputerName - # Tweaks -'Setting to stop Server Manager launch at logon.' | Write-ScriptLog -Context $env:ComputerName +'Stop Server Manager launch at logon.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotOpenServerManagerAtLogon' -Value 1 +'Stop Server Manager launch at logon completed.' | Write-ScriptLog -'Setting to stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog -Context $env:ComputerName +'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotPopWACConsoleAtSMLaunch' -Value 1 +'Stop Windows Admin Center popup at Server Manager launch completed.' | Write-ScriptLog -'Setting to hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog -Context $env:ComputerName +'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Network' -KeyName 'NewNetworkWindowOff' +'Hide the Network Location wizard completed.' | Write-ScriptLog -'Setting to hide the first run experience of Microsoft Edge.' | Write-ScriptLog -Context $env:ComputerName +'Hide the first run experience of Microsoft Edge.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SOFTWARE\Policies\Microsoft' -KeyName 'Edge' Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' -Name 'HideFirstRunExperience' -Value 1 - -'Some tweaks have been completed.' | Write-ScriptLog -Context $env:ComputerName +'Hide the first run experience of Microsoft Edge completed.' | Write-ScriptLog # Install tools $toolsToInstall = $labConfig.labHost.toolsToInstall -split ';' - if ($toolsToInstall -contains 'windowsterminal') { - 'Executing the Windows Terminal installation pre-tasks...' | Write-ScriptLog -Context $env:ComputerName + 'Execute the Windows Terminal installation pre-tasks.' | Write-ScriptLog Invoke-WindowsTerminalInstallation -DownloadFolderPath $labConfig.labHost.folderPath.temp + 'Execute the Windows Terminal installation pre-tasks completed.' | Write-ScriptLog } if ($toolsToInstall -contains 'vscode') { - 'Installing Visual Studio Code...' | Write-ScriptLog -Context $env:ComputerName + 'Install Visual Studio Code.' | Write-ScriptLog Invoke-VSCodeInstallation -DownloadFolderPath $labConfig.labHost.folderPath.temp + 'Install Visual Studio Code completed.' | Write-ScriptLog } # Shortcuts: Windows Admin Center -'Creating a shortcut for open Windows Admin Center on the desktop....' | Write-ScriptLog -Context $env:ComputerName +'Create a new shortcut on the desktop for accessing Windows Admin Center.' | Write-ScriptLog $params = @{ ShortcutFilePath = 'C:\Users\Public\Desktop\Windows Admin Center.lnk' TargetPath = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' @@ -259,10 +292,11 @@ $params = @{ IconLocation = 'imageres.dll,1' } New-ShortcutFile @params +'Create a new shortcut on the desktop for accessing Windows Admin Center completed.' | Write-ScriptLog # Shortcuts: Remote Desktop connection -'Creating a shortcut for Remote Desktop connection to the management tools server VM on the desktop....' | Write-ScriptLog -Context $env:ComputerName +'Create a new shortcut on the desktop for connecting to the management server using Remote Desktop connection.' | Write-ScriptLog $params = @{ ShortcutFilePath = 'C:\Users\Public\Desktop\Management tools server.lnk' TargetPath = '%windir%\System32\mstsc.exe' @@ -270,9 +304,10 @@ $params = @{ Description = 'Make a remote desktop connection to the Windows Admin Center VM in your lab environment.' } New-ShortcutFile @params +'Create a new shortcut on the desktop for connecting to the management server using Remote Desktop connection completed.' | Write-ScriptLog +'Create a new shortcut on the desktop for connecting to the first HCI node using Remote Desktop connection.' | Write-ScriptLog $firstHciNodeName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index 0 -'Creating a shortcut for Remote Desktop connection to the {0} VM on the desktop...' -f $firstHciNodeName | Write-ScriptLog -Context $env:ComputerName $params = @{ ShortcutFilePath = 'C:\Users\Public\Desktop\{0}.lnk' -f $firstHciNodeName TargetPath = '%windir%\System32\mstsc.exe' @@ -280,10 +315,11 @@ $params = @{ Description = 'Make a remote desktop connection to the member node "{0}" VM of the HCI cluster in your lab environment.' -f $firstHciNodeName } New-ShortcutFile @params +'Create a new shortcut on the desktop for connecting to the first HCI node using Remote Desktop connection completed.' | Write-ScriptLog # Shortcuts: Hyper-V Manager -'Creating a shortcut for Hyper-V Manager on the desktop....' | Write-ScriptLog -Context $env:ComputerName +'Create a new shortcut on the desktop for launching Hyper-V Manager.' | Write-ScriptLog $params = @{ ShortcutFilePath = 'C:\Users\Public\Desktop\Hyper-V Manager.lnk' TargetPath = '%windir%\System32\mmc.exe' @@ -292,7 +328,7 @@ $params = @{ IconLocation = '%ProgramFiles%\Hyper-V\SnapInAbout.dll,0' } New-ShortcutFile @params +'Create a new shortcut on the desktop for launching Hyper-V Manager completed.' | Write-ScriptLog -'Shortcut creation has been completed.' | Write-ScriptLog -Context $env:ComputerName - +'The lab host configuration has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-base-vhd-job.ps1 b/template/customscripts/create-base-vhd-job.ps1 index 6136969..3a1fb13 100644 --- a/template/customscripts/create-base-vhd-job.ps1 +++ b/template/customscripts/create-base-vhd-job.ps1 @@ -29,6 +29,9 @@ Import-Module -Name $PSModuleNameToImport -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log -FileName $LogFileName +Set-ScriptLogDefaultContext -LogContext ('{0}_{1}_{2}' -f $OperatingSystem, $ImageIndex, $Culture) + +'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host $params = @{ @@ -49,12 +52,13 @@ if ($PSBoundParameters.Keys.Contains('IsoFileNameSuffix')) { Suffix = $IsoFileNameSuffix } $copiedIsoFilePath = [IO.Path]::Combine($labConfig.labHost.folderPath.temp, (Format-IsoFileName @params)) - 'Copying an ISO file for concurrency from "{0}" to "{1}"...' -f $isoFilePath, $copiedIsoFilePath | Write-ScriptLog -Context $env:ComputerName - Copy-Item -LiteralPath $isoFilePath -Destination $copiedIsoFilePath -Force -PassThru | Format-List -Property '*' | Out-String | Write-ScriptLog -Context $env:ComputerName + 'Copy an ISO file to "{0}" from "{1}" for concurrency.' -f $copiedIsoFilePath, $isoFilePath | Write-ScriptLog + Copy-Item -LiteralPath $isoFilePath -Destination $copiedIsoFilePath -Force -PassThru | Format-List -Property '*' | Out-String | Write-ScriptLog $isoFilePath = $copiedIsoFilePath + 'Copy an ISO file for concurrency completed.' | Write-ScriptLog } -'Converting the ISO file to a VHD file...' | Write-ScriptLog -Context $env:ComputerName +'Convert the ISO file to a VHD file.' | Write-ScriptLog $params = @{ OperatingSystem = $OperatingSystem @@ -85,13 +89,19 @@ if ($updatePackage.Count -ne 0) { } Convert-WindowsImage @params +'Convert the ISO file to a VHD file completed.' | Write-ScriptLog + if (-not (Test-Path -PathType Leaf -LiteralPath $vhdFilePath)) { - throw 'The created VHD "{0}" does not exist.' -f $vhdFilePath + $logMessage = 'The converted VHD file "{0}" does not exist.' -f $vhdFilePath + $logMessage | Write-ScriptLog -Level Error + throw $logMessage } if ($PSBoundParameters.Keys.Contains('IsoFileNameSuffix')) { - # Remove the copied ISO file. + 'Remove the copied ISO file "{0}".' -f $isoFilePath | Write-ScriptLog Remove-Item -LiteralPath $isoFilePath -Force + 'Remove the copied ISO file completed.' | Write-ScriptLog } +'The base VHD creation job has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-base-vhd.ps1 b/template/customscripts/create-base-vhd.ps1 index f3c8c38..8de6189 100644 --- a/template/customscripts/create-base-vhd.ps1 +++ b/template/customscripts/create-base-vhd.ps1 @@ -70,6 +70,8 @@ try { $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log + + 'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host # Base VHD specs. @@ -89,13 +91,15 @@ try { Culture = $labConfig.guestOS.culture } - 'Creating the temp folder if it does not exist...' | Write-ScriptLog -Context $env:ComputerName + 'Create the temp folder if it does not exist.' | Write-ScriptLog New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.temp -Force + 'Create the temp folder completed.' | Write-ScriptLog - 'Creating the VHD folder if it does not exist...' | Write-ScriptLog -Context $env:ComputerName + 'Create the VHD folder if it does not exist.' | Write-ScriptLog New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.vhd -Force + 'Create the VHD folder completed.' | Write-ScriptLog - 'Downloading the Convert-WindowsImage.ps1...' | Write-ScriptLog -Context $env:ComputerName + 'Download the Convert-WindowsImage.ps1.' | Write-ScriptLog $params = @{ SourceUri = 'https://raw.githubusercontent.com/microsoft/MSLab/master/Tools/Convert-WindowsImage.ps1' DownloadFolder = $labConfig.labHost.folderPath.temp @@ -103,14 +107,16 @@ try { } $convertWimScriptFile = Invoke-FileDownload @params $convertWimScriptFile + 'Download the Convert-WindowsImage.ps1 completed.' | Write-ScriptLog - 'Clarifying the base VHD''s specification...' | Write-ScriptLog -Context $env:ComputerName + 'Clarify the base VHD''s specification.' | Write-ScriptLog $dedupedVhdSpecs = Get-DeduplicatedBaseVhdSpec -BaseVhdSpec $addsDcVhdSpec, $wacVhdSpec, $hciNodeVhdSpec - $dedupedVhdSpecs | Format-Table -Property 'OperatingSystem', 'ImageIndex', 'Culture' | Out-String | Write-ScriptLog -Context $env:ComputerName + $dedupedVhdSpecs | Format-Table -Property 'OperatingSystem', 'ImageIndex', 'Culture' | Out-String | Write-ScriptLog $vhdSpecs = Get-PracticalBaseVhdSpec -BaseVhdSpec $dedupedVhdSpecs - $vhdSpecs | Format-Table -Property 'OperatingSystem', 'ImageIndex', 'Culture', 'IsoFileNameSuffix' | Out-String | Write-ScriptLog -Context $env:ComputerName + $vhdSpecs | Format-Table -Property 'OperatingSystem', 'ImageIndex', 'Culture', 'IsoFileNameSuffix' | Out-String | Write-ScriptLog + 'Clarify the base VHD''s specification completed.' | Write-ScriptLog - 'Creating the base VHD creation jobs...' | Write-ScriptLog -Context $env:ComputerName + 'Create the base VHD creation jobs.' | Write-ScriptLog $jobScriptFilePath = [IO.Path]::Combine($PSScriptRoot, 'create-base-vhd-job.ps1') $jobs = @() foreach ($spec in $vhdSpecs) { @@ -125,15 +131,16 @@ try { if ($spec.IsoFileNameSuffix -ne $null) { $jobParams.IsoFileNameSuffix = $spec.IsoFileNameSuffix } - 'Starting a base VHD creation job "{0}"...' -f $jobName | Write-ScriptLog -Context $env:ComputerName + 'Start a base VHD creation job "{0}".' -f $jobName | Write-ScriptLog $jobs += Start-Job -Name $jobName -LiteralPath $jobScriptFilePath -InputObject ([PSCustomObject] $jobParams) } $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime $jobs | Receive-Job -Wait $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime + 'Create the base VHD creation jobs completed.' | Write-ScriptLog - 'The base VHDs creation has been completed.' | Write-ScriptLog -Context $env:ComputerName + 'The base VHDs creation has been completed.' | Write-ScriptLog } catch { $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime diff --git a/template/customscripts/create-hci-cluster.ps1 b/template/customscripts/create-hci-cluster.ps1 index 0193493..c81a1b2 100644 --- a/template/customscripts/create-hci-cluster.ps1 +++ b/template/customscripts/create-hci-cluster.ps1 @@ -10,6 +10,7 @@ Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'common.psm1')) -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log +'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host $nodeNames = @() @@ -20,25 +21,25 @@ $nodeNames += for ($nodeIndex = 0; $nodeIndex -lt $labConfig.hciNode.nodeCount; $adminPassword = Get-Secret -KeyVaultName $labConfig.keyVault.name -SecretName $labConfig.keyVault.secretName.adminPassword $domainCredential = New-LogonCredential -DomainFqdn $labConfig.addsDomain.fqdn -Password $adminPassword -'Create PowerShell Direct session for the HCI nodes...' | Write-ScriptLog -Context $env:ComputerName +'Create PowerShell Direct session for the HCI nodes.' | Write-ScriptLog $hciNodeDomainAdminCredPSSessions = @() foreach ($nodeName in $nodeNames) { $hciNodeDomainAdminCredPSSessions += New-PSSession -VMName $nodeName -Credential $domainCredential } -$hciNodeDomainAdminCredPSSessions | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName +$hciNodeDomainAdminCredPSSessions | Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create PowerShell Direct session for the HCI nodes completed.' | Write-ScriptLog -'Copying the common module file into the HCI nodes...' | Write-ScriptLog -Context $env:ComputerName +'Copy the common module file into the HCI nodes.' | Write-ScriptLog foreach ($domainAdminCredPSSession in $hciNodeDomainAdminCredPSSessions) { $commonModuleFilePathInVM = Copy-PSModuleIntoVM -Session $domainAdminCredPSSession -ModuleFilePathToCopy (Get-Module -Name 'common').Path } +'Copy the common module file into the HCI nodes completed.' | Write-ScriptLog -'Setup the PowerShell Direct session for the HCI nodes...' | Write-ScriptLog -Context $env:ComputerName +'Setup the PowerShell Direct session for the HCI nodes.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $hciNodeDomainAdminCredPSSessions -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session for the HCI nodes completed.' | Write-ScriptLog -'Creating virtual switches on each HCI node...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock +'Create virtual switches on each HCI node.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ NetAdapterName = [PSCustomObject] @{ @@ -76,26 +77,38 @@ Invoke-Command @params -Session $hciNodeDomainAdminCredPSSessions -ScriptBlock { Sort-Object -Property 'PSComputerName' | Format-Table -Property 'PSComputerName', 'Name', 'SwitchType', 'AllowManagementOS', 'EmbeddedTeamingEnabled' | Out-String | - Write-ScriptLog -Context $env:ComputerName + Write-ScriptLog +'Create virtual switches on each HCI node completed.' | Write-ScriptLog -'Preparing HCI node''s drives...' | Write-ScriptLog -Context $env:ComputerName +'Prepare HCI node''s drives.' | Write-ScriptLog Invoke-Command -Session $hciNodeDomainAdminCredPSSessions -ScriptBlock { # Updates the cache of the service for a particular provider and associated child objects. + 'Update the storage provider cache.' | Write-ScriptLog Update-StorageProviderCache + 'Update the storage provider cache completed.' | Write-ScriptLog # Disable read-only state of storage pools except the Primordial pool. + 'Disable read-only state of the storage pool.' | Write-ScriptLog Get-StoragePool | Where-Object -Property 'IsPrimordial' -EQ -Value $false | Set-StoragePool -IsReadOnly:$false + 'Disable read-only state of the storage pool completed.' | Write-ScriptLog # Delete virtual disks in storage pools except the Primordial pool. + 'Delete virtual disks in the storage pool.' | Write-ScriptLog Get-StoragePool | Where-Object -Property 'IsPrimordial' -EQ -Value $false | Get-VirtualDisk | Remove-VirtualDisk -Confirm:$false -ErrorAction Continue + 'Delete virtual disks in the storage pool completed.' | Write-ScriptLog # Delete storage pools except the Primordial pool. + 'Delete the storage pool.' | Write-ScriptLog Get-StoragePool | Where-Object -Property 'IsPrimordial' -EQ -Value $false | Remove-StoragePool -Confirm:$false + 'Delete the storage pool completed.' | Write-ScriptLog - # Reset the status of a physical disks. (Delete the storage pool's metadata from physical disks) + # Reset the status of physical disks. (Delete the storage pool's metadata from physical disks) + 'Delete the storage pool''s metadata from physical disks.' | Write-ScriptLog Get-PhysicalDisk | Reset-PhysicalDisk + 'Delete the storage pool''s metadata from physical disks completed.' | Write-ScriptLog # Cleans disks by removing all partition information and un-initializing it, erasing all data on the disks. + 'Erase all data on the disks.' | Write-ScriptLog Get-Disk | Where-Object -Property 'Number' -NE $null | Where-Object -Property 'IsBoot' -NE $true | @@ -108,6 +121,7 @@ Invoke-Command -Session $hciNodeDomainAdminCredPSSessions -ScriptBlock { $_ | Set-Disk -IsReadOnly:$true $_ | Set-Disk -IsOffline:$true } + 'Erase all data on the disks completed.' | Write-ScriptLog Get-Disk | Where-Object -Property 'Number' -NE $null | @@ -120,35 +134,39 @@ Invoke-Command -Session $hciNodeDomainAdminCredPSSessions -ScriptBlock { Sort-Object -Property 'PSComputerName' | Format-Table -Property 'PSComputerName', 'Count', 'Name' | Out-String | - Write-ScriptLog -Context $env:ComputerName + Write-ScriptLog +'Prepare HCI node''s drives completed.' | Write-ScriptLog -'Cleaning up the PowerShell Direct session for the HCI nodes...' | Write-ScriptLog -Context $env:ComputerName +'Clean up the PowerShell Direct session for the HCI nodes.' | Write-ScriptLog Invoke-PSDirectSessionCleanup -Session $hciNodeDomainAdminCredPSSessions -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Clean up the PowerShell Direct session for the HCI nodes completed.' | Write-ScriptLog -'Create PowerShell Direct sessions for the management machine...' | Write-ScriptLog -Context $env:ComputerName +'Create PowerShell Direct sessions for the management server.' | Write-ScriptLog $wacDomainAdminCredPSSession = New-PSSession -VMName $labConfig.wac.vmName -Credential $domainCredential $wacDomainAdminCredPSSession | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName + Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create PowerShell Direct sessions for the management server completed.' | Write-ScriptLog -'Copying the common module file into the management machine...' | Write-ScriptLog -Context $env:ComputerName +'Copy the common module file into the management server.' | Write-ScriptLog $commonModuleFilePathInVM = Copy-PSModuleIntoVM -Session $wacDomainAdminCredPSSession -ModuleFilePathToCopy (Get-Module -Name 'common').Path +'Copy the common module file into the management server completed.' | Write-ScriptLog -'Setup the PowerShell Direct session for the management machine...' | Write-ScriptLog -Context $env:ComputerName +'Setup the PowerShell Direct session for the management server.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $wacDomainAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session for the management server completed.' | Write-ScriptLog -'Getting the node''s UI culture...' | Write-ScriptLog -Context $env:ComputerName +'Get the node''s UI culture.' | Write-ScriptLog $langTag = Invoke-Command -Session $wacDomainAdminCredPSSession -ScriptBlock { (Get-UICulture).IetfLanguageTag } -'The node''s UI culture is "{0}".' -f $langTag | Write-ScriptLog -Context $env:ComputerName +'The node''s UI culture is "{0}".' -f $langTag | Write-ScriptLog $localizedDataFileName = ('create-hci-cluster-test-cat-{0}.psd1' -f $langTag).ToLower() -'Localized data file name: {0}' -f $localizedDataFileName | Write-ScriptLog -Context $env:ComputerName +'Localized data file name: {0}' -f $localizedDataFileName | Write-ScriptLog Import-LocalizedData -FileName $localizedDataFileName -BindingVariable 'clusterTestCategories' +'Import the localized data completed.' | Write-ScriptLog -'Testing the HCI cluster nodes...' | Write-ScriptLog -Context $env:ComputerName +'Test the HCI cluster nodes.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ Node = $nodeNames @@ -171,9 +189,10 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { ErrorAction = [Management.Automation.ActionPreference]::Stop } Test-Cluster @params -} | Out-String | Write-ScriptLog -Context $env:ComputerName +} | Out-String | Write-ScriptLog +'Test the HCI cluster nodes completed.' | Write-ScriptLog -'Creating an HCI cluster...' | Write-ScriptLog -Context $env:ComputerName +'Create an HCI cluster.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterName = $labConfig.hciCluster.name @@ -202,9 +221,10 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { ErrorAction = [Management.Automation.ActionPreference]::Stop } New-Cluster @params -} | Out-String | Write-ScriptLog -Context $env:ComputerName +} | Out-String | Write-ScriptLog +'Create an HCI cluster completed.' | Write-ScriptLog -'Waiting for the cluster to be ready...' | Write-ScriptLog -Context $env:ComputerName +'Wait for the HCI cluster to be ready.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterName = $labConfig.hciCluster.name @@ -230,19 +250,25 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { return } catch { - ( - 'Probing the cluster ready state... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-Host + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Probing the cluster ready state...', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning } Start-Sleep -Seconds $RetryIntervalSeconds } - throw 'The cluster was not ready in the acceptable time ({0}).' -f $RetyTimeout.ToString() -} | Out-String | Write-ScriptLog -Context $env:ComputerName -'Configuring the cluster quorum...' | Write-ScriptLog -Context $env:ComputerName + $logMessage = 'The cluster was not ready in the acceptable time ({0}).' -f $RetyTimeout.ToString() + $logMessage | Write-ScriptLog -Level Error + throw $logMessage +} | Out-String | Write-ScriptLog +'The HCI cluster is ready.' | Write-ScriptLog + +'Configure the cluster quorum.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterName = $labConfig.hciCluster.name @@ -271,9 +297,10 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { ErrorAction = [Management.Automation.ActionPreference]::Stop } Set-ClusterQuorum @params -} | Out-String | Write-ScriptLog -Context $env:ComputerName +} | Out-String | Write-ScriptLog +'Configure the cluster quorum completed.' | Write-ScriptLog -'Renaming the cluster network names...' | Write-ScriptLog -Context $env:ComputerName +'Rename the cluster network names.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterName = $labConfig.hciCluster.name @@ -314,15 +341,16 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { foreach ($clusterNetwork in $clusterNetworks) { foreach ($hciNodeNetwork in $HciNodeNetworks) { if (($clusterNetwork.Ipv4Addresses[0] -eq $hciNodeNetwork.IPAddress) -and ($clusterNetwork.Ipv4PrefixLengths[0] -eq $hciNodeNetwork.PrefixLength)) { - 'Rename the cluster network "{0}" to "{1}".' -f $clusterNetwork.Name, $hciNodeNetwork.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Rename the cluster network to "{0}" from "{1}".' -f $hciNodeNetwork.Name, $clusterNetwork.Name | Write-ScriptLog $clusterNetwork.Name = $hciNodeNetwork.Name break } } } -} | Out-String | Write-ScriptLog -Context $env:ComputerName +} | Out-String | Write-ScriptLog +'Rename the cluster network names completed.' | Write-ScriptLog -'Changing the cluster network order for live migration...' | Write-ScriptLog -Context $env:ComputerName +'Change the cluster network order for live migration.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterName = $labConfig.hciCluster.name @@ -346,12 +374,13 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { for ($i = 0; $i -lt $MigrationNetworkOrder.Length; $i++) { $migrationNetworkOrderValue += (Get-ClusterNetwork -Cluster $ClusterName -Name $MigrationNetworkOrder[$i]).Id } - 'Cluster network order for live migration: {0}' -f ($migrationNetworkOrderValue -join '; ') | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Cluster network order for live migration: {0}' -f ($migrationNetworkOrderValue -join '; ') | Write-ScriptLog Get-ClusterResourceType -Cluster $ClusterName -Name 'Virtual Machine' | Set-ClusterParameter -Name 'MigrationNetworkOrder' -Value ($migrationNetworkOrderValue -join ';') -} | Out-String | Write-ScriptLog -Context $env:ComputerName +} | Out-String | Write-ScriptLog +'Change the cluster network order for live migration completed.' | Write-ScriptLog -'Enabling Storage Space Direct (S2D)...' | Write-ScriptLog -Context $env:ComputerName +'Enable Storage Space Direct (S2D).' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ HciNodeName = $nodeNames[0] @@ -376,10 +405,13 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { } Enable-ClusterStorageSpacesDirect @params + 'Clean up CIM sessions.' | Write-ScriptLog Get-CimSession | Remove-CimSession -} | Out-String | Write-ScriptLog -Context $env:ComputerName + 'Clean up CIM sessions completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Enable Storage Space Direct (S2D) completed.' | Write-ScriptLog -'Creating a volume on S2D...' | Write-ScriptLog -Context $env:ComputerName +'Create a volume on S2D.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ HciNodeName = $nodeNames[0] @@ -412,10 +444,13 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { } New-Volume @params + 'Clean up CIM sessions.' | Write-ScriptLog Get-CimSession | Remove-CimSession -} | Out-String | Write-ScriptLog -Context $env:ComputerName + 'Clean up CIM sessions completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Create a volume on S2D completed.' | Write-ScriptLog -'Importing a WAC connection for the HCI cluster...' | Write-ScriptLog -Context $env:ComputerName +'Import a WAC connection for the HCI cluster.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ ClusterFqdn = '{0}.{1}' -f $LabConfig.hciCluster.name, $LabConfig.addsDomain.fqdn @@ -427,25 +462,33 @@ Invoke-Command @params -Session $wacDomainAdminCredPSSession -ScriptBlock { [string] $ClusterFqdn ) + 'Import the WAC connection tools PowerShell module.' | Write-ScriptLog $wacConnectionToolsPSModulePath = [IO.Path]::Combine($env:ProgramFiles, 'Windows Admin Center\PowerShell\Modules\ConnectionTools\ConnectionTools.psm1') Import-Module -Name $wacConnectionToolsPSModulePath -Force + 'Import the WAC connection tools PowerShell module completed.' | Write-ScriptLog - # Create a connection list file to import to Windows Admin Center. + 'Create a connection list file to import to Windows Admin Center.' | Write-ScriptLog $connectionEntries = @( (New-WacConnectionFileEntry -Name $ClusterFqdn -Type 'msft.sme.connection-type.cluster' -Tag $ClusterFqdn) ) $wacConnectionFilePathInVM = [IO.Path]::Combine('C:\Windows\Temp', 'wac-connections.txt') New-WacConnectionFileContent -ConnectionEntry $connectionEntries | Set-Content -LiteralPath $wacConnectionFilePathInVM -Force + 'Create a connection list file to import to Windows Admin Center completed.' | Write-ScriptLog - # Import connections to Windows Admin Center. + 'Import the HCI cluster connection to Windows Admin Center.' | Write-ScriptLog [Uri] $gatewayEndpointUri = 'https://{0}' -f $env:ComputerName Import-Connection -GatewayEndpoint $gatewayEndpointUri -FileName $wacConnectionFilePathInVM + 'Import the HCI cluster connection to Windows Admin Center completed.' | Write-ScriptLog + + 'Delete the connection list file.' | Write-ScriptLog Remove-Item -LiteralPath $wacConnectionFilePathInVM -Force -} | Out-String | Write-ScriptLog -Context $env:ComputerName + 'Delete the connection list file completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Import a WAC connection for the HCI cluster completed.' | Write-ScriptLog -'Cleaning up the PowerShell Direct session for the management machine...' | Write-ScriptLog -Context $env:ComputerName +'Clean up the PowerShell Direct session for the management server.' | Write-ScriptLog Invoke-PSDirectSessionCleanup -Session $wacDomainAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Clean up the PowerShell Direct session for the management server completed.' | Write-ScriptLog -'The HCI cluster creation has been completed.' | Write-ScriptLog -Context $env:ComputerName - +'The HCI cluster creation has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-vm-job-addsdc.ps1 b/template/customscripts/create-vm-job-addsdc.ps1 index 3fa98a6..baa9368 100644 --- a/template/customscripts/create-vm-job-addsdc.ps1 +++ b/template/customscripts/create-vm-job-addsdc.ps1 @@ -16,14 +16,17 @@ Import-Module -Name $PSModuleNameToImport -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log -FileName $LogFileName -$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog -Context $env:ComputerName +Set-ScriptLogDefaultContext -LogContext $labConfig.addsDC.vmName -$vmName = $labConfig.addsDC.vmName +'Lab deployment config:' | Write-ScriptLog +$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog -'Block the AD DS domain operations on other VMs.' | Write-ScriptLog -Context $vmName +'Start blocking the AD DS domain operations on other VMs.' | Write-ScriptLog Block-AddsDomainOperation -'Creating the OS disk for the VM...' | Write-ScriptLog -Context $vmName +# Hyper-V VM + +'Create the OS disk for the VM.' | Write-ScriptLog $params = @{ OperatingSystem = [HciLab.OSSku]::WindowsServer2022 ImageIndex = [HciLab.OSImageIndex]::WSDatacenterServerCore # Datacenter (Server Core) @@ -33,40 +36,45 @@ $parentVhdFileName = Format-BaseVhdFileName @params $params = @{ Differencing = $true ParentPath = [IO.Path]::Combine($labConfig.labHost.folderPath.vhd, $parentVhdFileName) - Path = [IO.Path]::Combine($labConfig.labHost.folderPath.vm, $vmName, 'osdisk.vhdx') + Path = [IO.Path]::Combine($labConfig.labHost.folderPath.vm, $labConfig.addsDC.vmName, 'osdisk.vhdx') } $vmOSDiskVhd = New-VHD @params +'Create the OS disk for the VM completed.' | Write-ScriptLog -'Creating the VM...' | Write-ScriptLog -Context $vmName +'Create the VM.' | Write-ScriptLog $params = @{ - Name = $vmName + Name = $labConfig.addsDC.vmName Path = $labConfig.labHost.folderPath.vm VHDPath = $vmOSDiskVhd.Path Generation = 2 } -New-VM @params | Out-String | Write-ScriptLog -Context $vmName +New-VM @params | Out-String | Write-ScriptLog +'Create the VM completed.' | Write-ScriptLog -'Changing the VM''s automatic stop action...' | Write-ScriptLog -Context $vmName -Set-VM -Name $vmName -AutomaticStopAction ShutDown +'Change the VM''s automatic stop action.' | Write-ScriptLog +Set-VM -Name $labConfig.addsDC.vmName -AutomaticStopAction ShutDown +'Change the VM''s automatic stop action completed.' | Write-ScriptLog -'Setting the VM''s processor configuration...' | Write-ScriptLog -Context $vmName +'Configure the VM''s processor.' | Write-ScriptLog $vmProcessorCount = 4 if ((Get-VMHost).LogicalProcessorCount -lt $vmProcessorCount) { $vmProcessorCount = (Get-VMHost).LogicalProcessorCount } -Set-VMProcessor -VMName $vmName -Count $vmProcessorCount +Set-VMProcessor -VMName $labConfig.addsDC.vmName -Count $vmProcessorCount +'Configure the VM''s processor completed.' | Write-ScriptLog -'Setting the VM''s memory configuration...' | Write-ScriptLog -Context $vmName +'Configure the VM''s memory.' | Write-ScriptLog $params = @{ - VMName = $vmName + VMName = $labConfig.addsDC.vmName StartupBytes = 1GB DynamicMemoryEnabled = $true MinimumBytes = 512MB MaximumBytes = $labConfig.addsDC.maximumRamBytes } Set-VMMemory @params +'Configure the VM''s memory completed.' | Write-ScriptLog -'Enabling vTPM...' | Write-ScriptLog -Context $vmName +'Enable the VM''s vTPM.' | Write-ScriptLog $params = @{ - VMName = $vmName + VMName = $labConfig.addsDC.vmName NewLocalKeyProtector = $true Passthru = $true ErrorAction = [Management.Automation.ActionPreference]::Stop @@ -75,23 +83,27 @@ try { Set-VMKeyProtector @params | Enable-VMTPM } catch { - ( - 'Caught exception on enable vTPM, will retry to enable vTPM... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $vmName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Caught exception on enable vTPM, will retry to enable vTPM.', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning # Rescue only once by retry. Set-VMKeyProtector @params | Enable-VMTPM } +'Enable the VM''s vTPM completed.' | Write-ScriptLog -'Setting the VM''s network adapter configuration...' | Write-ScriptLog -Context $vmName -Get-VMNetworkAdapter -VMName $vmName | Remove-VMNetworkAdapter +'Configure the VM''s network adapters.' | Write-ScriptLog +Get-VMNetworkAdapter -VMName $labConfig.addsDC.vmName | Remove-VMNetworkAdapter # Management +'Configure the {0} network adapter.' -f $labConfig.addsDC.netAdapters.management.name | Write-ScriptLog $paramsForAdd = @{ - VMName = $vmName + VMName = $labConfig.addsDC.vmName Name = $labConfig.addsDC.netAdapters.management.name SwitchName = $labConfig.labHost.vSwitch.nat.name DeviceNaming = [Microsoft.HyperV.PowerShell.OnOffState]::On @@ -105,26 +117,29 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $labConfig.addsDC.netAdapters.management.name | Write-ScriptLog -'Generating the unattend answer XML...' | Write-ScriptLog -Context $vmName +'Generate the unattend answer XML.' | Write-ScriptLog $adminPassword = Get-Secret -KeyVaultName $labConfig.keyVault.name -SecretName $labConfig.keyVault.secretName.adminPassword $params = @{ - ComputerName = $vmName + ComputerName = $labConfig.addsDC.vmName Password = $adminPassword Culture = $labConfig.guestOS.culture TimeZone = $labConfig.guestOS.timeZone } $unattendAnswerFileContent = New-UnattendAnswerFileContent @params +'Generate the unattend answer XML completed.' | Write-ScriptLog -'Injecting the unattend answer file to the VM...' | Write-ScriptLog -Context $vmName +'Inject the unattend answer file to the "{0}".' -f $vmOSDiskVhd.Path | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path UnattendAnswerFileContent = $unattendAnswerFileContent LogFolder = $labConfig.labHost.folderPath.log } Set-UnattendAnswerFileToVhd @params +'Inject the unattend answer file to the "{0}" completed.' -f $vmOSDiskVhd.Path | Write-ScriptLog -'Installing the roles and features to the VHD...' | Write-ScriptLog -Context $vmName +'Install the roles and features to the "{0}".' -f $vmOSDiskVhd.Path | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path FeatureName = @( @@ -134,40 +149,49 @@ $params = @{ LogFolder = $labConfig.labHost.folderPath.log } Install-WindowsFeatureToVhd @params +'Install the roles and features to the "{0}" completed.' -f $vmOSDiskVhd.Path | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $vmName -Start-VMWithRetry -VMName $vmName +'Start the VM.' | Write-ScriptLog +Start-VMWithRetry -VMName $labConfig.addsDC.vmName +'Start the VM completed.' | Write-ScriptLog -'Waiting for the VM to be ready...' | Write-ScriptLog -Context $vmName +'Wait for the VM to be ready.' | Write-ScriptLog $localAdminCredential = New-LogonCredential -DomainFqdn '.' -Password $adminPassword -Wait-PowerShellDirectReady -VMName $vmName -Credential $localAdminCredential +Wait-PowerShellDirectReady -VMName $labConfig.addsDC.vmName -Credential $localAdminCredential +'The VM is ready.' | Write-ScriptLog + +# Guest OS -'Create a PowerShell Direct session...' | Write-ScriptLog -Context $vmName -$localAdminCredPSSession = New-PSSession -VMName $vmName -Credential $localAdminCredential -$localAdminCredPSSession | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName +'Create a PowerShell Direct session.' | Write-ScriptLog +$localAdminCredPSSession = New-PSSession -VMName $labConfig.addsDC.vmName -Credential $localAdminCredential +$localAdminCredPSSession | Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create a PowerShell Direct session completed.' | Write-ScriptLog -'Copying the common module file into the VM...' | Write-ScriptLog -Context $vmName +'Copy the common module file into the VM.' | Write-ScriptLog $commonModuleFilePathInVM = Copy-PSModuleIntoVM -Session $localAdminCredPSSession -ModuleFilePathToCopy (Get-Module -Name 'common').Path +'Copy the common module file into the VM completed.' | Write-ScriptLog -'Setup the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Setup the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $localAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session completed.' | Write-ScriptLog -'Configuring registry values within the VM...' | Write-ScriptLog -Context $vmName +'Configure registry values within the VM.' | Write-ScriptLog Invoke-Command -Session $localAdminCredPSSession -ScriptBlock { - 'Stop Server Manager launch at logon.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Stop Server Manager launch at logon.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotOpenServerManagerAtLogon' -Value 1 + 'Stop Server Manager launch at logon completed.' | Write-ScriptLog - 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotPopWACConsoleAtSMLaunch' -Value 1 + 'Stop Windows Admin Center popup at Server Manager launch completed.' | Write-ScriptLog - 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Network' -KeyName 'NewNetworkWindowOff' -} | Out-String | Write-ScriptLog -Context $vmName + 'Hide the Network Location wizard completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Configure registry values within the VM completed.' | Write-ScriptLog -'Configuring network settings within the VM...' | Write-ScriptLog -Context $vmName +'Configure network settings within the VM.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ VMConfig = $LabConfig.addsDC @@ -179,13 +203,14 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { [PSCustomObject] $VMConfig ) - 'Renaming the network adapters...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Rename the network adapters.' | Write-ScriptLog Get-NetAdapterAdvancedProperty -RegistryKeyword 'HyperVNetworkAdapterName' | ForEach-Object -Process { Rename-NetAdapter -Name $_.Name -NewName $_.DisplayValue } + 'Rename the network adapters completed.' | Write-ScriptLog # Management - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.netAdapters.management.name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.netAdapters.management.name | Write-ScriptLog $paramsForSetNetIPInterface = @{ AddressFamily = 'IPv4' Dhcp = 'Disabled' @@ -203,12 +228,34 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Get-NetAdapter -Name $VMConfig.netAdapters.management.name | Set-NetIPInterface @paramsForSetNetIPInterface | New-NetIPAddress @paramsForNewIPAddress | - Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.netAdapters.management.name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock - -} | Out-String | Write-ScriptLog -Context $vmName - -'Installing AD DS (Creating a new forest) within the VM...' | Write-ScriptLog -Context $vmName + Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.netAdapters.management.name | Write-ScriptLog + + 'Network adapter IP configurations:' | Write-ScriptLog + Get-NetIPAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + 'AddressFamily', + 'IPAddress', + 'PrefixLength', + 'PrefixOrigin', + 'SuffixOrigin', + 'AddressState', + 'Store' + ) | Out-String -Width 200 | Write-ScriptLog + + 'Network adapter DNS configurations:' | Write-ScriptLog + Get-DnsClientServerAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + @{ Label = 'AddressFamily'; Expression = { Switch ($_.AddressFamily) { 2 { 'IPv4' } 23 { 'IPv6' } default { $_.AddressFamily } } } } + @{ Label = 'DNSServers'; Expression = { $_.ServerAddresses } } + ) | Out-String -Width 200 | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Configure network settings within the VM completed.' | Write-ScriptLog + +'Install AD DS (Creating a new forest) within the VM.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ DomainName = $labConfig.addsDomain.fqdn @@ -232,25 +279,29 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Force = $true } Install-ADDSForest @params -} | Out-String | Write-ScriptLog -Context $vmName +} | Out-String | Write-ScriptLog +'Install AD DS (Creating a new forest) within the VM completed.' | Write-ScriptLog -'Cleaning up the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Clean up the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionCleanup -Session $localAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Clean up the PowerShell Direct session completed.' | Write-ScriptLog -'Stopping the VM...' | Write-ScriptLog -Context $vmName -Stop-VM -Name $vmName +'Stop the VM.' | Write-ScriptLog +Stop-VM -Name $labConfig.addsDC.vmName +'Stop the VM completed.' | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $vmName -Start-VM -Name $vmName +'Start the VM.' | Write-ScriptLog +Start-VM -Name $labConfig.addsDC.vmName +'Start the VM completed.' | Write-ScriptLog -'Waiting for ready to the domain controller...' | Write-ScriptLog -Context $vmName +'Wait for ready to the domain controller.' | Write-ScriptLog $domainAdminCredential = New-LogonCredential -DomainFqdn $labConfig.addsDomain.fqdn -Password $adminPassword # The DC's computer name is the same as the VM name. It's specified in the unattend.xml. -Wait-DomainControllerServiceReady -AddsDcVMName $vmName -AddsDcComputerName $vmName -Credential $domainAdminCredential +Wait-DomainControllerServiceReady -AddsDcVMName $labConfig.addsDC.vmName -AddsDcComputerName $labConfig.addsDC.vmName -Credential $domainAdminCredential +'The domain controller is ready.' | Write-ScriptLog -'Allow the AD DS domain operations on other VMs.' | Write-ScriptLog -Context $vmName +'Allow the AD DS domain operations on other VMs.' | Write-ScriptLog Unblock-AddsDomainOperation -'The AD DS Domain Controller VM creation has been completed.' | Write-ScriptLog -Context $vmName - +'The AD DS Domain Controller VM creation has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-vm-job-hcinode.ps1 b/template/customscripts/create-vm-job-hcinode.ps1 index 15ac854..66324e1 100644 --- a/template/customscripts/create-vm-job-hcinode.ps1 +++ b/template/customscripts/create-vm-job-hcinode.ps1 @@ -29,10 +29,10 @@ function Invoke-HciNodeRamSizeCalculation $totalRamBytes = (Get-VMHost).MemoryCapacity $labHostReservedRamBytes = [Math]::Floor($totalRamBytes * 0.06) # Reserve a few percent of the total RAM for the lab host. - 'TotalRamBytes: {0}' -f $totalRamBytes | Write-ScriptLog -Context $env:ComputerName - 'LabHostReservedRamBytes: {0}' -f $labHostReservedRamBytes | Write-ScriptLog -Context $env:ComputerName - 'AddsDcVMRamBytes: {0}' -f $AddsDcVMRamBytes | Write-ScriptLog -Context $env:ComputerName - 'WacVMRamBytes: {0}' -f $WacVMRamBytes | Write-ScriptLog -Context $env:ComputerName + 'TotalRamBytes: {0}' -f $totalRamBytes | Write-ScriptLog + 'LabHostReservedRamBytes: {0}' -f $labHostReservedRamBytes | Write-ScriptLog + 'AddsDcVMRamBytes: {0}' -f $AddsDcVMRamBytes | Write-ScriptLog + 'WacVMRamBytes: {0}' -f $WacVMRamBytes | Write-ScriptLog # StartupBytes should be a multiple of 2 MB (2 * 1024 * 1024 bytes). return [Math]::Floor((($totalRamBytes - $labHostReservedRamBytes - $AddsDcVMRamBytes - $WacVMRamBytes) / $NodeCount) / 2MB) * 2MB @@ -74,9 +74,12 @@ Import-Module -Name $PSModuleNameToImport -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log -FileName $LogFileName -$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog -Context $env:ComputerName -$vmName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index $NodeIndex +$nodeVMName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index $NodeIndex +Set-ScriptLogDefaultContext -LogContext $nodeVMName + +'Lab deployment config:' | Write-ScriptLog +$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog $params = @{ OperatingSystem = $labConfig.hciNode.operatingSystem.sku @@ -92,9 +95,9 @@ $params = @{ } $ramBytes = Invoke-HciNodeRamSizeCalculation @params -'Creating a VM configuraton for the HCI node VM...' -f $vmName | Write-ScriptLog -Context $vmName +'Create a VM configuraton for the HCI node VM.' | Write-ScriptLog $nodeConfig = [PSCustomObject] @{ - VMName = $vmName + VMName = $nodeVMName ParentVhdPath = $parentVhdPath RamBytes = $ramBytes OperatingSystem = $labConfig.hciNode.operatingSystem.sku @@ -132,33 +135,40 @@ $nodeConfig = [PSCustomObject] @{ } } } -$nodeConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog -Context $vmName +$nodeConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog +'Create a VM configuraton for the HCI node VM completed.' | Write-ScriptLog + +# Hyper-V VM -'Creating the OS disk...' | Write-ScriptLog -Context $nodeConfig.VMName +'Create the OS disk.' | Write-ScriptLog $params = @{ Path = [IO.Path]::Combine($labConfig.labHost.folderPath.vm, $nodeConfig.VMName, 'osdisk.vhdx') Differencing = $true ParentPath = $nodeConfig.ParentVhdPath } $vmOSDiskVhd = New-VHD @params +'Create the OS disk completed.' | Write-ScriptLog -'Creating the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Create the VM.' | Write-ScriptLog $params = @{ Name = $nodeConfig.VMName Path = $labConfig.labHost.folderPath.vm VHDPath = $vmOSDiskVhd.Path Generation = 2 } -New-VM @params | Out-String | Write-ScriptLog -Context $vmName +New-VM @params | Out-String | Write-ScriptLog +'Create the VM completed.' | Write-ScriptLog -'Changing the VM''s automatic stop action...' | Write-ScriptLog -Context $nodeConfig.VMName +'Change the VM''s automatic stop action.' | Write-ScriptLog Set-VM -Name $nodeConfig.VMName -AutomaticStopAction ShutDown +'Change the VM''s automatic stop action completed.' | Write-ScriptLog -'Setting processor configuration...' | Write-ScriptLog -Context $nodeConfig.VMName +'Configure the VM''s processor.' | Write-ScriptLog $vmProcessorCount = (Get-VMHost).LogicalProcessorCount Set-VMProcessor -VMName $nodeConfig.VMName -Count $vmProcessorCount -ExposeVirtualizationExtensions $true +'Configure the VM''s processor completed.' | Write-ScriptLog -'Setting memory configuration...' | Write-ScriptLog -Context $nodeConfig.VMName +'Configure the VM''s memory.' | Write-ScriptLog $params = @{ VMName = $nodeConfig.VMName StartupBytes = $nodeConfig.RamBytes @@ -167,8 +177,9 @@ $params = @{ MaximumBytes = $nodeConfig.RamBytes } Set-VMMemory @params +'Configure the VM''s memory completed.' | Write-ScriptLog -'Enabling vTPM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Enable the VM''s vTPM.' | Write-ScriptLog $params = @{ VMName = $nodeConfig.VMName NewLocalKeyProtector = $true @@ -179,21 +190,25 @@ try { Set-VMKeyProtector @params | Enable-VMTPM } catch { - ( - 'Caught exception on enable vTPM, will retry to enable vTPM... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $nodeConfig.VMName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Caught exception on enable vTPM, will retry to enable vTPM.', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning # Rescue only once by retry. Set-VMKeyProtector @params | Enable-VMTPM } +'Enable the VM''s vTPM completed.' | Write-ScriptLog -'Setting the VM''s network adapter configuration...' | Write-ScriptLog -Context $nodeConfig.VMName +'Configure the VM''s network adapters.' | Write-ScriptLog Get-VMNetworkAdapter -VMName $nodeConfig.VMName | Remove-VMNetworkAdapter # Management +'Configure the {0} network adapter.' -f $nodeConfig.NetAdapters.Management.Name | Write-ScriptLog $paramsForAdd = @{ VMName = $nodeConfig.VMName Name = $nodeConfig.NetAdapters.Management.Name @@ -209,8 +224,10 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $nodeConfig.NetAdapters.Management.Name | Write-ScriptLog # Compute +'Configure the {0} network adapter.' -f $nodeConfig.NetAdapters.Compute.Name | Write-ScriptLog $paramsForAdd = @{ VMName = $nodeConfig.VMName Name = $nodeConfig.NetAdapters.Compute.Name @@ -226,8 +243,10 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $nodeConfig.NetAdapters.Compute.Name | Write-ScriptLog # Storage 1 +'Configure the {0} network adapter.' -f $nodeConfig.NetAdapters.Storage1.Name | Write-ScriptLog $paramsForAdd = @{ VMName = $nodeConfig.VMName Name = $nodeConfig.NetAdapters.Storage1.Name @@ -242,8 +261,10 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $nodeConfig.NetAdapters.Storage1.Name | Write-ScriptLog # Storage 2 +'Configure the {0} network adapter.' -f $nodeConfig.NetAdapters.Storage2.Name | Write-ScriptLog $paramsForAdd = @{ VMName = $nodeConfig.VMName Name = $nodeConfig.NetAdapters.Storage2.Name @@ -258,8 +279,9 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $nodeConfig.NetAdapters.Storage2.Name | Write-ScriptLog -'Creating the data disks...' | Write-ScriptLog -Context $nodeConfig.VMName +'Create the data disks.' | Write-ScriptLog $diskCount = 8 for ($diskIndex = 1; $diskIndex -le $diskCount; $diskIndex++) { $params = @{ @@ -268,10 +290,11 @@ for ($diskIndex = 1; $diskIndex -le $diskCount; $diskIndex++) { SizeBytes = $nodeConfig.DataDiskSizeBytes } $vmDataDiskVhd = New-VHD @params - Add-VMHardDiskDrive -VMName $nodeConfig.VMName -Path $vmDataDiskVhd.Path -Passthru | Out-String | Write-ScriptLog -Context $vmName + Add-VMHardDiskDrive -VMName $nodeConfig.VMName -Path $vmDataDiskVhd.Path -Passthru | Out-String | Write-ScriptLog } +'Create the data disks completed.' | Write-ScriptLog -'Generating the unattend answer XML...' | Write-ScriptLog -Context $nodeConfig.VMName +'Generate the unattend answer XML.'| Write-ScriptLog $params = @{ ComputerName = $nodeConfig.VMName Password = $nodeConfig.AdminPassword @@ -279,63 +302,75 @@ $params = @{ TimeZone = $labConfig.guestOS.timeZone } $unattendAnswerFileContent = New-UnattendAnswerFileContent @params +'Generate the unattend answer XML completed.'| Write-ScriptLog -'Injecting the unattend answer file to the VHD...' | Write-ScriptLog -Context $nodeConfig.VMName +'Inject the unattend answer file to the VHD.' | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path UnattendAnswerFileContent = $unattendAnswerFileContent LogFolder = $labConfig.labHost.folderPath.log } Set-UnattendAnswerFileToVhd @params +'Inject the unattend answer file to the VHD completed.' | Write-ScriptLog -'Installing the roles and features to the VHD...' | Write-ScriptLog -Context $nodeConfig.VMName +'Install the roles and features to the VHD.' | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path FeatureName = Get-WindowsFeatureToInstall -HciNodeOperatingSystemSku $labConfig.hciNode.operatingSystem.sku LogFolder = $labConfig.labHost.folderPath.log } Install-WindowsFeatureToVhd @params +'Install the roles and features to the VHD completed' | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Start the VM.' | Write-ScriptLog Start-VMWithRetry -VMName $nodeConfig.VMName +'Start the VM completed.' | Write-ScriptLog -'Waiting for the VM to be ready...' | Write-ScriptLog -Context $nodeConfig.VMName +'Wait for the VM to be ready.' | Write-ScriptLog $localAdminCredential = New-LogonCredential -DomainFqdn '.' -Password $nodeConfig.AdminPassword Wait-PowerShellDirectReady -VMName $nodeConfig.VMName -Credential $localAdminCredential +'The VM is ready.' | Write-ScriptLog + +# Guest OS -'Create a PowerShell Direct session...' | Write-ScriptLog -Context $nodeConfig.VMName +'Create a PowerShell Direct session.' | Write-ScriptLog $localAdminCredPSSession = New-PSSession -VMName $nodeConfig.VMName -Credential $localAdminCredential -$localAdminCredPSSession | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName +$localAdminCredPSSession | Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create a PowerShell Direct session completed.' | Write-ScriptLog -'Copying the common module file into the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Copy the common module file into the VM.' | Write-ScriptLog $commonModuleFilePathInVM = Copy-PSModuleIntoVM -Session $localAdminCredPSSession -ModuleFilePathToCopy (Get-Module -Name 'common').Path +'Copy the common module file into the VM completed.' | Write-ScriptLog -'Setup the PowerShell Direct session...' | Write-ScriptLog -Context $nodeConfig.VMName +'Setup the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $localAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session completed.' | Write-ScriptLog # If the HCI node OS is Windows Server 2022 with Desktop Experience. if (($NodeConfig.OperatingSystem -eq [HciLab.OSSku]::WindowsServer2022) -and ($NodeConfig.ImageIndex -eq [HciLab.OSImageIndex]::WSDatacenterDesktopExperience)) { - 'Configuring registry values within the VM...' | Write-ScriptLog -Context $nodeConfig.VMName + 'Configure registry values within the VM.' | Write-ScriptLog Invoke-Command -Session $localAdminCredPSSession -ScriptBlock { - 'Stop Server Manager launch at logon.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Stop Server Manager launch at logon.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotOpenServerManagerAtLogon' -Value 1 + 'Stop Server Manager launch at logon completed.' | Write-ScriptLog - 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotPopWACConsoleAtSMLaunch' -Value 1 + 'Stop Windows Admin Center popup at Server Manager launch completed.' | Write-ScriptLog - 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Network' -KeyName 'NewNetworkWindowOff' + 'Hide the Network Location wizard completed.' | Write-ScriptLog - 'Setting to hide the first run experience of Microsoft Edge.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Hide the first run experience of Microsoft Edge.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SOFTWARE\Policies\Microsoft' -KeyName 'Edge' Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' -Name 'HideFirstRunExperience' -Value 1 - } | Out-String | Write-ScriptLog -Context $nodeConfig.VMName + 'Hide the first run experience of Microsoft Edge completed.' | Write-ScriptLog + } | Out-String | Write-ScriptLog } +'Configure registry values within the VM completed.' | Write-ScriptLog -'Configuring network settings within the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Configure network settings within the VM.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ VMConfig = $NodeConfig @@ -347,13 +382,14 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { [PSCustomObject] $VMConfig ) - 'Renaming the network adapters...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Rename the network adapters.' | Write-ScriptLog Get-NetAdapterAdvancedProperty -RegistryKeyword 'HyperVNetworkAdapterName' | ForEach-Object -Process { Rename-NetAdapter -Name $_.Name -NewName $_.DisplayValue } + 'Rename the network adapters completed.' | Write-ScriptLog # Management - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog $paramsForSetNetIPInterface = @{ AddressFamily = 'IPv4' Dhcp = 'Disabled' @@ -371,11 +407,12 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Get-NetAdapter -Name $VMConfig.NetAdapters.Management.Name | Set-NetIPInterface @paramsForSetNetIPInterface | New-NetIPAddress @paramsForNewIPAddress | - Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog # Compute - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.NetAdapters.Compute.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.NetAdapters.Compute.Name | Write-ScriptLog $paramsForSetNetIPInterface = @{ AddressFamily = 'IPv4' Dhcp = 'Disabled' @@ -388,11 +425,12 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { } Get-NetAdapter -Name $VMConfig.NetAdapters.Compute.Name | Set-NetIPInterface @paramsForSetNetIPInterface | - New-NetIPAddress @paramsForNewIPAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.NetAdapters.Compute.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + New-NetIPAddress @paramsForNewIPAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.NetAdapters.Compute.Name | Write-ScriptLog # Storage 1 - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.NetAdapters.Storage1.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.NetAdapters.Storage1.Name | Write-ScriptLog $paramsForSetNetAdapter = @{ VlanID = $VMConfig.NetAdapters.Storage1.VlanId Confirm = $false @@ -411,11 +449,12 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Get-NetAdapter -Name $VMConfig.NetAdapters.Storage1.Name | Set-NetAdapter @paramsForSetNetAdapter | Set-NetIPInterface @paramsForSetNetIPInterface | - New-NetIPAddress @paramsForNewIPAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.NetAdapters.Storage1.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + New-NetIPAddress @paramsForNewIPAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.NetAdapters.Storage1.Name | Write-ScriptLog # Storage 2 - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.NetAdapters.Storage2.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.NetAdapters.Storage2.Name | Write-ScriptLog $paramsForSetNetAdapter = @{ VlanID = $VMConfig.NetAdapters.Storage2.VlanId Confirm = $false @@ -434,19 +473,42 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Get-NetAdapter -Name $VMConfig.NetAdapters.Storage2.Name | Set-NetAdapter @paramsForSetNetAdapter | Set-NetIPInterface @paramsForSetNetIPInterface | - New-NetIPAddress @paramsForNewIPAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.NetAdapters.Storage2.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock - -} | Out-String | Write-ScriptLog -Context $nodeConfig.VMName - -'Cleaning up the PowerShell Direct session...' | Write-ScriptLog -Context $nodeConfig.VMName + New-NetIPAddress @paramsForNewIPAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.NetAdapters.Storage2.Name | Write-ScriptLog + + 'Network adapter IP configurations:' | Write-ScriptLog + Get-NetIPAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + 'AddressFamily', + 'IPAddress', + 'PrefixLength', + 'PrefixOrigin', + 'SuffixOrigin', + 'AddressState', + 'Store' + ) | Out-String -Width 200 | Write-ScriptLog + + 'Network adapter DNS configurations:' | Write-ScriptLog + Get-DnsClientServerAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + @{ Label = 'AddressFamily'; Expression = { Switch ($_.AddressFamily) { 2 { 'IPv4' } 23 { 'IPv6' } default { $_.AddressFamily } } } } + @{ Label = 'DNSServers'; Expression = { $_.ServerAddresses } } + ) | Out-String -Width 200 | Write-ScriptLog +} | Out-String | Write-ScriptLog + +'Clean up the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionCleanup -Session $localAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Clean up the PowerShell Direct session completed.' | Write-ScriptLog if ($labConfig.hciNode.shouldJoinToAddsDomain) { - 'Waiting for the domain controller to complete deployment...' | Write-ScriptLog -Context $nodeConfig.VMName + 'Wait for the domain controller to complete deployment.' | Write-ScriptLog Wait-AddsDcDeploymentCompletion + 'The domain controller deployment completed.' | Write-ScriptLog - 'Waiting for the domain controller to be ready...' | Write-ScriptLog -Context $nodeConfig.VMName + 'Wait for the domain controller to be ready.' | Write-ScriptLog $domainAdminCredential = New-LogonCredential -DomainFqdn $labConfig.addsDomain.fqdn -Password $nodeConfig.AdminPassword # The DC's computer name is the same as the VM name. It's specified in the unattend.xml. $params = @{ @@ -455,8 +517,9 @@ if ($labConfig.hciNode.shouldJoinToAddsDomain) { Credential = $domainAdminCredential } Wait-DomainControllerServiceReady @params + 'The domain controller is ready.' | Write-ScriptLog - 'Joining the VM to the AD domain...' | Write-ScriptLog -Context $nodeConfig.VMName + 'Join the VM to the AD domain.' | Write-ScriptLog $params = @{ VMName = $nodeConfig.VMName LocalAdminCredential = $localAdminCredential @@ -464,15 +527,18 @@ if ($labConfig.hciNode.shouldJoinToAddsDomain) { DomainAdminCredential = $domainAdminCredential } Add-VMToADDomain @params + 'Join the VM to the AD domain completed.' | Write-ScriptLog } -'Stopping the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Stop the VM.' | Write-ScriptLog Stop-VM -Name $nodeConfig.VMName +'Stop the VM completed.' | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $nodeConfig.VMName +'Start the VM.' | Write-ScriptLog Start-VM -Name $nodeConfig.VMName +'Start the VM completed.' | Write-ScriptLog -'Waiting for the VM to be ready...' | Write-ScriptLog -Context $nodeConfig.VMName +'Wait for the VM to be ready.' | Write-ScriptLog $credentialForWaiting = if ($labConfig.hciNode.shouldJoinToAddsDomain) { New-LogonCredential -DomainFqdn $labConfig.addsDomain.fqdn -Password $nodeConfig.AdminPassword } @@ -480,7 +546,7 @@ else { $localAdminCredential } Wait-PowerShellDirectReady -VMName $nodeConfig.VMName -Credential $credentialForWaiting +'The VM is ready.' | Write-ScriptLog -'The HCI node VM creation has been completed.' | Write-ScriptLog -Context $nodeConfig.VMName - +'The HCI node VM creation has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-vm-job-wac.ps1 b/template/customscripts/create-vm-job-wac.ps1 index d217fc4..df755d6 100644 --- a/template/customscripts/create-vm-job-wac.ps1 +++ b/template/customscripts/create-vm-job-wac.ps1 @@ -16,7 +16,10 @@ Import-Module -Name $PSModuleNameToImport -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log -FileName $LogFileName -$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog -Context $env:ComputerName +Set-ScriptLogDefaultContext -LogContext $labConfig.wac.vmName + +'Lab deployment config:' | Write-ScriptLog +$labConfig | ConvertTo-Json -Depth 16 | Out-String | Write-ScriptLog function Invoke-WindowsAdminCenterInstallerDownload { @@ -63,9 +66,9 @@ function New-CertificateForWindowsAdminCenter return $wacCret } -$vmName = $labConfig.wac.vmName +# Hyper-V VM -'Creating the OS disk for the VM...' | Write-ScriptLog -Context $vmName +'Create the OS disk for the VM.' | Write-ScriptLog $params = @{ OperatingSystem = [HciLab.OSSku]::WindowsServer2022 ImageIndex = [HciLab.OSImageIndex]::WSDatacenterDesktopExperience # Datacenter with Desktop Experience @@ -75,40 +78,45 @@ $parentVhdFileName = Format-BaseVhdFileName @params $params = @{ Differencing = $true ParentPath = [IO.Path]::Combine($labConfig.labHost.folderPath.vhd, $parentVhdFileName) - Path = [IO.Path]::Combine($labConfig.labHost.folderPath.vm, $vmName, 'osdisk.vhdx') + Path = [IO.Path]::Combine($labConfig.labHost.folderPath.vm, $labConfig.wac.vmName, 'osdisk.vhdx') } $vmOSDiskVhd = New-VHD @params +'Create the OS disk for the VM completed.' | Write-ScriptLog -'Creating the VM...' | Write-ScriptLog -Context $vmName +'Create the VM.' | Write-ScriptLog $params = @{ - Name = $vmName + Name = $labConfig.wac.vmName Path = $labConfig.labHost.folderPath.vm VHDPath = $vmOSDiskVhd.Path Generation = 2 } -New-VM @params | Out-String | Write-ScriptLog -Context $vmName +New-VM @params | Out-String | Write-ScriptLog +'Create the VM completed.' | Write-ScriptLog -'Changing the VM''s automatic stop action...' | Write-ScriptLog -Context $vmName -Set-VM -Name $vmName -AutomaticStopAction ShutDown +'Change the VM''s automatic stop action.' | Write-ScriptLog +Set-VM -Name $labConfig.wac.vmName -AutomaticStopAction ShutDown +'Change the VM''s automatic stop action completed' | Write-ScriptLog -'Setting the VM''s processor configuration...' | Write-ScriptLog -Context $vmName +'Configure the VM''s processor.' | Write-ScriptLog $vmProcessorCount = 6 if ((Get-VMHost).LogicalProcessorCount -lt $vmProcessorCount) { $vmProcessorCount = (Get-VMHost).LogicalProcessorCount } -Set-VMProcessor -VMName $vmName -Count $vmProcessorCount +Set-VMProcessor -VMName $labConfig.wac.vmName -Count $vmProcessorCount +'Configure the VM''s processor completed.' | Write-ScriptLog -'Setting the VM''s memory configuration...' | Write-ScriptLog -Context $vmName +'Configure the VM''s memory.' | Write-ScriptLog $params = @{ - VMName = $vmName + VMName = $labConfig.wac.vmName StartupBytes = 1GB DynamicMemoryEnabled = $true MinimumBytes = 512MB MaximumBytes = $labConfig.wac.maximumRamBytes } Set-VMMemory @params +'Configure the VM''s memory completed.' | Write-ScriptLog -'Enabling vTPM...' | Write-ScriptLog -Context $vmName +'Enable the VM''s vTPM.' | Write-ScriptLog $params = @{ - VMName = $vmName + VMName = $labConfig.wac.vmName NewLocalKeyProtector = $true Passthru = $true ErrorAction = [Management.Automation.ActionPreference]::Stop @@ -117,23 +125,27 @@ try { Set-VMKeyProtector @params | Enable-VMTPM } catch { - ( - 'Caught exception on enable vTPM, will retry to enable vTPM... ' + - '(ExceptionMessage: {0} | Exception: {1} | FullyQualifiedErrorId: {2} | CategoryInfo: {3} | ErrorDetailsMessage: {4})' - ) -f @( - $_.Exception.Message, $_.Exception.GetType().FullName, $_.FullyQualifiedErrorId, $_.CategoryInfo.ToString(), $_.ErrorDetails.Message - ) | Write-ScriptLog -Context $vmName + '{0} (ExceptionMessage: {1} | Exception: {2} | FullyQualifiedErrorId: {3} | CategoryInfo: {4} | ErrorDetailsMessage: {5})' -f @( + 'Caught exception on enable vTPM, will retry to enable vTPM.', + $_.Exception.Message, + $_.Exception.GetType().FullName, + $_.FullyQualifiedErrorId, + $_.CategoryInfo.ToString(), + $_.ErrorDetails.Message + ) | Write-ScriptLog -Level Warning # Rescue only once by retry. Set-VMKeyProtector @params | Enable-VMTPM } +'Enable the VM''s vTPM completed.' | Write-ScriptLog -'Setting the VM''s network adapter configuration...' | Write-ScriptLog -Context $vmName -Get-VMNetworkAdapter -VMName $vmName | Remove-VMNetworkAdapter +'Configure the VM''s network adapters.' | Write-ScriptLog +Get-VMNetworkAdapter -VMName $labConfig.wac.vmName | Remove-VMNetworkAdapter # Management +'Configure the {0} network adapter.' -f $labConfig.wac.netAdapters.management.name | Write-ScriptLog $paramsForAdd = @{ - VMName = $vmName + VMName = $labConfig.wac.vmName Name = $labConfig.wac.netAdapters.management.name SwitchName = $labConfig.labHost.vSwitch.nat.name DeviceNaming = [Microsoft.HyperV.PowerShell.OnOffState]::On @@ -147,26 +159,29 @@ $paramsForSet = @{ Add-VMNetworkAdapter @paramsForAdd | Set-VMNetworkAdapter @paramsForSet | Set-VMNetworkAdapterVlan -Trunk -NativeVlanId 0 -AllowedVlanIdList '1-4094' +'Configure the {0} network adapter completed.' -f $labConfig.wac.netAdapters.management.name | Write-ScriptLog -'Generating the unattend answer XML...' | Write-ScriptLog -Context $vmName +'Generate the unattend answer XML.' | Write-ScriptLog $adminPassword = Get-Secret -KeyVaultName $labConfig.keyVault.name -SecretName $labConfig.keyVault.secretName.adminPassword $params = @{ - ComputerName = $vmName + ComputerName = $labConfig.wac.vmName Password = $adminPassword Culture = $labConfig.guestOS.culture TimeZone = $labConfig.guestOS.timeZone } $unattendAnswerFileContent = New-UnattendAnswerFileContent @params +'Generate the unattend answer XML completed.' | Write-ScriptLog -'Injecting the unattend answer file to the VM...' | Write-ScriptLog -Context $vmName +'Inject the unattend answer file to the "{0}".' -f $vmOSDiskVhd.Path | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path UnattendAnswerFileContent = $unattendAnswerFileContent LogFolder = $labConfig.labHost.folderPath.log } Set-UnattendAnswerFileToVhd @params +'Inject the unattend answer file to the "{0}" completed.' -f $vmOSDiskVhd.Path | Write-ScriptLog -'Installing the roles and features to the VHD...' | Write-ScriptLog -Context $vmName +'Install the roles and features to the "{0}".' -f $vmOSDiskVhd.Path | Write-ScriptLog $params = @{ VhdPath = $vmOSDiskVhd.Path FeatureName = @( @@ -184,44 +199,54 @@ $params = @{ LogFolder = $labConfig.labHost.folderPath.log } Install-WindowsFeatureToVhd @params +'Install the roles and features to the "{0}" completed.' -f $vmOSDiskVhd.Path | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $vmName -Start-VMWithRetry -VMName $vmName +'Start the VM.' | Write-ScriptLog +Start-VMWithRetry -VMName $labConfig.wac.vmName +'Start the VM completed.' | Write-ScriptLog -'Waiting for the VM to be ready...' | Write-ScriptLog -Context $vmName +'Wait for the VM to be ready.' | Write-ScriptLog $localAdminCredential = New-LogonCredential -DomainFqdn '.' -Password $adminPassword -Wait-PowerShellDirectReady -VMName $vmName -Credential $localAdminCredential +Wait-PowerShellDirectReady -VMName $labConfig.wac.vmName -Credential $localAdminCredential +'The VM is ready.' | Write-ScriptLog + +# Guest OS -'Create a PowerShell Direct session...' | Write-ScriptLog -Context $vmName -$localAdminCredPSSession = New-PSSession -VMName $vmName -Credential $localAdminCredential -$localAdminCredPSSession | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName +'Create a PowerShell Direct session.' | Write-ScriptLog +$localAdminCredPSSession = New-PSSession -VMName $labConfig.wac.vmName -Credential $localAdminCredential +$localAdminCredPSSession | Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create a PowerShell Direct session completed.' | Write-ScriptLog -'Copying the common module file into the VM...' | Write-ScriptLog -Context $vmName +'Copy the common module file into the VM.' | Write-ScriptLog $commonModuleFilePathInVM = Copy-PSModuleIntoVM -Session $localAdminCredPSSession -ModuleFilePathToCopy (Get-Module -Name 'common').Path +'Copy the common module file into the VM completed.' | Write-ScriptLog -'Setup the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Setup the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $localAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session completed.' | Write-ScriptLog -'Configuring registry values within the VM...' | Write-ScriptLog -Context $vmName +'Configure registry values within the VM.' | Write-ScriptLog Invoke-Command -Session $localAdminCredPSSession -ScriptBlock { - 'Stop Server Manager launch at logon.' | Write-ScriptLog -Context $env:ComputerName-UseInScriptBlock + 'Stop Server Manager launch at logon.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotOpenServerManagerAtLogon' -Value 1 + 'Stop Server Manager launch at logon completed.' | Write-ScriptLog - 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Stop Windows Admin Center popup at Server Manager launch.' | Write-ScriptLog Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name 'DoNotPopWACConsoleAtSMLaunch' -Value 1 + 'Stop Windows Admin Center popup at Server Manager launch completed.' | Write-ScriptLog - 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Hide the Network Location wizard. All networks will be Public.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Network' -KeyName 'NewNetworkWindowOff' + 'Hide the Network Location wizard completed.' | Write-ScriptLog - 'Setting to hide the first run experience of Microsoft Edge.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Hide the first run experience of Microsoft Edge.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SOFTWARE\Policies\Microsoft' -KeyName 'Edge' Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' -Name 'HideFirstRunExperience' -Value 1 -} | Out-String | Write-ScriptLog -Context $vmName + 'Hide the first run experience of Microsoft Edge completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Configure registry values within the VM completed.' | Write-ScriptLog -'Configuring network settings within the VM...' | Write-ScriptLog -Context $vmName +'Configure network settings within the VM.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ VMConfig = $LabConfig.wac @@ -233,13 +258,14 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { [PSCustomObject] $VMConfig ) - 'Renaming the network adapters...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Rename the network adapters.' | Write-ScriptLog Get-NetAdapterAdvancedProperty -RegistryKeyword 'HyperVNetworkAdapterName' | ForEach-Object -Process { Rename-NetAdapter -Name $_.Name -NewName $_.DisplayValue } + 'Rename the network adapters completed.' | Write-ScriptLog # Management - 'Setting the IP & DNS configuration on the {0} network adapter...' -f $VMConfig.netAdapters.management.name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Configure the IP & DNS on the "{0}" network adapter.' -f $VMConfig.netAdapters.management.name | Write-ScriptLog $paramsForSetNetIPInterface = @{ AddressFamily = 'IPv4' Dhcp = 'Disabled' @@ -257,32 +283,60 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Get-NetAdapter -Name $VMConfig.netAdapters.management.name | Set-NetIPInterface @paramsForSetNetIPInterface | New-NetIPAddress @paramsForNewIPAddress | - Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress - 'The IP & DNS configuration on the {0} network adapter is completed.' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock - -} | Out-String | Write-ScriptLog -Context $vmName - -'Downloading the Windows Admin Center installer...' | Write-ScriptLog -Context $vmName + Set-DnsClientServerAddress @paramsForSetDnsClientServerAddress | + Out-Null + 'Configure the IP & DNS on the "{0}" network adapter completed.' -f $VMConfig.NetAdapters.Management.Name | Write-ScriptLog + + 'Network adapter IP configurations:' | Write-ScriptLog + Get-NetIPAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + 'AddressFamily', + 'IPAddress', + 'PrefixLength', + 'PrefixOrigin', + 'SuffixOrigin', + 'AddressState', + 'Store' + ) | Out-String -Width 200 | Write-ScriptLog + + 'Network adapter DNS configurations:' | Write-ScriptLog + Get-DnsClientServerAddress | Format-Table -Property @( + 'InterfaceIndex', + 'InterfaceAlias', + @{ Label = 'AddressFamily'; Expression = { Switch ($_.AddressFamily) { 2 { 'IPv4' } 23 { 'IPv6' } default { $_.AddressFamily } } } } + @{ Label = 'DNSServers'; Expression = { $_.ServerAddresses } } + ) | Out-String -Width 200 | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Configure network settings within the VM completed.' | Write-ScriptLog + +# Windows Admin Center + +'Donwload the Windows Admin Center installer.' | Write-ScriptLog $wacInstallerFile = Invoke-WindowsAdminCenterInstallerDownload -DownloadFolderPath $labConfig.labHost.folderPath.temp -$wacInstallerFile | Out-String | Write-ScriptLog -Context $vmName +$wacInstallerFile | Out-String | Write-ScriptLog +'Donwload the Windows Admin Center installer completed.' | Write-ScriptLog -'Creating a new SSL server authentication certificate for Windows Admin Center...' | Write-ScriptLog -Context $vmName -$wacCret = New-CertificateForWindowsAdminCenter -VMName $vmName +'Create a new SSL server authentication certificate for Windows Admin Center.' | Write-ScriptLog +$wacCret = New-CertificateForWindowsAdminCenter -VMName $labConfig.wac.vmName +'Create a new SSL server authentication certificate for Windows Admin Center completed.' | Write-ScriptLog -'Exporting the Windows Admin Center certificate...' | Write-ScriptLog -Context $vmName +'Export the Windows Admin Center certificate.' | Write-ScriptLog $wacPfxFilePathOnLabHost = [IO.Path]::Combine($labConfig.labHost.folderPath.temp, 'wac.pfx') -$wacCret | Export-PfxCertificate -FilePath $wacPfxFilePathOnLabHost -Password $adminPassword | Out-String | Write-ScriptLog -Context $vmName +$wacCret | Export-PfxCertificate -FilePath $wacPfxFilePathOnLabHost -Password $adminPassword | Out-String | Write-ScriptLog +'Export the Windows Admin Center certificate completed.' | Write-ScriptLog -# Copy the Windows Admin Center related files into the VM. -'Copying the Windows Admin Center installer into the VM...' | Write-ScriptLog -Context $vmName +'Copy the Windows Admin Center installer into the VM.' | Write-ScriptLog $wacInstallerFilePathInVM = [IO.Path]::Combine('C:\Windows\Temp', [IO.Path]::GetFileName($wacInstallerFile.FullName)) Copy-Item -ToSession $localAdminCredPSSession -Path $wacInstallerFile.FullName -Destination $wacInstallerFilePathInVM +'Copy the Windows Admin Center installer into the VM completed.' | Write-ScriptLog -'Copying the Windows Admin Center certificate into the VM...' | Write-ScriptLog -Context $vmName +'Copy the Windows Admin Center certificate into the VM.' | Write-ScriptLog $wacPfxFilePathInVM = [IO.Path]::Combine('C:\Windows\Temp', [IO.Path]::GetFileName($wacPfxFilePathOnLabHost)) Copy-Item -ToSession $localAdminCredPSSession -Path $wacPfxFilePathOnLabHost -Destination $wacPfxFilePathInVM +'Copy the Windows Admin Center certificate into the VM completed.' | Write-ScriptLog -'Installing Windows Admin Center within the VM...' | Write-ScriptLog -Context $vmName +'Install Windows Admin Center within the VM.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ WacInstallerFilePathInVM = $wacInstallerFilePathInVM @@ -302,50 +356,81 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { [SecureString] $WacPfxPassword ) - # Import the certificate to Root and My both stores required. - 'Importing Windows Admin Center certificate...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock - Import-PfxCertificate -CertStoreLocation 'Cert:\LocalMachine\Root' -FilePath $WacPfxFilePathInVM -Password $WacPfxPassword -Exportable - $wacCert = Import-PfxCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -FilePath $WacPfxFilePathInVM -Password $WacPfxPassword -Exportable + # NOTE: Import the certificate to Root and My both stores required. + 'Import the Windows Admin Center certificate to the Root store.' | Write-ScriptLog + $wacCertRootStore = Import-PfxCertificate -CertStoreLocation 'Cert:\LocalMachine\Root' -FilePath $WacPfxFilePathInVM -Password $WacPfxPassword -Exportable + $wacCertRootStore | Format-Table -Property 'Thumbprint', 'Subject' | Out-String | Write-ScriptLog + 'Import the Windows Admin Center certificate to the Root store completed.' | Write-ScriptLog + + 'Import the Windows Admin Center certificate to the My store.' | Write-ScriptLog + $wacCertMyStore = Import-PfxCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -FilePath $WacPfxFilePathInVM -Password $WacPfxPassword -Exportable + $wacCertMyStore | Format-Table -Property 'Thumbprint', 'Subject' | Out-String | Write-ScriptLog + 'Import the Windows Admin Center certificate to the My store completed.' | Write-ScriptLog + + 'Delete the Windows Admin Center certificate.' | Write-ScriptLog Remove-Item -LiteralPath $WacPfxFilePathInVM -Force + 'Delete the Windows Admin Center certificate completed.' | Write-ScriptLog - 'Installing Windows Admin Center...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Install Windows Admin Center.' | Write-ScriptLog $msiArgs = @( '/i', ('"{0}"' -f $WacInstallerFilePathInVM), '/qn', '/L*v', - '"C:\Windows\Temp\wac-install-log.txt"', + '"C:\Windows\Temp\wac-install-log.log"', 'SME_PORT=443', - ('SME_THUMBPRINT={0}' -f $wacCert.Thumbprint), + ('SME_THUMBPRINT={0}' -f $wacCertMyStore.Thumbprint), 'SSL_CERTIFICATE_OPTION=installed' #'SSL_CERTIFICATE_OPTION=generate' ) $result = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru - $result | Format-List -Property '*' + $result | Format-List -Property @( + @{ Label = 'FileName'; Expression = { $_.StartInfo.FileName } }, + @{ Label = 'Arguments'; Expression = { $_.StartInfo.Arguments } }, + @{ Label = 'WorkingDirectory'; Expression = { $_.StartInfo.WorkingDirectory } }, + 'Id', + 'HasExited', + 'ExitCode', + 'StartTime', + 'ExitTime', + 'TotalProcessorTime', + 'PrivilegedProcessorTime', + 'UserProcessorTime' + ) | Out-String | Write-ScriptLog if ($result.ExitCode -ne 0) { - throw ('Windows Admin Center installation failed with exit code {0}.' -f $result.ExitCode) + $exceptionMessage = 'Windows Admin Center installation failed with exit code {0}.' -f $result.ExitCode + $exceptionMessage | Write-ScriptLog -Level Error + throw $exceptionMessage } + 'Install Windows Admin Center completed.' | Write-ScriptLog + + 'Delete the Windows Admin Center installer.' | Write-ScriptLog Remove-Item -LiteralPath $WacInstallerFilePathInVM -Force + 'Delete the Windows Admin Center installer completed.' | Write-ScriptLog + 'Wait for the ServerManagementGateway service to be ready.' | Write-ScriptLog &{ $wacConnectionTestTimeout = (New-TimeSpan -Minutes 5) $wacConnectionTestIntervalSeconds = 5 $startTime = Get-Date while ((Get-Date) -lt ($startTime + $wacConnectionTestTimeout)) { - 'Testing connection to the ServerManagementGateway service...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Test connection to the ServerManagementGateway service.' | Write-ScriptLog if ((Test-NetConnection -ComputerName 'localhost' -Port 443).TcpTestSucceeded) { - 'Connection test to the ServerManagementGateway service succeeded.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Connection test to the ServerManagementGateway service succeeded.' | Write-ScriptLog return } Start-Sleep -Seconds $wacConnectionTestIntervalSeconds } - 'Connection test to the ServerManagementGateway service failed.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Connection test to the ServerManagementGateway service failed.' | Write-ScriptLog -Level Warning } + 'The ServerManagementGateway service is ready.' | Write-ScriptLog - 'Updating Windows Admin Center extensions...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Import the ExtensionTools module.' | Write-ScriptLog $wacExtensionToolsPSModulePath = [IO.Path]::Combine($env:ProgramFiles, 'Windows Admin Center\PowerShell\Modules\ExtensionTools\ExtensionTools.psm1') Import-Module -Name $wacExtensionToolsPSModulePath -Force + 'Import the ExtensionTools module completed.' | Write-ScriptLog + 'Update the Windows Admin Center extensions.' | Write-ScriptLog &{ $retryLimit = 50 $retryInterval = 15 @@ -360,26 +445,30 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { $wacExtension = $_ Update-Extension -GatewayEndpoint $gatewayEndpointUri -ExtensionId $wacExtension.id -Verbose -ErrorAction Stop | Out-Null } - 'Windows Admin Center extension update succeeded.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Windows Admin Center extension update succeeded.' | Write-ScriptLog + 'Windows Admin Center extension status:' | Write-ScriptLog Get-Extension -GatewayEndpoint $gatewayEndpointUri | Sort-Object -Property id | - Format-table -Property id, status, version, isLatestVersion, title + Format-table -Property id, status, version, isLatestVersion, title | + Out-String | Write-ScriptLog return } catch { - 'Will retry updating Windows Admin Center extensions...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Retry updating the Windows Admin Center extensions.' | Write-ScriptLog -Level Warning Start-Sleep -Seconds $retryInterval } } - 'Windows Admin Center extension update failed. Need manual update later.' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Windows Admin Center extension update failed. Need manual update later.' | Write-ScriptLog -Level Warning } + 'Update the Windows Admin Center extensions completed.' | Write-ScriptLog - 'Setting Windows Integrated Authentication registry for Windows Admin Center...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Set the Windows Integrated Authentication registry for Windows Admin Center.' | Write-ScriptLog New-RegistryKey -ParentPath 'HKLM:\SOFTWARE\Policies\Microsoft' -KeyName 'Edge' Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' -Name 'AuthServerAllowlist' -Value $env:ComputerName + 'Set the Windows Integrated Authentication registry for Windows Admin Center completed.' | Write-ScriptLog - 'Creating shortcut for Windows Admin Center on the desktop...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Create the shortcut for Windows Admin Center on the desktop.' | Write-ScriptLog $params = @{ ShortcutFilePath = 'C:\Users\Public\Desktop\Windows Admin Center.lnk' TargetPath = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' @@ -388,13 +477,14 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { IconLocation = 'imageres.dll,1' } New-ShortcutFile @params -} | Out-String | Write-ScriptLog -Context $vmName + 'Create the shortcut for Windows Admin Center on the desktop completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Install Windows Admin Center within the VM completed.' | Write-ScriptLog -$firstHciNodeName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index 0 -'Creating a shortcut for Remote Desktop connection to the {0} VM on the desktop...' -f $firstHciNodeName | Write-ScriptLog -Context $vmName +'Create a new shortcut on the desktop for connecting to the first HCI node using Remote Desktop connection.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ - FirstHciNodeName = $firstHciNodeName + FirstHciNodeName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index 0 } } Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { @@ -410,16 +500,19 @@ Invoke-Command @params -Session $localAdminCredPSSession -ScriptBlock { Description = 'Make a remote desktop connection to the member node "{0}" VM of the HCI cluster in your lab environment.' -f $FirstHciNodeName } New-ShortcutFile @params -} | Out-String | Write-ScriptLog -Context $vmName +} | Out-String | Write-ScriptLog +'Create a new shortcut on the desktop for connecting to the first HCI node using Remote Desktop connection completed.' | Write-ScriptLog -'Cleaning up the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Clean up the PowerShell Direct session.' | Write-ScriptLog # NOTE: The common module not be deleted within the VM at this time because it will be used afterwards. $localAdminCredPSSession | Remove-PSSession +'Clean up the PowerShell Direct session completed.' | Write-ScriptLog -'Waiting for the domain controller to complete deployment...' | Write-ScriptLog -Context $vmName +'Wait for the domain controller to complete deployment.' | Write-ScriptLog Wait-AddsDcDeploymentCompletion +'The domain controller deployment completed.' | Write-ScriptLog -'Waiting for the domain controller to be ready...' | Write-ScriptLog -Context $vmName +'Wait for the domain controller to be ready.' | Write-ScriptLog $domainAdminCredential = New-LogonCredential -DomainFqdn $labConfig.addsDomain.fqdn -Password $adminPassword $params = @{ AddsDcVMName = $labConfig.addsDC.vmName @@ -427,38 +520,42 @@ $params = @{ Credential = $domainAdminCredential } Wait-DomainControllerServiceReady @params +'The domain controller is ready.' | Write-ScriptLog -'Joining the VM to the AD domain...' | Write-ScriptLog -Context $vmName +'Join the VM to the AD domain.' | Write-ScriptLog $params = @{ - VMName = $vmName + VMName = $labConfig.wac.vmName LocalAdminCredential = $localAdminCredential DomainFqdn = $labConfig.addsDomain.fqdn DomainAdminCredential = $domainAdminCredential } Add-VMToADDomain @params +'Join the VM to the AD domain completed.' | Write-ScriptLog -'Stopping the VM...' | Write-ScriptLog -Context $vmName -Stop-VM -Name $vmName +'Stop the VM.' | Write-ScriptLog +Stop-VM -Name $labConfig.wac.vmName +'Stop the VM completed.' | Write-ScriptLog -'Starting the VM...' | Write-ScriptLog -Context $vmName -Start-VM -Name $vmName +'Start the VM.' | Write-ScriptLog +Start-VM -Name $labConfig.wac.vmName +'Start the VM completed.' | Write-ScriptLog -'Waiting for the VM to be ready...' | Write-ScriptLog -Context $vmName -Wait-PowerShellDirectReady -VMName $vmName -Credential $domainAdminCredential +'Wait for the VM to be ready.' | Write-ScriptLog +Wait-PowerShellDirectReady -VMName $labConfig.wac.vmName -Credential $domainAdminCredential +'The VM is ready.' | Write-ScriptLog -'Create a PowerShell Direct session with the domain credential...' | Write-ScriptLog -Context $vmName -$domainAdminCredPSSession = New-PSSession -VMName $vmName -Credential $domainAdminCredential -$domainAdminCredPSSession | - Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | - Out-String | - Write-ScriptLog -Context $env:ComputerName +'Create a PowerShell Direct session with the domain credential.' | Write-ScriptLog +$domainAdminCredPSSession = New-PSSession -VMName $labConfig.wac.vmName -Credential $domainAdminCredential +$domainAdminCredPSSession | Format-Table -Property 'Id', 'Name', 'ComputerName', 'ComputerType', 'State', 'Availability' | Out-String | Write-ScriptLog +'Create a PowerShell Direct session with the domain credential completed.' | Write-ScriptLog -'Setup the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Setup the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionSetup -Session $domainAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Setup the PowerShell Direct session completed.' | Write-ScriptLog # NOTE: To preset WAC connections for the domain Administrator, the preset operation is required by # the domain Administrator because WAC connections are managed based on each user. -'Configuring Windows Admin Center for the domain Administrator...' | Write-ScriptLog -Context $vmName +'Configure Windows Admin Center for the domain Administrator.' | Write-ScriptLog $params = @{ InputObject = [PSCustomObject] @{ LabConfig = $LabConfig @@ -470,9 +567,10 @@ Invoke-Command @params -Session $domainAdminCredPSSession -ScriptBlock { [PSCustomObject] $LabConfig ) - 'Importing server connections to Windows Admin Center for the domain Administrator...' | Write-ScriptLog -Context $env:ComputerName -UseInScriptBlock + 'Import the ConnectionTools module.' | Write-ScriptLog $wacConnectionToolsPSModulePath = [IO.Path]::Combine($env:ProgramFiles, 'Windows Admin Center\PowerShell\Modules\ConnectionTools\ConnectionTools.psm1') Import-Module -Name $wacConnectionToolsPSModulePath -Force + 'Import the ConnectionTools module completed.' | Write-ScriptLog # Create a connection entry list to import to Windows Admin Center. $connectionEntries = @( @@ -501,19 +599,25 @@ Invoke-Command @params -Session $domainAdminCredPSSession -ScriptBlock { } } - # Create a connection list file to import to Windows Admin Center. + 'Create a connection list file to import to Windows Admin Center.' | Write-ScriptLog $wacConnectionFilePathInVM = [IO.Path]::Combine('C:\Windows\Temp', 'wac-connections.txt') New-WacConnectionFileContent -ConnectionEntry $connectionEntries | Set-Content -LiteralPath $wacConnectionFilePathInVM -Force + 'Create a connection list file to import to Windows Admin Center completed.' | Write-ScriptLog - # Import connections to Windows Admin Center. + 'Import server connections to Windows Admin Center for the domain Administrator.' | Write-ScriptLog [Uri] $gatewayEndpointUri = 'https://{0}' -f $env:ComputerName Import-Connection -GatewayEndpoint $gatewayEndpointUri -FileName $wacConnectionFilePathInVM + 'Import server connections to Windows Admin Center for the domain Administrator completed.' | Write-ScriptLog + + 'Delete the Windows Admin Center connection list file.' | Write-ScriptLog Remove-Item -LiteralPath $wacConnectionFilePathInVM -Force -} | Out-String | Write-ScriptLog -Context $vmName + 'Delete the Windows Admin Center connection list file completed.' | Write-ScriptLog +} | Out-String | Write-ScriptLog +'Configure Windows Admin Center for the domain Administrator completed.' | Write-ScriptLog -'Cleaning up the PowerShell Direct session...' | Write-ScriptLog -Context $vmName +'Clean up the PowerShell Direct session.' | Write-ScriptLog Invoke-PSDirectSessionCleanup -Session $domainAdminCredPSSession -CommonModuleFilePathInVM $commonModuleFilePathInVM +'Clean up the PowerShell Direct session completed.' | Write-ScriptLog -'The WAC VM creation has been completed.' | Write-ScriptLog -Context $vmName - +'The WAC VM creation has been completed.' | Write-ScriptLog Stop-ScriptLogging diff --git a/template/customscripts/create-vm.ps1 b/template/customscripts/create-vm.ps1 index 190ed92..d40b235 100644 --- a/template/customscripts/create-vm.ps1 +++ b/template/customscripts/create-vm.ps1 @@ -11,31 +11,34 @@ try { $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log + 'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host $jobs = @() - 'Creating an AD DS VM...' | Write-ScriptLog -Context $env:ComputerName + 'Start the AD DS VM creation job.' | Write-ScriptLog $jobScriptFilePath = [IO.Path]::Combine($PSScriptRoot, 'create-vm-job-addsdc.ps1') $params = @{ PSModuleNameToImport = (Get-Module -Name 'common').Path LogFileName = [IO.Path]::GetFileNameWithoutExtension($jobScriptFilePath) } $jobs += Start-Job -Name 'addsdc-vm' -LiteralPath $jobScriptFilePath -InputObject ([PSCustomObject] $params) + 'Start the AD DS VM creation job completed.' | Write-ScriptLog - 'Creating a WAC VM...' | Write-ScriptLog -Context $env:ComputerName + 'Start the management server VM creation job.' | Write-ScriptLog $jobScriptFilePath = [IO.Path]::Combine($PSScriptRoot, 'create-vm-job-wac.ps1') $params = @{ PSModuleNameToImport = (Get-Module -Name 'common').Path LogFileName = [IO.Path]::GetFileNameWithoutExtension($jobScriptFilePath) } $jobs += Start-Job -Name 'wac-vm' -LiteralPath $jobScriptFilePath -InputObject ([PSCustomObject] $params) + 'Start the management server VM creation job completed.' | Write-ScriptLog - 'Creating HCI node VMs...' | Write-ScriptLog -Context $env:ComputerName + 'Start the HCI node VM creation jobs.' | Write-ScriptLog $jobScriptFilePath = [IO.Path]::Combine($PSScriptRoot, 'create-vm-job-hcinode.ps1') for ($nodeIndex = 0; $nodeIndex -lt $labConfig.hciNode.nodeCount; $nodeIndex++) { $vmName = Format-HciNodeName -Format $labConfig.hciNode.vmName -Offset $labConfig.hciNode.vmNameOffset -Index $nodeIndex - 'Start creating a HCI node VM...' -f $vmName | Write-ScriptLog -Context $vmName + 'Start a VM creation job for the VM "{0}".' -f $vmName | Write-ScriptLog $params = @{ NodeIndex = $nodeIndex PSModuleNameToImport = (Get-Module -Name 'common').Path @@ -43,12 +46,13 @@ try { } $jobs += Start-Job -Name $vmName -LiteralPath $jobScriptFilePath -InputObject ([PSCustomObject] $params) } + 'Start the HCI node VM creation jobs completed.' | Write-ScriptLog $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime $jobs | Receive-Job -Wait $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime - 'The HCI lab VMs creation has been completed.' | Write-ScriptLog -Context $env:ComputerName + 'The HCI lab VMs creation has been completed.' | Write-ScriptLog } catch { $jobs | Format-Table -Property Id, Name, State, HasMoreData, PSBeginTime, PSEndTime diff --git a/template/customscripts/download-iso-updates.ps1 b/template/customscripts/download-iso-updates.ps1 index 984b550..b9505f2 100644 --- a/template/customscripts/download-iso-updates.ps1 +++ b/template/customscripts/download-iso-updates.ps1 @@ -10,6 +10,7 @@ Import-Module -Name ([IO.Path]::Combine($PSScriptRoot, 'common.psm1')) -Force $labConfig = Get-LabDeploymentConfig Start-ScriptLogging -OutputDirectory $labConfig.labHost.folderPath.log +'Lab deployment config:' | Write-ScriptLog $labConfig | ConvertTo-Json -Depth 16 | Write-Host function Invoke-IsoFileDownload @@ -69,15 +70,17 @@ function Invoke-UpdateFileDonwload } } -'Reading the asset URL data file...' | Write-ScriptLog -Context $env:ComputerName +'Import the material URL data file.' | Write-ScriptLog $assetUrls = Import-PowerShellDataFile -LiteralPath ([IO.Path]::Combine($PSScriptRoot, 'download-iso-updates-asset-urls.psd1')) +'Import the material URL data file completed.' | Write-ScriptLog # ISO -'Creating the download folder if it does not exist...' | Write-ScriptLog -Context $env:ComputerName +'Create the download folder if it does not exist.' | Write-ScriptLog New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.temp -Force +'Create the download folder completed.' | Write-ScriptLog -'Downloading the ISO file for HCI nodes...' | Write-ScriptLog -Context $env:ComputerName +'Download the ISO file for HCI nodes.' | Write-ScriptLog $params = @{ OperatingSystem = $labConfig.hciNode.operatingSystem.sku Culture = $labConfig.guestOS.culture @@ -85,10 +88,11 @@ $params = @{ AssetUrls = $assetUrls } Invoke-IsoFileDownload @params +'Download the ISO file for HCI nodes completed.' | Write-ScriptLog # The Windows Server 2022 ISO is always needed for the domain controller VM. if ($labConfig.hciNode.operatingSystem.sku -ne [HciLab.OSSku]::WindowsServer2022) { - 'Downloading the Windows Server ISO file...' | Write-ScriptLog -Context $env:ComputerName + 'Donwload the Windows Server ISO file.' | Write-ScriptLog $params = @{ OperatingSystem = [HciLab.OSSku]::WindowsServer2022 Culture = $labConfig.guestOS.culture @@ -96,39 +100,40 @@ if ($labConfig.hciNode.operatingSystem.sku -ne [HciLab.OSSku]::WindowsServer2022 AssetUrls = $assetUrls } Invoke-IsoFileDownload @params + 'Donwload the Windows Server ISO file completed.' | Write-ScriptLog } -'The ISO files download has been completed.' | Write-ScriptLog -Context $env:ComputerName - # Updates # Download the updates if the flag is set. if ($labConfig.guestOS.shouldInstallUpdates) { - 'Creating the updates folder if it does not exist...' | Write-ScriptLog -Context $env:ComputerName + 'Create the updates folder if it does not exist.' | Write-ScriptLog New-Item -ItemType Directory -Path $labConfig.labHost.folderPath.updates -Force + 'Create the updates folder completed.' | Write-ScriptLog - 'Downloading updates...' | Write-ScriptLog -Context $env:ComputerName + 'Download updates for HCI nodes.' | Write-ScriptLog $params = @{ OperatingSystem = $labConfig.hciNode.operatingSystem.sku DownloadFolderBasePath = $labConfig.labHost.folderPath.updates AssetUrls = $assetUrls } Invoke-UpdateFileDonwload @params + 'Download updates. for HCI nodes completed.' | Write-ScriptLog if ($labConfig.hciNode.operatingSystem.sku -ne [HciLab.OSSku]::WindowsServer2022) { - 'Downloading the Windows Server updates...' | Write-ScriptLog -Context $env:ComputerName + 'Download the Windows Server updates.' | Write-ScriptLog $params = @{ OperatingSystem = [HciLab.OSSku]::WindowsServer2022 DownloadFolderBasePath = $labConfig.labHost.folderPath.updates AssetUrls = $assetUrls } Invoke-UpdateFileDonwload @params + 'Download the Windows Server updates completed.' | Write-ScriptLog } - - 'The update files download has been completed.' | Write-ScriptLog -Context $env:ComputerName } else { - 'Skipped download of updates due to shouldInstallUpdates not set.' | Write-ScriptLog -Context $env:ComputerName + 'Skip the download of updates due to shouldInstallUpdates not set.' | Write-ScriptLog } +'The material download has been completed.' | Write-ScriptLog Stop-ScriptLogging