|
|
|
@ -42,6 +42,30 @@ in
|
|
|
|
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
|
|
|
|
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
|
|
|
|
# delayOobeRun = false: sysprep + OOBE + activation in build VM
|
|
|
|
# delayOobeRun = false: sysprep + OOBE + activation in build VM
|
|
|
|
delayOobeRun ? false,
|
|
|
|
delayOobeRun ? false,
|
|
|
|
|
|
|
|
# configMedium = true: this is a generic sealed base whose per-VM data
|
|
|
|
|
|
|
|
# (hostname, static IP, timezone, desktop tint) is NOT baked. The target's
|
|
|
|
|
|
|
|
# first boot reads it off a small removable config CD (see makeConfigMedium)
|
|
|
|
|
|
|
|
# via baked finder/apply scripts. Implies the OOBE is deferred to the target,
|
|
|
|
|
|
|
|
# so it is only meaningful together with delayOobeRun = true. Lets one sealed
|
|
|
|
|
|
|
|
# store path be shared by every VM built from it.
|
|
|
|
|
|
|
|
configMedium ? false,
|
|
|
|
|
|
|
|
# Skip sysprep's SID reset (use /oobe without /generalize), so every rebuild
|
|
|
|
|
|
|
|
# of the layers above the cached base install carries the same machine SID --
|
|
|
|
|
|
|
|
# and therefore the same account SID. A profile kept on a persistent disk then
|
|
|
|
|
|
|
|
# matches on file ACLs, its NTUSER.DAT hive, and ProfileList across rebuilds,
|
|
|
|
|
|
|
|
# with no ownership fixups. As a side effect MountedDevices survives too, so
|
|
|
|
|
|
|
|
# the data disk keeps its drive letter without a boot-time reassign.
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# Correct only for an image that is always this one machine; a fleet that
|
|
|
|
|
|
|
|
# deploys the same image to many hosts wants the default generalization.
|
|
|
|
|
|
|
|
keepMachineSid ? false,
|
|
|
|
|
|
|
|
# Known-Folder redirection: keep the SID-bound profile on C: (so /generalize
|
|
|
|
|
|
|
|
# can still randomize the SID per machine) but point the user's data folders
|
|
|
|
|
|
|
|
# at the persistent data disk, so files -- not per-user registry settings --
|
|
|
|
|
|
|
|
# survive an OS rebuild. e.g. { base = "D:\\UserData"; folders = [ "Desktop"
|
|
|
|
|
|
|
|
# "Documents" "Downloads" ]; }. Needs dataDisk to provide the volume, and is
|
|
|
|
|
|
|
|
# mutually exclusive with profilesDirectory.
|
|
|
|
|
|
|
|
folderRedirect ? null,
|
|
|
|
}: let
|
|
|
|
}: let
|
|
|
|
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
|
|
|
|
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
|
|
|
|
hexToRgbStr = hex: let
|
|
|
|
hexToRgbStr = hex: let
|
|
|
|
@ -93,12 +117,161 @@ in
|
|
|
|
# interface arrives DHCP-managed -- assigning an address without turning DHCP
|
|
|
|
# interface arrives DHCP-managed -- assigning an address without turning DHCP
|
|
|
|
# off first does not stick, which is how a VM meant to be at a fixed address
|
|
|
|
# off first does not stick, which is how a VM meant to be at a fixed address
|
|
|
|
# ended up holding a lease instead.
|
|
|
|
# ended up holding a lease instead.
|
|
|
|
|
|
|
|
# PowerShell in its own file: it grew a wait loop and a retry, which are no
|
|
|
|
|
|
|
|
# fun to keep correct inside a cmd one-liner.
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# Two things it must survive. DHCP is turned off before the address is set,
|
|
|
|
|
|
|
|
# so any failure to set it strands the box with no address at all -- which is
|
|
|
|
|
|
|
|
# exactly what happened after an internal reboot, where a stale ARP entry for
|
|
|
|
|
|
|
|
# the address from the previous instance tripped duplicate-address detection
|
|
|
|
|
|
|
|
# and New-NetIPAddress threw. DadTransmits 0 turns that detection off so the
|
|
|
|
|
|
|
|
# static always binds, and the assignment is retried rather than fatal.
|
|
|
|
|
|
|
|
# Two shapes. Baked: the address is a build-time literal. configMedium: the
|
|
|
|
|
|
|
|
# address is read from C:\vmix-config.ps1 (dot-sourced), which the finder
|
|
|
|
|
|
|
|
# dropped there off the config CD -- so the same sealed script serves every
|
|
|
|
|
|
|
|
# VM. The wait/retry logic is identical either way.
|
|
|
|
|
|
|
|
staticIPAssign = if configMedium
|
|
|
|
|
|
|
|
then { addr = "$VmixIpAddress"; prefix = "$VmixPrefixLength"; gw = "$VmixGateway"; dns = "$VmixDns"; }
|
|
|
|
|
|
|
|
else { addr = "'${staticIP.address}'"; prefix = toString staticIP.prefixLength; gw = "'${staticIP.gateway}'"; dns = staticDnsList; };
|
|
|
|
|
|
|
|
staticIPScriptPs1 = pkgs.writeText "vmix-static-ip.ps1" ''
|
|
|
|
|
|
|
|
${lib.optionalString configMedium ''
|
|
|
|
|
|
|
|
if (-not (Test-Path C:\vmix-config.ps1)) { Write-Output 'vmix: no config yet'; exit 0 }
|
|
|
|
|
|
|
|
. C:\vmix-config.ps1
|
|
|
|
|
|
|
|
if (-not $VmixIpAddress) { Write-Output 'vmix: no static address in config'; exit 0 }
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
$a = $null
|
|
|
|
|
|
|
|
for ($n = 0; $n -lt 30; $n++) {
|
|
|
|
|
|
|
|
$a = Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1
|
|
|
|
|
|
|
|
if ($a) { break }
|
|
|
|
|
|
|
|
Start-Sleep -Seconds 2
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (-not $a) { Write-Output 'vmix: no adapter came up'; exit 1 }
|
|
|
|
|
|
|
|
$i = $a.ifIndex
|
|
|
|
|
|
|
|
Set-NetIPInterface -InterfaceIndex $i -Dhcp Disabled -DadTransmits 0 -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
Remove-NetRoute -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
$ok = $false
|
|
|
|
|
|
|
|
for ($k = 0; $k -lt 5 -and -not $ok; $k++) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
New-NetIPAddress -InterfaceIndex $i -IPAddress ${staticIPAssign.addr} -PrefixLength ${staticIPAssign.prefix} -DefaultGateway ${staticIPAssign.gw} -ErrorAction Stop | Out-Null
|
|
|
|
|
|
|
|
$ok = $true
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
Write-Output ('vmix: assign attempt ' + $k + ' failed: ' + $_.Exception.Message)
|
|
|
|
|
|
|
|
Start-Sleep -Seconds 2
|
|
|
|
|
|
|
|
Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (-not $ok) { Write-Output 'vmix: could not set static address'; exit 1 }
|
|
|
|
|
|
|
|
Set-DnsClientServerAddress -InterfaceIndex $i -ServerAddresses ${staticIPAssign.dns}
|
|
|
|
|
|
|
|
New-NetFirewallRule -DisplayName 'ICMPv4 Echo' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -Enabled True -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
Write-Output ('vmix: set ' + ${staticIPAssign.addr} + ' on ifIndex ' + $i)
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Finder: the config CD's drive letter is unknown, so scan for the marker file
|
|
|
|
|
|
|
|
# and stage it on C: where the baked scripts expect it. Runs on the target's
|
|
|
|
|
|
|
|
# first boot (post-oobe), before the per-boot static-IP task needs it.
|
|
|
|
|
|
|
|
loadConfigScript = pkgs.writeText "vmix-load-config.cmd" ''
|
|
|
|
|
|
|
|
@echo off
|
|
|
|
|
|
|
|
for %%D in (E F G H I J K L M N O P Q R S T U V W X Y Z D) do (
|
|
|
|
|
|
|
|
if exist %%D:\vmix-config.ps1 (
|
|
|
|
|
|
|
|
copy /y %%D:\vmix-config.ps1 C:\vmix-config.ps1 >nul
|
|
|
|
|
|
|
|
goto :done
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
:done
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Applies the per-VM config that is not an answer-file field: timezone, the
|
|
|
|
|
|
|
|
# desktop tint (per user, so run under the created account in post-oobe), and
|
|
|
|
|
|
|
|
# the machine rename. Rename is pending until the post-oobe reboot.
|
|
|
|
|
|
|
|
applyConfigScript = pkgs.writeText "vmix-apply-config.ps1" ''
|
|
|
|
|
|
|
|
if (-not (Test-Path C:\vmix-config.ps1)) { exit 0 }
|
|
|
|
|
|
|
|
. C:\vmix-config.ps1
|
|
|
|
|
|
|
|
if ($VmixTimeZone) { & tzutil /s "$VmixTimeZone" }
|
|
|
|
|
|
|
|
if ($VmixBgColor) {
|
|
|
|
|
|
|
|
$hex = ([string]$VmixBgColor).TrimStart('#')
|
|
|
|
|
|
|
|
$r = [Convert]::ToInt32($hex.Substring(0,2),16)
|
|
|
|
|
|
|
|
$g = [Convert]::ToInt32($hex.Substring(2,2),16)
|
|
|
|
|
|
|
|
$b = [Convert]::ToInt32($hex.Substring(4,2),16)
|
|
|
|
|
|
|
|
Set-ItemProperty 'HKCU:\Control Panel\Colors' -Name Background -Value "$r $g $b"
|
|
|
|
|
|
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallPaper -Value ""
|
|
|
|
|
|
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallpaperStyle -Value '0'
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($VmixHostname -and $env:COMPUTERNAME -ne $VmixHostname) {
|
|
|
|
|
|
|
|
Rename-Computer -NewName $VmixHostname -Force -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Creates the real per-VM account from the config and hands the machine over
|
|
|
|
|
|
|
|
# to it. The sealed image bakes only a generic bootstrap account (${username})
|
|
|
|
|
|
|
|
# -- enough to carry OOBE to a logon so this can run -- and the real account
|
|
|
|
|
|
|
|
# is made here, on the target, from the CD. Autologon is switched to it and a
|
|
|
|
|
|
|
|
# one-shot cleanup is armed; the post-oobe reboot then lets the real account
|
|
|
|
|
|
|
|
# log in and build its own SID-bound profile on D:\Users\<username>, after
|
|
|
|
|
|
|
|
# which the bootstrap is retired. So the account, like the SID, is per-VM and
|
|
|
|
|
|
|
|
# nothing about it is shared or baked. Runs as the bootstrap user in post-oobe.
|
|
|
|
|
|
|
|
createUserScript = pkgs.writeText "vmix-create-user.ps1" ''
|
|
|
|
|
|
|
|
if (-not (Test-Path C:\vmix-config.ps1)) { exit 0 }
|
|
|
|
|
|
|
|
. C:\vmix-config.ps1
|
|
|
|
|
|
|
|
if (-not $VmixUsername) { exit 0 }
|
|
|
|
|
|
|
|
if ($VmixUsername -ieq '${username}') { exit 0 }
|
|
|
|
|
|
|
|
& net user $VmixUsername $VmixPassword /add
|
|
|
|
|
|
|
|
& net localgroup Administrators $VmixUsername /add
|
|
|
|
|
|
|
|
$w = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
|
|
|
|
|
|
|
Set-ItemProperty $w -Name AutoAdminLogon -Value '1'
|
|
|
|
|
|
|
|
Set-ItemProperty $w -Name DefaultUserName -Value $VmixUsername
|
|
|
|
|
|
|
|
Set-ItemProperty $w -Name DefaultPassword -Value $VmixPassword
|
|
|
|
|
|
|
|
Remove-ItemProperty $w -Name DefaultDomainName -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
# Fires at the real account's first logon (HKLM RunOnce = next user to log
|
|
|
|
|
|
|
|
# on), i.e. after the reboot below, once the bootstrap is no longer in use.
|
|
|
|
|
|
|
|
Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' `
|
|
|
|
|
|
|
|
-Name vmixFinalize -Value 'cmd /c C:\vmix-finalize.cmd' -Type String
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Runs once as the real account (RunOnce, after the hand-over reboot), so the
|
|
|
|
|
|
|
|
# fresh machine SID and the real profile already exist. This is where activation
|
|
|
|
|
|
|
|
# belongs -- on that fresh SID, not the throwaway bootstrap's. It then finishes
|
|
|
|
|
|
|
|
# per-user setup, retires the bootstrap, unmounts the config CD for good, and
|
|
|
|
|
|
|
|
# wipes every vmix artifact off C:\ so the running machine carries no leftover
|
|
|
|
|
|
|
|
# setup files. Self-deletes last.
|
|
|
|
|
|
|
|
finalizeScript = pkgs.writeText "vmix-finalize.cmd" ''
|
|
|
|
|
|
|
|
@echo off
|
|
|
|
|
|
|
|
:: 1) Activate Windows on the real account's fresh SID (TSforge, offline).
|
|
|
|
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D
|
|
|
|
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /rilc
|
|
|
|
|
|
|
|
net stop sppsvc /y 2>nul
|
|
|
|
|
|
|
|
net start sppsvc
|
|
|
|
|
|
|
|
ping -n 8 127.0.0.1 >nul
|
|
|
|
|
|
|
|
if exist C:\MAS_AIO.cmd ( echo. | call C:\MAS_AIO.cmd /Z-Windows )
|
|
|
|
|
|
|
|
if exist "C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE" (
|
|
|
|
|
|
|
|
if exist C:\MAS_AIO.cmd ( echo. | call C:\MAS_AIO.cmd /Ohook )
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
:: 2) Per-user desktop tint, now that the real account is logged in.
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-apply-config.ps1 >nul 2>&1
|
|
|
|
|
|
|
|
:: 3) Retire the bootstrap account and its profile (idle now).
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_UserProfile | Where-Object { $_.LocalPath -like '*\${username}' } | Remove-CimInstance -ErrorAction SilentlyContinue"
|
|
|
|
|
|
|
|
net user ${username} /delete >nul 2>&1
|
|
|
|
|
|
|
|
:: 4) Unmount the config CD and keep it unmounted -- drop its letter and stop
|
|
|
|
|
|
|
|
:: the mount manager auto-lettering it (D: keeps its explicit assignment).
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Volume -Filter 'DriveType=5 AND DriveLetter IS NOT NULL' | ForEach-Object { $_.DriveLetter=$null; [void]($_ | Set-CimInstance) }"
|
|
|
|
|
|
|
|
> C:\Windows\Temp\vmix-am.txt echo automount disable
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-am.txt echo automount scrub
|
|
|
|
|
|
|
|
diskpart /s C:\Windows\Temp\vmix-am.txt >nul 2>&1
|
|
|
|
|
|
|
|
del /q C:\Windows\Temp\vmix-am.txt 2>nul
|
|
|
|
|
|
|
|
:: 5) Wipe every vmix setup artifact from C:\.
|
|
|
|
|
|
|
|
del /q C:\MAS_AIO.cmd C:\vmix-config.ps1 C:\vmix-data-initialized 2>nul
|
|
|
|
|
|
|
|
del /q C:\vmix-load-config.cmd C:\vmix-apply-config.ps1 C:\vmix-create-user.ps1 2>nul
|
|
|
|
|
|
|
|
del /q C:\vmix-init-data-disk.cmd C:\vmix-data-profile.cmd C:\vmix-heal-profile.ps1 2>nul
|
|
|
|
|
|
|
|
del /q C:\vmix-static-ip.cmd C:\vmix-static-ip.ps1 2>nul
|
|
|
|
|
|
|
|
del /q C:\oobe-unattend.xml C:\post-oobe.cmd 2>nul
|
|
|
|
|
|
|
|
:: 6) Self-delete.
|
|
|
|
|
|
|
|
(goto) 2>nul & del "%~f0"
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Thin launcher, so the scheduled task has a cmd to point at.
|
|
|
|
staticIPScript = pkgs.writeText "vmix-static-ip.cmd" ''
|
|
|
|
staticIPScript = pkgs.writeText "vmix-static-ip.cmd" ''
|
|
|
|
@echo off
|
|
|
|
@echo off
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$a = $null; for ($n = 0; $n -lt 30; $n++) { $a = Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1; if ($a) { break }; Start-Sleep -Seconds 2 }; if (-not $a) { Write-Output 'vmix: no adapter came up'; exit 1 }; $i = $a.ifIndex; Set-NetIPInterface -InterfaceIndex $i -Dhcp Disabled -ErrorAction SilentlyContinue; Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; Remove-NetRoute -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; New-NetIPAddress -InterfaceIndex $i -IPAddress '${staticIP.address}' -PrefixLength ${toString staticIP.prefixLength} -DefaultGateway '${staticIP.gateway}' -ErrorAction Stop | Out-Null; Set-DnsClientServerAddress -InterfaceIndex $i -ServerAddresses ${staticDnsList}; Write-Output ('vmix: set ' + '${staticIP.address}' + ' on ifIndex ' + $i)" > C:\Windows\Temp\vmix-static-ip.log 2>&1
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-static-ip.ps1 > C:\Windows\Temp\vmix-static-ip.log 2>&1
|
|
|
|
:: Answer pings. Windows blocks ICMP by default, which makes a box with a
|
|
|
|
|
|
|
|
:: fixed address look dead to everything that checks it the obvious way.
|
|
|
|
|
|
|
|
powershell -NoProfile -Command "New-NetFirewallRule -DisplayName 'ICMPv4 Echo' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -Enabled True | Out-Null" > nul 2>&1
|
|
|
|
|
|
|
|
'';
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
dataDriveLetter = if dataDisk != null then (dataDisk.driveLetter or "D") else "D";
|
|
|
|
dataDriveLetter = if dataDisk != null then (dataDisk.driveLetter or "D") else "D";
|
|
|
|
@ -112,7 +285,59 @@ in
|
|
|
|
# a GPT label, one full-size NTFS partition and the drive letter, while a disk
|
|
|
|
# a GPT label, one full-size NTFS partition and the drive letter, while a disk
|
|
|
|
# that already holds data keeps it and only has its letter re-asserted. The
|
|
|
|
# that already holds data keeps it and only has its letter re-asserted. The
|
|
|
|
# OS disk is added to QEMU first and so is always disk 0.
|
|
|
|
# OS disk is added to QEMU first and so is always disk 0.
|
|
|
|
initDataDiskScript = pkgs.writeText "vmix-init-data-disk.cmd" ''
|
|
|
|
initDataDiskScript = pkgs.writeText "vmix-init-data-disk.cmd" (if configMedium then ''
|
|
|
|
|
|
|
|
@echo off
|
|
|
|
|
|
|
|
:: Sealed-image variant. Two extra hazards over the baked path:
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
:: 1. The per-VM config rides an optical drive, and on the target's first
|
|
|
|
|
|
|
|
:: boot the raw data disk has no volume yet -- so Windows letters the CD
|
|
|
|
|
|
|
|
:: as ${dataDriveLetter}:, exactly where the profile volume must go. The
|
|
|
|
|
|
|
|
:: plain `if exist ${dataDriveLetter}:\` guard would then see the CD and
|
|
|
|
|
|
|
|
:: skip, leaving ProfilesDirectory pointed at read-only media. So a first
|
|
|
|
|
|
|
|
:: boot is tracked by a marker, not by the letter, and any occupant of
|
|
|
|
|
|
|
|
:: ${dataDriveLetter}: is moved aside before the data disk claims it.
|
|
|
|
|
|
|
|
:: 2. Left to Shell-Setup's FolderLocations, ProfilesDirectory can be
|
|
|
|
|
|
|
|
:: evaluated before this disk exists (unordered within specialize) and
|
|
|
|
|
|
|
|
:: silently fall back to C:. Setting it here, in the same step that just
|
|
|
|
|
|
|
|
:: created the volume, removes that race.
|
|
|
|
|
|
|
|
if exist C:\vmix-data-initialized goto :ensure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:: First boot: park whatever holds ${dataDriveLetter}: (the config CD) on Y:
|
|
|
|
|
|
|
|
:: so the data disk can take the letter. Harmless if the letter is free.
|
|
|
|
|
|
|
|
> C:\Windows\Temp\vmix-cd.txt echo select volume ${dataDriveLetter}
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-cd.txt echo assign letter=Y noerr
|
|
|
|
|
|
|
|
diskpart /s C:\Windows\Temp\vmix-cd.txt > nul 2>&1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:: Lay disk 1 (the host zvol) out from scratch and give it the letter.
|
|
|
|
|
|
|
|
> C:\Windows\Temp\vmix-dd-init.txt echo select disk 1
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-init.txt echo clean
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-init.txt echo convert gpt
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-init.txt echo create partition primary
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-init.txt echo format fs=ntfs quick label="${dataLabel}"
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-init.txt echo assign letter=${dataDriveLetter}
|
|
|
|
|
|
|
|
diskpart /s C:\Windows\Temp\vmix-dd-init.txt
|
|
|
|
|
|
|
|
echo initialized > C:\vmix-data-initialized
|
|
|
|
|
|
|
|
goto :ensure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:ensure
|
|
|
|
|
|
|
|
:: The letter normally persists via MountedDevices; re-assert if it is gone.
|
|
|
|
|
|
|
|
if exist ${dataDriveLetter}:\ goto :profiledir
|
|
|
|
|
|
|
|
> C:\Windows\Temp\vmix-dd-assign.txt echo select disk 1
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-assign.txt echo select partition 1
|
|
|
|
|
|
|
|
>> C:\Windows\Temp\vmix-dd-assign.txt echo assign letter=${dataDriveLetter}
|
|
|
|
|
|
|
|
diskpart /s C:\Windows\Temp\vmix-dd-assign.txt > nul 2>&1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:profiledir
|
|
|
|
|
|
|
|
${lib.optionalString (profilesDirectory != null) ''
|
|
|
|
|
|
|
|
:: Point new profiles at the data volume, now that it exists. REG_EXPAND_SZ
|
|
|
|
|
|
|
|
:: to match Windows' own ProfilesDirectory type.
|
|
|
|
|
|
|
|
if exist ${dataDriveLetter}:\ (
|
|
|
|
|
|
|
|
if not exist "${profilesDirectory}" mkdir "${profilesDirectory}"
|
|
|
|
|
|
|
|
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" /v ProfilesDirectory /t REG_EXPAND_SZ /d "${profilesDirectory}" /f > nul 2>&1
|
|
|
|
|
|
|
|
)''}
|
|
|
|
|
|
|
|
del /q C:\Windows\Temp\vmix-cd.txt C:\Windows\Temp\vmix-dd-init.txt C:\Windows\Temp\vmix-dd-assign.txt 2>nul
|
|
|
|
|
|
|
|
:done
|
|
|
|
|
|
|
|
'' else ''
|
|
|
|
@echo off
|
|
|
|
@echo off
|
|
|
|
:: diskpart rather than the Storage cmdlets. New-Partition and
|
|
|
|
:: diskpart rather than the Storage cmdlets. New-Partition and
|
|
|
|
:: Format-Volume need services that are not up yet this early in
|
|
|
|
:: Format-Volume need services that are not up yet this early in
|
|
|
|
@ -141,6 +366,102 @@ in
|
|
|
|
:cleanup
|
|
|
|
:cleanup
|
|
|
|
del /q C:\Windows\Temp\vmix-dd-assign.txt C:\Windows\Temp\vmix-dd-init.txt 2>nul
|
|
|
|
del /q C:\Windows\Temp\vmix-dd-assign.txt C:\Windows\Temp\vmix-dd-init.txt 2>nul
|
|
|
|
:done
|
|
|
|
:done
|
|
|
|
|
|
|
|
'');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# PowerShell in its own file, so neither cmd quoting nor Nix's '' need to be
|
|
|
|
|
|
|
|
# fought. If the account's real profile got backed up to a .bak key (the
|
|
|
|
|
|
|
|
# temporary-profile fallback), put it back: drop the temp key, rename .bak to
|
|
|
|
|
|
|
|
# the live SID, remove the temp directory, and drop a flag so the caller
|
|
|
|
|
|
|
|
# knows to reboot.
|
|
|
|
|
|
|
|
# Known-Folder GUIDs for the redirectable user folders.
|
|
|
|
|
|
|
|
knownFolderGuids = {
|
|
|
|
|
|
|
|
Desktop = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}";
|
|
|
|
|
|
|
|
Documents = "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}";
|
|
|
|
|
|
|
|
Downloads = "{374DE290-123F-4565-9164-39C4925E467B}";
|
|
|
|
|
|
|
|
Pictures = "{33E28130-4E1E-4676-835A-98395C3BC476}";
|
|
|
|
|
|
|
|
Music = "{4BD8D571-6D19-48D3-BE97-422220080E43}";
|
|
|
|
|
|
|
|
Videos = "{18989B1D-99B5-455B-841C-AB7C74E4DDFC}";
|
|
|
|
|
|
|
|
Favorites = "{1777F761-68AD-4D8A-87BD-30B759FA33DD}";
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
redirectFolders = if folderRedirect != null
|
|
|
|
|
|
|
|
then (folderRedirect.folders or [ "Desktop" "Documents" "Downloads" "Pictures" "Music" "Videos" ])
|
|
|
|
|
|
|
|
else [ ];
|
|
|
|
|
|
|
|
redirectBase = if folderRedirect != null then (folderRedirect.base or "D:\\UserData") else "D:\\UserData";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Per-user, run once per profile via Active Setup: point each known folder at
|
|
|
|
|
|
|
|
# its directory under the data volume. SHSetKnownFolderPath updates both the
|
|
|
|
|
|
|
|
# registration and the shell-folder registry; it does not move files, so a
|
|
|
|
|
|
|
|
# freshly created profile's empty C: folder is simply repointed at the D: one,
|
|
|
|
|
|
|
|
# which already holds this user's accumulated files after a rebuild.
|
|
|
|
|
|
|
|
folderRedirectPs1 = pkgs.writeText "vmix-redirect-folders.ps1" (lib.optionalString (folderRedirect != null) ''
|
|
|
|
|
|
|
|
$sig = @'
|
|
|
|
|
|
|
|
[DllImport("shell32.dll", CharSet=CharSet.Unicode)]
|
|
|
|
|
|
|
|
public static extern int SHSetKnownFolderPath(ref System.Guid rfid, uint dwFlags, System.IntPtr hToken, string pszPath);
|
|
|
|
|
|
|
|
'@
|
|
|
|
|
|
|
|
$kf = Add-Type -MemberDefinition $sig -Name KFP -Namespace Vmix -PassThru
|
|
|
|
|
|
|
|
$map = @{
|
|
|
|
|
|
|
|
${lib.concatMapStringsSep "\n " (f: "'${f}' = '${knownFolderGuids.${f}}'") redirectFolders}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($name in $map.Keys) {
|
|
|
|
|
|
|
|
$target = Join-Path '${redirectBase}' $name
|
|
|
|
|
|
|
|
New-Item -ItemType Directory -Force -Path $target -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
$guid = [System.Guid]$map[$name]
|
|
|
|
|
|
|
|
[void]$kf::SHSetKnownFolderPath([ref]$guid, 0, [System.IntPtr]::Zero, $target)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
'');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Active Setup fires StubPath once per user at first logon -- including the
|
|
|
|
|
|
|
|
# fresh profile each generalized rebuild creates -- which is exactly when the
|
|
|
|
|
|
|
|
# redirection needs re-applying. Backslashes doubled for .reg.
|
|
|
|
|
|
|
|
activeSetupRegistry = lib.optionalString (folderRedirect != null) ''
|
|
|
|
|
|
|
|
Windows Registry Editor Version 5.00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\{6f3b8c2a-1d4e-4f9a-b8c1-0a1b2c3d4e5f}]
|
|
|
|
|
|
|
|
@="vmix folder redirection"
|
|
|
|
|
|
|
|
"StubPath"="powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\\vmix-redirect-folders.ps1"
|
|
|
|
|
|
|
|
"Version"="1"
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
healProfileScript = pkgs.writeText "vmix-heal-profile.ps1" (lib.optionalString (profilesDirectory != null) ''
|
|
|
|
|
|
|
|
$pl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
|
|
|
|
|
|
|
|
$bak = Get-ChildItem $pl -ErrorAction SilentlyContinue | Where-Object {
|
|
|
|
|
|
|
|
$_.PSChildName -like '*.bak' -and
|
|
|
|
|
|
|
|
(Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).ProfileImagePath -eq '${profilesDirectory}\${username}'
|
|
|
|
|
|
|
|
} | Select-Object -First 1
|
|
|
|
|
|
|
|
if ($bak) {
|
|
|
|
|
|
|
|
$sid = $bak.PSChildName -replace '\.bak$'
|
|
|
|
|
|
|
|
Remove-Item (Join-Path $pl $sid) -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
Rename-Item $bak.PSPath $sid -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
Remove-Item '${profilesDirectory}\TEMP' -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
New-Item -Path C:\Windows\Temp\vmix-profile-healed -ItemType File -Force | Out-Null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
'');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# One onstart / SYSTEM script for whatever the data disk needs before logon:
|
|
|
|
|
|
|
|
# assign its letter, and then either heal a relocated profile that went
|
|
|
|
|
|
|
|
# temporary (profilesDirectory) or make the redirected data folders reachable
|
|
|
|
|
|
|
|
# by whatever account this rebuild created (folderRedirect). Both cannot apply
|
|
|
|
|
|
|
|
# at once -- a profile is either wholly on D: or only its data folders are.
|
|
|
|
|
|
|
|
bootDataProfileScript = pkgs.writeText "vmix-data-profile.cmd" ''
|
|
|
|
|
|
|
|
@echo off
|
|
|
|
|
|
|
|
${lib.optionalString (dataDisk != null) "call C:\\vmix-init-data-disk.cmd"}
|
|
|
|
|
|
|
|
${lib.optionalString (profilesDirectory != null) ''
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-heal-profile.ps1 > C:\Windows\Temp\vmix-data-profile.log 2>&1
|
|
|
|
|
|
|
|
if exist C:\Windows\Temp\vmix-profile-healed (
|
|
|
|
|
|
|
|
del /q C:\Windows\Temp\vmix-profile-healed
|
|
|
|
|
|
|
|
shutdown /r /t 5 /c "vmix: repaired relocated profile, restarting"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
${lib.optionalString (folderRedirect != null) ''
|
|
|
|
|
|
|
|
:: The redirected folders live under a per-user account whose SID changes on
|
|
|
|
|
|
|
|
:: every generalized rebuild, so grant the well-known Users group -- which
|
|
|
|
|
|
|
|
:: any account joins and which is SID-stable across machines -- inheritable
|
|
|
|
|
|
|
|
:: full control, and let the per-user redirect (Active Setup) point the known
|
|
|
|
|
|
|
|
:: folders here. Runs as SYSTEM, before any logon.
|
|
|
|
|
|
|
|
if not exist "${redirectBase}" mkdir "${redirectBase}"
|
|
|
|
|
|
|
|
${lib.concatMapStringsSep "\n " (f: ''if not exist "${redirectBase}\${f}" mkdir "${redirectBase}\${f}"'') redirectFolders}
|
|
|
|
|
|
|
|
icacls "${redirectBase}" /grant *S-1-5-32-545:(OI)(CI)F /t > C:\Windows\Temp\vmix-redirect-prep.log 2>&1
|
|
|
|
|
|
|
|
''}
|
|
|
|
'';
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
folderLocationsXml = lib.optionalString (profilesDirectory != null) ''
|
|
|
|
folderLocationsXml = lib.optionalString (profilesDirectory != null) ''
|
|
|
|
@ -150,6 +471,17 @@ in
|
|
|
|
<ProfilesDirectory>${profilesDirectory}</ProfilesDirectory>
|
|
|
|
<ProfilesDirectory>${profilesDirectory}</ProfilesDirectory>
|
|
|
|
</FolderLocations>'';
|
|
|
|
</FolderLocations>'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ProfilesDirectory as an offline .reg merge, for the keepMachineSid path
|
|
|
|
|
|
|
|
# where the specialize pass (and its FolderLocations) does not run. virt-win-reg
|
|
|
|
|
|
|
|
# applies this before the Audit Mode boot, so it is in place when OOBE creates
|
|
|
|
|
|
|
|
# the account. Backslashes are doubled for .reg syntax.
|
|
|
|
|
|
|
|
profileListRegistry = lib.optionalString (keepMachineSid && profilesDirectory != null) ''
|
|
|
|
|
|
|
|
Windows Registry Editor Version 5.00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList]
|
|
|
|
|
|
|
|
"ProfilesDirectory"="${builtins.replaceStrings [''\''] [''\\''] profilesDirectory}"
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
dataDiskXml = lib.optionalString (dataDisk != null) ''
|
|
|
|
dataDiskXml = lib.optionalString (dataDisk != null) ''
|
|
|
|
<!-- Runs during specialize, before the first profile is created -->
|
|
|
|
<!-- Runs during specialize, before the first profile is created -->
|
|
|
|
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
|
|
|
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
|
|
|
@ -166,6 +498,13 @@ in
|
|
|
|
# Post-OOBE script: runs as the created user via FirstLogonCommands.
|
|
|
|
# Post-OOBE script: runs as the created user via FirstLogonCommands.
|
|
|
|
postOobeScript = pkgs.writeText "post-oobe.cmd" ''
|
|
|
|
postOobeScript = pkgs.writeText "post-oobe.cmd" ''
|
|
|
|
@echo off
|
|
|
|
@echo off
|
|
|
|
|
|
|
|
${lib.optionalString configMedium ''
|
|
|
|
|
|
|
|
:: Stage the per-VM config off the removable CD, then apply the parts that
|
|
|
|
|
|
|
|
:: are not answer-file fields (timezone, desktop tint, machine rename). The
|
|
|
|
|
|
|
|
:: static address is left to the per-boot task registered below.
|
|
|
|
|
|
|
|
call C:\vmix-load-config.cmd
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-apply-config.ps1 > C:\Windows\Temp\vmix-apply-config.log 2>&1
|
|
|
|
|
|
|
|
''}
|
|
|
|
${lib.optionalString (!autoLogon) ''
|
|
|
|
${lib.optionalString (!autoLogon) ''
|
|
|
|
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /f 2>nul
|
|
|
|
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /f 2>nul
|
|
|
|
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultUserName /f 2>nul
|
|
|
|
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultUserName /f 2>nul
|
|
|
|
@ -202,6 +541,7 @@ in
|
|
|
|
powershell -Command "Get-AppxPackage *MicrosoftEdgeDevToolsClient* | Remove-AppxPackage -ErrorAction SilentlyContinue"
|
|
|
|
powershell -Command "Get-AppxPackage *MicrosoftEdgeDevToolsClient* | Remove-AppxPackage -ErrorAction SilentlyContinue"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString (!configMedium) ''
|
|
|
|
:: Re-install product key and licenses to restore activation IDs after sysprep
|
|
|
|
:: Re-install product key and licenses to restore activation IDs after sysprep
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /rilc
|
|
|
|
cscript //nologo C:\Windows\System32\slmgr.vbs /rilc
|
|
|
|
@ -220,6 +560,9 @@ in
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
del /q C:\MAS_AIO.cmd 2>nul
|
|
|
|
del /q C:\MAS_AIO.cmd 2>nul
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
:: configMedium: activation is deferred to the boot-2 finalize, so it runs
|
|
|
|
|
|
|
|
:: on the real account's fresh SID. MAS_AIO.cmd is kept for it until then.
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString enableRDP ''
|
|
|
|
${lib.optionalString enableRDP ''
|
|
|
|
:: Enable RDP
|
|
|
|
:: Enable RDP
|
|
|
|
@ -233,9 +576,19 @@ in
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\SessionEnv" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\SessionEnv" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\UmRdpService" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\UmRdpService" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\TermService" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\TermService" /v Start /t REG_DWORD /d 2 /f
|
|
|
|
|
|
|
|
:: A VM reached over RDP must never suspend itself off the network. The
|
|
|
|
|
|
|
|
:: default Balanced plan sleeps after 15 min idle; switch to High
|
|
|
|
|
|
|
|
:: Performance and zero every idle timeout, and turn hibernate off.
|
|
|
|
|
|
|
|
powercfg /setactive SCHEME_MIN
|
|
|
|
|
|
|
|
powercfg /change standby-timeout-ac 0
|
|
|
|
|
|
|
|
powercfg /change standby-timeout-dc 0
|
|
|
|
|
|
|
|
powercfg /change hibernate-timeout-ac 0
|
|
|
|
|
|
|
|
powercfg /change hibernate-timeout-dc 0
|
|
|
|
|
|
|
|
powercfg /change monitor-timeout-ac 0
|
|
|
|
|
|
|
|
powercfg /hibernate off
|
|
|
|
''}
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString (staticIP != null) ''
|
|
|
|
${lib.optionalString (staticIP != null && !configMedium) ''
|
|
|
|
:: This VM's only NIC sits on a macvtap bridged to the host's LAN, so its
|
|
|
|
:: This VM's only NIC sits on a macvtap bridged to the host's LAN, so its
|
|
|
|
:: address is a LAN address that nothing hands out -- the guest asserts it.
|
|
|
|
:: address is a LAN address that nothing hands out -- the guest asserts it.
|
|
|
|
:: Registered to run at every boot rather than applied here. OOBE runs in
|
|
|
|
:: Registered to run at every boot rather than applied here. OOBE runs in
|
|
|
|
@ -247,6 +600,23 @@ in
|
|
|
|
schtasks /create /tn "vmix-static-ip" /tr "C:\vmix-static-ip.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1
|
|
|
|
schtasks /create /tn "vmix-static-ip" /tr "C:\vmix-static-ip.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1
|
|
|
|
''}
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString configMedium ''
|
|
|
|
|
|
|
|
:: configMedium (sealed images) run OOBE on the real target NIC, so the
|
|
|
|
|
|
|
|
:: address is set here once and left in the persistent store -- it survives
|
|
|
|
|
|
|
|
:: reboots on its own, no per-boot task and no script left on disk. Runs on
|
|
|
|
|
|
|
|
:: this bootstrap boot so the real account is already reachable on boot 2.
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-static-ip.ps1 > C:\Windows\Temp\vmix-static-ip.log 2>&1
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString (dataDisk != null && !configMedium) ''
|
|
|
|
|
|
|
|
:: Ensures D: is assigned on every boot -- the image ships without a
|
|
|
|
|
|
|
|
:: persisted letter for the data disk -- and heals a profile that went
|
|
|
|
|
|
|
|
:: temporary before D: was ready. Onstart / SYSTEM, like the address task.
|
|
|
|
|
|
|
|
:: configMedium does not need this: the letter persists via MountedDevices
|
|
|
|
|
|
|
|
:: in the overlay after the first boot, so there is nothing to re-assert.
|
|
|
|
|
|
|
|
schtasks /create /tn "vmix-data-profile" /tr "C:\vmix-data-profile.cmd" /sc onstart /ru SYSTEM /rl HIGHEST /f > nul 2>&1
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString (writeFilter != null) ''
|
|
|
|
${lib.optionalString (writeFilter != null) ''
|
|
|
|
:: Install the feature now, but defer configuring it: uwfmgr does not exist
|
|
|
|
:: Install the feature now, but defer configuring it: uwfmgr does not exist
|
|
|
|
:: until this has been through a reboot, and the swapfile belongs on the
|
|
|
|
:: until this has been through a reboot, and the swapfile belongs on the
|
|
|
|
@ -255,12 +625,31 @@ in
|
|
|
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v "vmixUwf" /t REG_SZ /d "C:\vmix-uwf-config.cmd" /f
|
|
|
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v "vmixUwf" /t REG_SZ /d "C:\vmix-uwf-config.cmd" /f
|
|
|
|
''}
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${lib.optionalString keepMachineSid ''
|
|
|
|
|
|
|
|
:: /oobe without /generalize leaves the system set to re-run windeploy (OOBE)
|
|
|
|
|
|
|
|
:: on every boot -- shutting down here in a FirstLogonCommand cuts OOBE off
|
|
|
|
|
|
|
|
:: before it resets that state itself. Clear it, or the target boots into a
|
|
|
|
|
|
|
|
:: Setup with no unattend left to consume and hangs on a black screen.
|
|
|
|
|
|
|
|
reg add "HKLM\SYSTEM\Setup" /v SetupType /t REG_DWORD /d 0 /f
|
|
|
|
|
|
|
|
reg delete "HKLM\SYSTEM\Setup" /v OOBEInProgress /f 2>nul
|
|
|
|
|
|
|
|
reg delete "HKLM\SYSTEM\Setup" /v CmdLine /f 2>nul
|
|
|
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
${lib.optionalString configMedium ''
|
|
|
|
|
|
|
|
:: Runs last, as the generic bootstrap account: create the real per-VM
|
|
|
|
|
|
|
|
:: account from the config, switch autologon to it and arm the cleanup. The
|
|
|
|
|
|
|
|
:: reboot below then logs the real account in for the first time, building
|
|
|
|
|
|
|
|
:: its SID-bound profile on D:\Users\<username>.
|
|
|
|
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-create-user.ps1 > C:\Windows\Temp\vmix-create-user.log 2>&1
|
|
|
|
|
|
|
|
''}
|
|
|
|
:: Clean up
|
|
|
|
:: Clean up
|
|
|
|
del /q C:\oobe-unattend.xml 2>nul
|
|
|
|
del /q C:\oobe-unattend.xml 2>nul
|
|
|
|
del /q C:\vmix-audit-script.cmd 2>nul
|
|
|
|
del /q C:\vmix-audit-script.cmd 2>nul
|
|
|
|
del /q C:\vmix-audit-wrapper.cmd 2>nul
|
|
|
|
del /q C:\vmix-audit-wrapper.cmd 2>nul
|
|
|
|
|
|
|
|
|
|
|
|
${if delayOobeRun then "" else "shutdown /s /t 5 /c \"vmix generalize complete\""}
|
|
|
|
${if configMedium then "shutdown /r /t 5 /c \"vmix: applying per-VM config\""
|
|
|
|
|
|
|
|
else if delayOobeRun then ""
|
|
|
|
|
|
|
|
else "shutdown /s /t 5 /c \"vmix generalize complete\""}
|
|
|
|
del /q C:\post-oobe.cmd 2>nul
|
|
|
|
del /q C:\post-oobe.cmd 2>nul
|
|
|
|
'';
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
@ -346,20 +735,46 @@ ${folderLocationsXml}
|
|
|
|
</unattend>
|
|
|
|
</unattend>
|
|
|
|
'';
|
|
|
|
'';
|
|
|
|
in {
|
|
|
|
in {
|
|
|
|
name = if delayOobeRun then "generalize-delay-oobe" else "generalize";
|
|
|
|
name = if configMedium then "seal" else if delayOobeRun then "generalize-delay-oobe" else "generalize";
|
|
|
|
inherit nicModel;
|
|
|
|
inherit nicModel;
|
|
|
|
|
|
|
|
# With keepMachineSid the specialize pass never runs (see the sysprep line),
|
|
|
|
|
|
|
|
# so the profile relocation cannot ride the unattend there. It is written to
|
|
|
|
|
|
|
|
# the registry offline instead, before the build's OOBE creates the profile,
|
|
|
|
|
|
|
|
# so the account still lands on the data volume. Empty otherwise.
|
|
|
|
|
|
|
|
windowsRegistry = profileListRegistry + activeSetupRegistry;
|
|
|
|
# The blank disk is attached for the Audit Mode boot itself, so the disk-init
|
|
|
|
# The blank disk is attached for the Audit Mode boot itself, so the disk-init
|
|
|
|
# command and the profile relocation both happen under OOBE in the build VM.
|
|
|
|
# command and the profile relocation both happen under OOBE in the build VM.
|
|
|
|
# That is what makes delayOobeRun unnecessary: nothing is left to do on real
|
|
|
|
# That is what makes delayOobeRun unnecessary: nothing is left to do on real
|
|
|
|
# hardware. The written disk comes back as this derivation's `data` output.
|
|
|
|
# hardware. The written disk comes back as this derivation's `data` output.
|
|
|
|
extraDisk = if dataDisk != null then { size = dataDisk.size or "100G"; } else null;
|
|
|
|
#
|
|
|
|
|
|
|
|
# Under delayOobeRun there is no build-VM OOBE to relocate into, and the real
|
|
|
|
|
|
|
|
# data volume is the host's zvol attached at deploy time -- so building an
|
|
|
|
|
|
|
|
# empty throwaway disk here would be pure waste. Gated off: the target's
|
|
|
|
|
|
|
|
# specialize formats the real disk (dataDiskXml) and OOBE creates the profile
|
|
|
|
|
|
|
|
# on it. This is what lets a sealed image ship without a `data` output.
|
|
|
|
|
|
|
|
extraDisk = if (dataDisk != null && !delayOobeRun) then { size = dataDisk.size or "100G"; } else null;
|
|
|
|
uploads = [
|
|
|
|
uploads = [
|
|
|
|
{ source = oobeXml; dest = "/oobe-unattend.xml"; }
|
|
|
|
{ source = oobeXml; dest = "/oobe-unattend.xml"; }
|
|
|
|
{ source = postOobeScript; dest = "/post-oobe.cmd"; }
|
|
|
|
{ source = postOobeScript; dest = "/post-oobe.cmd"; }
|
|
|
|
{ source = masScript; dest = "/MAS_AIO.cmd"; }
|
|
|
|
{ source = masScript; dest = "/MAS_AIO.cmd"; }
|
|
|
|
] ++ lib.optional (dataDisk != null) { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
|
|
|
|
] ++ lib.optionals (dataDisk != null) (
|
|
|
|
|
|
|
|
[ { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
|
|
|
|
|
|
|
|
{ source = bootDataProfileScript; dest = "/vmix-data-profile.cmd"; }
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
++ lib.optional (profilesDirectory != null) { source = healProfileScript; dest = "/vmix-heal-profile.ps1"; }
|
|
|
|
|
|
|
|
++ lib.optional (folderRedirect != null) { source = folderRedirectPs1; dest = "/vmix-redirect-folders.ps1"; }
|
|
|
|
|
|
|
|
)
|
|
|
|
++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }
|
|
|
|
++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }
|
|
|
|
++ lib.optional (staticIP != null) { source = staticIPScript; dest = "/vmix-static-ip.cmd"; };
|
|
|
|
++ lib.optionals configMedium [
|
|
|
|
|
|
|
|
{ source = loadConfigScript; dest = "/vmix-load-config.cmd"; }
|
|
|
|
|
|
|
|
{ source = applyConfigScript; dest = "/vmix-apply-config.ps1"; }
|
|
|
|
|
|
|
|
{ source = createUserScript; dest = "/vmix-create-user.ps1"; }
|
|
|
|
|
|
|
|
{ source = finalizeScript; dest = "/vmix-finalize.cmd"; }
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
++ lib.optionals (staticIP != null || configMedium) [
|
|
|
|
|
|
|
|
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
|
|
|
|
|
|
|
{ source = staticIPScriptPs1; dest = "/vmix-static-ip.ps1"; }
|
|
|
|
|
|
|
|
];
|
|
|
|
# delayOobeRun: sysprep + shutdown — OOBE runs on real hardware
|
|
|
|
# delayOobeRun: sysprep + shutdown — OOBE runs on real hardware
|
|
|
|
# generalize: sysprep + reboot into OOBE in the same QEMU session
|
|
|
|
# generalize: sysprep + reboot into OOBE in the same QEMU session
|
|
|
|
auditScript = ''
|
|
|
|
auditScript = ''
|
|
|
|
@ -368,7 +783,7 @@ in {
|
|
|
|
del /q C:\Windows\Panther\unattend.xml 2>nul
|
|
|
|
del /q C:\Windows\Panther\unattend.xml 2>nul
|
|
|
|
del /q C:\Windows\Panther\Unattend\unattend.xml 2>nul
|
|
|
|
del /q C:\Windows\Panther\Unattend\unattend.xml 2>nul
|
|
|
|
del /q C:\Windows\System32\Sysprep\Panther\unattend.xml 2>nul
|
|
|
|
del /q C:\Windows\System32\Sysprep\Panther\unattend.xml 2>nul
|
|
|
|
${lib.optionalString (dataDisk != null) ''
|
|
|
|
${lib.optionalString (dataDisk != null && !delayOobeRun) ''
|
|
|
|
:: Lay the data disk out here, in Audit Mode, rather than leaving it to the
|
|
|
|
:: Lay the data disk out here, in Audit Mode, rather than leaving it to the
|
|
|
|
:: specialize pass alone. Component order within a pass is not guaranteed,
|
|
|
|
:: specialize pass alone. Component order within a pass is not guaranteed,
|
|
|
|
:: and FolderLocations is applied by Shell-Setup while the disk is prepared
|
|
|
|
:: and FolderLocations is applied by Shell-Setup while the disk is prepared
|
|
|
|
@ -377,9 +792,13 @@ in {
|
|
|
|
:: fully booted OS with the disk already attached, so this always works.
|
|
|
|
:: fully booted OS with the disk already attached, so this always works.
|
|
|
|
:: The specialize copy stays as a letter re-assertion after generalize
|
|
|
|
:: The specialize copy stays as a letter re-assertion after generalize
|
|
|
|
:: clears MountedDevices.
|
|
|
|
:: clears MountedDevices.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
:: Only when there is a build disk to lay out. Under delayOobeRun (sealed
|
|
|
|
|
|
|
|
:: images) the disk is the host's zvol, present only on the target, so this
|
|
|
|
|
|
|
|
:: is left to the target's specialize pass alone.
|
|
|
|
call C:\vmix-init-data-disk.cmd
|
|
|
|
call C:\vmix-init-data-disk.cmd
|
|
|
|
''}
|
|
|
|
''}
|
|
|
|
C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml
|
|
|
|
C:\Windows\System32\Sysprep\sysprep.exe ${lib.optionalString (!keepMachineSid) "/generalize "}/oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml
|
|
|
|
'';
|
|
|
|
'';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|