You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

125 lines
7.8 KiB

[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $LogPath,
[Parameter(Mandatory)] [ValidatePattern('^[A-Za-z][A-Za-z0-9-]{0,14}$')] [string] $Name,
[Parameter(Mandatory)] [string] $OperatingSystem,
[Parameter(Mandatory)] [string] $IpAddress,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[ValidateRange(2, 16)] [int] $MemoryGB = 4,
[ValidateRange(1, 8)] [int] $Processors = 2,
[ValidateRange(5, 240)] [int] $StartTimeoutMinutes = 40,
[string] $BackupRoot = 'C:\ProgramData\AutomatedLab\Backups'
)
# Adds one machine to the already deployed matrix lab (Decision 24) and creates only that machine. AutomatedLab 5.61 has no supported way to
# extend a deployed lab: Add-LabMachineDefinition refuses while a lab is imported or exported, and Install-Lab creates every machine of the
# lab again. This script copies the lab metadata first (the copy is readable by administrators only, because the files hold the lab
# credentials), reloads the definition with Import-LabDefinition (never Import-Lab), adds the machine, exports the definition, and then runs
# the same steps Install-Lab runs for a single machine: base image, hosts entries, virtual machine, start. The other machines are neither
# created, started, nor changed. Windows PowerShell 5.1 on the host; run it elevated.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script must run in an elevated PowerShell session.' }
($stamp -f [DateTime]::UtcNow) + " START add-os-matrix-machine lab=$LabName name=$Name os='$OperatingSystem' ip=$IpAddress" | Set-Content -LiteralPath $LogPath
$lockPath = $null
try {
Import-Module -Name AutomatedLab -ErrorAction Stop
if ((Get-Lab -List) -notcontains $LabName) { throw "The lab '$LabName' does not exist." }
if (Get-VM -Name $Name -ErrorAction SilentlyContinue) { throw "A virtual machine named '$Name' exists already." }
$hostsText = Get-Content -LiteralPath (Join-Path -Path $env:SystemRoot -ChildPath 'System32\drivers\etc\hosts') -Raw
if ($hostsText -match ('(?im)^\s*[^#\s]+\s+{0}(\.|\s|$)' -f [regex]::Escape($Name)) -or $hostsText -match ('(?im)^\s*{0}\s' -f [regex]::Escape($IpAddress))) {
throw "The hosts file mentions '$Name' or $IpAddress already."
}
$labFolder = Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Labs\$LabName"
$backup = Join-Path -Path $BackupRoot -ChildPath ('{0}-{1:yyyyMMdd-HHmmss}' -f $LabName, [DateTime]::UtcNow)
$null = New-Item -ItemType Directory -Path $backup -Force
$null = & icacls.exe $backup /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F'
if ($LASTEXITCODE -ne 0) { throw "icacls failed on the backup folder (exit code $LASTEXITCODE)." }
Copy-Item -LiteralPath $labFolder -Destination $backup -Recurse
Write-Step "lab metadata copied to $backup"
Import-LabDefinition -Name $LabName
$definition = Get-LabDefinition
$before = @(Get-LabMachineDefinition | ForEach-Object -Process { $_.Name })
$domainName = $definition.Domains[0].Name
$rootDc = Get-LabMachineDefinition | Where-Object -FilterScript { 'RootDC' -in $_.Roles.Name } | Select-Object -First 1
$dcAddress = ($rootDc.NetworkAdapters | Select-Object -First 1).Ipv4Address.IpAddress.AddressAsString
$dcPrefix = ($dcAddress -split '\.')[0..2] -join '.'
$newPrefix = ($IpAddress -split '\.')[0..2] -join '.'
if ($dcPrefix -ne $newPrefix) { throw "$IpAddress isn't in the /24 of the domain controller ($dcAddress)." }
Write-Step ("definition loaded: domain {0}; machines {1}; installation account {2}" -f $domainName, ($before -join ','), $definition.DefaultInstallationCredential.UserName)
$parameters = @{
Name = $Name; DomainName = $domainName; OperatingSystem = $OperatingSystem; Memory = ($MemoryGB * 1GB)
Processors = $Processors; Network = $LabName; IpAddress = $IpAddress
}
if ($OperatingSystem -like 'Windows 11*') {
$parameters.HypervProperties = @{ EnableSecureBoot = 'On'; SecureBootTemplate = 'MicrosoftWindows'; EnableTpm = 'true' }
}
Add-LabMachineDefinition @parameters
Export-LabDefinition -Force -ExportDefaultUnattendedXml
Import-Lab -Name $LabName -NoValidation -NoDisplay
$after = @(Get-LabVM -IncludeLinux | ForEach-Object -Process { $_.Name })
$difference = @(Compare-Object -ReferenceObject ($before + $Name) -DifferenceObject $after)
if ($difference.Count -gt 0) { throw "The exported lab doesn't hold exactly the old machines plus $Name. Restore the metadata from $backup." }
Write-Step 'definition extended and exported'
$lockCandidate = Get-LabConfigurationItem -Name DiskDeploymentInProgressPath
if (Test-Path -LiteralPath $lockCandidate) { throw "Another lab disk deployment seems to be in progress ($lockCandidate)." }
$null = New-Item -Path $lockCandidate -ItemType File -Value $LabName
# Only a lock that this script created is removed in the finally block below.
$lockPath = $lockCandidate
New-LabBaseImages
Write-Step 'base images ready'
$machine = Get-LabVM -ComputerName $Name
$address = ($machine.NetworkAdapters | Select-Object -First 1).Ipv4Address.IpAddress.AddressAsString
$null = Add-HostEntry -HostName $machine.Name -IpAddress $address -Section $LabName
$null = Add-HostEntry -HostName $machine.FQDN -IpAddress $address -Section $LabName
New-LabVM -Name $Name
Set-LabDefinition -Machines (Get-Lab).Machines
Export-LabDefinition -Force -ExportDefaultUnattendedXml -Silent
Write-Step 'virtual machine created and definition exported'
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
$lockPath = $null
Start-LabVM -ComputerName $Name -ProgressIndicator 30 -TimeoutInMinutes $StartTimeoutMinutes -Wait
Write-Step 'machine started and reachable with the lab credentials'
$userName = (Get-Lab).DefaultInstallationCredential.UserName
Invoke-LabCommand -ActivityName 'Setting PasswordNeverExpires for local deployment accounts' -ComputerName $Name -NoDisplay -Variable (Get-Variable -Name userName) -ScriptBlock {
Get-CimInstance -Query "Select * from Win32_UserAccount where name = '$userName' and localaccount='true'" | Set-CimInstance -Property @{ PasswordExpires = $false }
}
$evidence = Invoke-LabCommand -ComputerName $Name -ActivityName 'Readiness of the new member' -NoDisplay -PassThru -ErrorAction Stop -ArgumentList $domainName -ScriptBlock {
param ($Domain)
$current = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
[pscustomobject]@{
Build = '{0}.{1}' -f $current.CurrentBuildNumber, $current.UBR
Product = $current.ProductName
Domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
SecureChannel = [bool] (Test-ComputerSecureChannel)
Verify = (@(& nltest.exe "/sc_verify:$Domain" 2>&1) -join ' | ')
}
}
Write-Step ('new member: build {0} ({1}); domain {2}; secure channel {3}; nltest: {4}' -f $evidence.Build, $evidence.Product, $evidence.Domain, $evidence.SecureChannel, $evidence.Verify)
if (-not $evidence.SecureChannel) { throw "The secure channel of $Name is broken." }
Write-Step 'add-os-matrix-machine-DONE'
exit 0
}
catch {
Write-Step ('add-os-matrix-machine-FAILED: {0}' -f $_)
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
exit 1
}
finally {
if ($lockPath) { Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue }
}