windows: seal mode -- generic OOBE-deferred base, per-VM data on a config CD
A sealed image bakes no per-VM data. generalize.nix gains a gated configMedium flag (false path byte-identical, so the shared macOS generalize path is untouched): the baked answer file stays generic and the target's first boot reads hostname/static-IP/timezone/tint off a small removable CD via a finder (vmix-load-config.cmd) + applier (vmix-apply-config.ps1), with the static-IP script dot-sourcing the same config. templates.seal is a thin preset (delayOobeRun + configMedium + D:\Users profiles + a data disk); images gain a .seal leaf next to .generalize; makeConfigMedium renders the per-VM ISO. So one sealed store path is shared by every VM, each mints its own SID on first boot and builds the whole profile on D:, and only a cheap ISO is per-VM. Also folds in the delayOobeRun reconcile: extraDisk (and the audit-mode data-disk init) are gated off under deferral, so a sealed image ships with no throwaway `data` output -- the target's specialize formats the host zvol instead. vms: disks.config.file attaches the config medium as a second CD-ROM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
parent
c40f4460e3
commit
702f723e6d
6 changed files with 173 additions and 15 deletions
|
|
@ -3,6 +3,7 @@ let
|
||||||
windows = rec {
|
windows = rec {
|
||||||
drivers = import ./drivers { inherit pkgs system; };
|
drivers = import ./drivers { inherit pkgs system; };
|
||||||
makeFilesISO = (import ./helpers/makeFilesISO.nix) { inherit pkgs; };
|
makeFilesISO = (import ./helpers/makeFilesISO.nix) { inherit pkgs; };
|
||||||
|
makeConfigMedium = (import ./helpers/makeConfigMedium.nix) { inherit pkgs lib makeFilesISO; };
|
||||||
customizeImage = (import ./helpers/customizeImage.nix) { inherit pkgs lib; };
|
customizeImage = (import ./helpers/customizeImage.nix) { inherit pkgs lib; };
|
||||||
customizeImageFold = builtins.foldl' customizeImage;
|
customizeImageFold = builtins.foldl' customizeImage;
|
||||||
templates = (import ./templates) { inherit pkgs lib system drivers makeFilesISO; };
|
templates = (import ./templates) { inherit pkgs lib system drivers makeFilesISO; };
|
||||||
|
|
@ -14,14 +15,20 @@ let
|
||||||
win10 = (import ./win10) { inherit pkgs lib system windows; };
|
win10 = (import ./win10) { inherit pkgs lib system windows; };
|
||||||
win11 = (import ./win11) { inherit pkgs lib system windows; };
|
win11 = (import ./win11) { inherit pkgs lib system windows; };
|
||||||
|
|
||||||
# Recursively add .generalize to every derivation leaf in the image tree
|
# Recursively add .generalize and .seal to every derivation leaf in the tree
|
||||||
addGeneralize = val:
|
addGeneralize = val:
|
||||||
if val ? _vmixOsType then
|
if val ? _vmixOsType then
|
||||||
val // { generalize = args:
|
val // {
|
||||||
|
generalize = args:
|
||||||
let
|
let
|
||||||
templateArgs = builtins.removeAttrs args [ "vncDisplay" ];
|
templateArgs = builtins.removeAttrs args [ "vncDisplay" ];
|
||||||
displayArgs = lib.optionalAttrs (args ? vncDisplay) { inherit (args) vncDisplay; };
|
displayArgs = lib.optionalAttrs (args ? vncDisplay) { inherit (args) vncDisplay; };
|
||||||
in windows.customizeImage val (windows.templates.generalize templateArgs // displayArgs);
|
in windows.customizeImage val (windows.templates.generalize templateArgs // displayArgs);
|
||||||
|
seal = args:
|
||||||
|
let
|
||||||
|
templateArgs = builtins.removeAttrs args [ "vncDisplay" ];
|
||||||
|
displayArgs = lib.optionalAttrs (args ? vncDisplay) { inherit (args) vncDisplay; };
|
||||||
|
in windows.customizeImage val (windows.templates.seal templateArgs // displayArgs);
|
||||||
}
|
}
|
||||||
else if builtins.isAttrs val then
|
else if builtins.isAttrs val then
|
||||||
lib.mapAttrs (_: addGeneralize) val
|
lib.mapAttrs (_: addGeneralize) val
|
||||||
|
|
|
||||||
51
lib/images/windows/helpers/makeConfigMedium.nix
Normal file
51
lib/images/windows/helpers/makeConfigMedium.nix
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Per-VM config medium for a sealed Windows image (see templates.seal).
|
||||||
|
#
|
||||||
|
# A sealed image carries no per-VM data. The values that differ between VMs --
|
||||||
|
# hostname, the static address the guest asserts, timezone, desktop tint -- are
|
||||||
|
# written here as a PowerShell data file and packed into a tiny ISO. config.nix
|
||||||
|
# attaches it as a read-only CD-ROM; the image's baked first-boot scripts
|
||||||
|
# (vmix-load-config.cmd finds it, then dot-source it) apply the values. So one
|
||||||
|
# sealed store path is shared by every VM, and only this cheap ISO is per-VM.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# makeConfigMedium {
|
||||||
|
# name = "win-config";
|
||||||
|
# hostname = "panda-win";
|
||||||
|
# staticIP = { address = "10.10.10.26"; prefixLength = 24;
|
||||||
|
# gateway = "10.10.10.1"; dns = [ "10.10.10.1" ]; };
|
||||||
|
# timezone = "E. South America Standard Time";
|
||||||
|
# bgColor = "#856558";
|
||||||
|
# }
|
||||||
|
{ pkgs, lib, makeFilesISO, ... }:
|
||||||
|
{
|
||||||
|
name ? "vmix-config",
|
||||||
|
hostname ? "",
|
||||||
|
# { address; prefixLength; gateway; dns = [ ... ]; }
|
||||||
|
staticIP ? null,
|
||||||
|
timezone ? null,
|
||||||
|
# Solid desktop background as a hex string, e.g. "#856558". Converted to the
|
||||||
|
# registry's decimal "R G B" on the target, in vmix-apply-config.ps1.
|
||||||
|
bgColor ? null,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
dnsList = lib.optionalString (staticIP != null)
|
||||||
|
(lib.concatMapStringsSep "," (s: "'${s}'") staticIP.dns);
|
||||||
|
|
||||||
|
# Consumed by dot-sourcing (. C:\vmix-config.ps1), so it only assigns
|
||||||
|
# variables. Anything not set here is simply absent, and the baked scripts
|
||||||
|
# guard on that ($VmixIpAddress being null skips the static-IP assignment).
|
||||||
|
configPs1 = pkgs.writeText "vmix-config.ps1" ''
|
||||||
|
# vmix per-VM config -- generated, read by the sealed image's baked scripts.
|
||||||
|
$VmixHostname = '${hostname}'
|
||||||
|
${lib.optionalString (staticIP != null) ''
|
||||||
|
$VmixIpAddress = '${staticIP.address}'
|
||||||
|
$VmixPrefixLength = ${toString staticIP.prefixLength}
|
||||||
|
$VmixGateway = '${staticIP.gateway}'
|
||||||
|
$VmixDns = @(${dnsList})''}
|
||||||
|
${lib.optionalString (timezone != null) "$VmixTimeZone = '${timezone}'"}
|
||||||
|
${lib.optionalString (bgColor != null) "$VmixBgColor = '${bgColor}'"}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
# makeFilesISO strips the store-hash prefix, so this lands at the ISO root as
|
||||||
|
# exactly vmix-config.ps1 -- which is what vmix-load-config.cmd scans for.
|
||||||
|
makeFilesISO { inherit name; files = [ configPs1 ]; }
|
||||||
|
|
@ -39,6 +39,18 @@ in rec {
|
||||||
# Generalize (sysprep + OOBE). Pass seal=true for hardware deployment.
|
# Generalize (sysprep + OOBE). Pass seal=true for hardware deployment.
|
||||||
generalize = import ./generalize.nix args;
|
generalize = import ./generalize.nix args;
|
||||||
|
|
||||||
|
# Seal: a generic OOBE-deferred base whose per-VM data is not baked but
|
||||||
|
# delivered at deploy time on a config medium (helpers/makeConfigMedium.nix).
|
||||||
|
# One sealed store path is shared by every VM; each VM's first boot mints its
|
||||||
|
# own SID and builds the whole profile on the relocated data volume (D:).
|
||||||
|
# Forces only the structural bits -- account, RDP and locale stay caller args.
|
||||||
|
seal = templateArgs: generalize ({
|
||||||
|
delayOobeRun = true;
|
||||||
|
configMedium = true;
|
||||||
|
profilesDirectory = "D:\\Users";
|
||||||
|
dataDisk = { driveLetter = "D"; label = "data"; };
|
||||||
|
} // templateArgs);
|
||||||
|
|
||||||
# Offline registry templates
|
# Offline registry templates
|
||||||
reg = import ./registry args;
|
reg = import ./registry args;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ 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
|
# 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 --
|
# 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
|
# and therefore the same account SID. A profile kept on a persistent disk then
|
||||||
|
|
@ -119,7 +126,19 @@ in
|
||||||
# the address from the previous instance tripped duplicate-address detection
|
# the address from the previous instance tripped duplicate-address detection
|
||||||
# and New-NetIPAddress threw. DadTransmits 0 turns that detection off so the
|
# and New-NetIPAddress threw. DadTransmits 0 turns that detection off so the
|
||||||
# static always binds, and the assignment is retried rather than fatal.
|
# 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" ''
|
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
|
$a = $null
|
||||||
for ($n = 0; $n -lt 30; $n++) {
|
for ($n = 0; $n -lt 30; $n++) {
|
||||||
$a = Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1
|
$a = Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1
|
||||||
|
|
@ -134,7 +153,7 @@ in
|
||||||
$ok = $false
|
$ok = $false
|
||||||
for ($k = 0; $k -lt 5 -and -not $ok; $k++) {
|
for ($k = 0; $k -lt 5 -and -not $ok; $k++) {
|
||||||
try {
|
try {
|
||||||
New-NetIPAddress -InterfaceIndex $i -IPAddress '${staticIP.address}' -PrefixLength ${toString staticIP.prefixLength} -DefaultGateway '${staticIP.gateway}' -ErrorAction Stop | Out-Null
|
New-NetIPAddress -InterfaceIndex $i -IPAddress ${staticIPAssign.addr} -PrefixLength ${staticIPAssign.prefix} -DefaultGateway ${staticIPAssign.gw} -ErrorAction Stop | Out-Null
|
||||||
$ok = $true
|
$ok = $true
|
||||||
} catch {
|
} catch {
|
||||||
Write-Output ('vmix: assign attempt ' + $k + ' failed: ' + $_.Exception.Message)
|
Write-Output ('vmix: assign attempt ' + $k + ' failed: ' + $_.Exception.Message)
|
||||||
|
|
@ -143,9 +162,44 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (-not $ok) { Write-Output 'vmix: could not set static address'; exit 1 }
|
if (-not $ok) { Write-Output 'vmix: could not set static address'; exit 1 }
|
||||||
Set-DnsClientServerAddress -InterfaceIndex $i -ServerAddresses ${staticDnsList}
|
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
|
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 ${staticIP.address} on ifIndex ' + $i)
|
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
|
||||||
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Thin launcher, so the scheduled task has a cmd to point at.
|
# Thin launcher, so the scheduled task has a cmd to point at.
|
||||||
|
|
@ -326,6 +380,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
|
||||||
|
|
@ -395,7 +456,7 @@ in
|
||||||
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
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${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
|
||||||
|
|
@ -437,7 +498,9 @@ in
|
||||||
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
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
@ -523,7 +586,7 @@ ${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),
|
# With keepMachineSid the specialize pass never runs (see the sysprep line),
|
||||||
# so the profile relocation cannot ride the unattend there. It is written to
|
# so the profile relocation cannot ride the unattend there. It is written to
|
||||||
|
|
@ -534,7 +597,13 @@ in {
|
||||||
# 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"; }
|
||||||
|
|
@ -547,7 +616,11 @@ in {
|
||||||
++ lib.optional (folderRedirect != null) { source = folderRedirectPs1; dest = "/vmix-redirect-folders.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.optionals (staticIP != null) [
|
++ lib.optionals configMedium [
|
||||||
|
{ source = loadConfigScript; dest = "/vmix-load-config.cmd"; }
|
||||||
|
{ source = applyConfigScript; dest = "/vmix-apply-config.ps1"; }
|
||||||
|
]
|
||||||
|
++ lib.optionals (staticIP != null || configMedium) [
|
||||||
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
||||||
{ source = staticIPScriptPs1; dest = "/vmix-static-ip.ps1"; }
|
{ source = staticIPScriptPs1; dest = "/vmix-static-ip.ps1"; }
|
||||||
];
|
];
|
||||||
|
|
@ -559,7 +632,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
|
||||||
|
|
@ -568,6 +641,10 @@ 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 ${lib.optionalString (!keepMachineSid) "/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
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ let
|
||||||
''} \
|
''} \
|
||||||
${optionalString hasOsDisk "-drive file=${osDiskPath},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"}"} \
|
${optionalString hasOsDisk "-drive file=${osDiskPath},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"}"} \
|
||||||
${optionalString (vmCfg.disks.iso.file != null) "-drive file=${toString vmCfg.disks.iso.file},media=cdrom,readonly=on"} \
|
${optionalString (vmCfg.disks.iso.file != null) "-drive file=${toString vmCfg.disks.iso.file},media=cdrom,readonly=on"} \
|
||||||
|
${optionalString (vmCfg.disks.config.file != null) "-drive file=${toString vmCfg.disks.config.file},media=cdrom,readonly=on"} \
|
||||||
${concatMapStrings (diskCfg: ''
|
${concatMapStrings (diskCfg: ''
|
||||||
-drive file=${toString diskCfg.file},format=${diskCfg.format},if=${vmCfg.disks.bus} \
|
-drive file=${toString diskCfg.file},format=${diskCfg.format},if=${vmCfg.disks.bus} \
|
||||||
'') (attrValues vmCfg.disks.add)} \
|
'') (attrValues vmCfg.disks.add)} \
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,16 @@ with lib;
|
||||||
description = "Path to the ISO file. Can be a Nix store path or a string path to a local file.";
|
description = "Path to the ISO file. Can be a Nix store path or a string path to a local file.";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
disks.config.file = mkOption {
|
||||||
|
type = types.nullOr (types.either types.path types.str);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
A small read-only config medium attached as a second CD-ROM. For Windows
|
||||||
|
sealed images (templates.seal) this is the per-VM ISO from
|
||||||
|
vmixLib.windows.makeConfigMedium, carrying hostname/static-IP/etc. that
|
||||||
|
the image's baked first-boot scripts consume. Null to attach nothing.
|
||||||
|
'';
|
||||||
|
};
|
||||||
disks.add = mkOption {
|
disks.add = mkOption {
|
||||||
default = {};
|
default = {};
|
||||||
type = types.attrsOf (types.submodule {
|
type = types.attrsOf (types.submodule {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue