unattend

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 13 Imported by: 1

Documentation

Index

Constants

View Source
const (
	// BootstrapScriptName is the script placed on the answer volume and
	// invoked by the single FirstLogonCommands entry.
	BootstrapScriptName = "devcell-bootstrap.ps1"
	// BootstrapLogName is the transcript the script writes next to itself.
	BootstrapLogName = "devcell-bootstrap.log"
	// OpenSSHVersion pins the Win32-OpenSSH release. Win32-OpenSSH is
	// Microsoft's own signed distribution of the same code the OpenSSH.Server
	// capability installs, so this is not a third-party substitute: Windows
	// 11 24H2 ships 9.5.6.1 in System32\OpenSSH, the same generation.
	//
	// Pinned rather than tracked from `latest`, because 9.8 split sshd into
	// sshd.exe + sshd-session.exe + sshd-auth.exe. WinPE's shell runs as
	// NT AUTHORITY\SYSTEM, so sshd takes the privilege-separation path and
	// the auth child exits 0xC0000142 STATUS_DLL_INIT_FAILED, dropping the
	// connection right after KEXINIT. Before 9.8 there is one sshd.exe and
	// no such child. Tracking `latest` is what moved us onto 10.0 silently.
	OpenSSHVersion = "v9.5.0.0p1-Beta"
	// OpenSSHPayloadName is the Win32-OpenSSH release shipped on the answer
	// volume. Windows servicing cannot install OpenSSH Server from our media,
	// so the standalone build is the primary install path.
	//
	// The version is part of the name on purpose: DownloadOpenSSH treats a
	// cached file plus its .done marker as a hit, so a bump that kept the old
	// name would leave the previous archive in place and change nothing.
	OpenSSHPayloadName = "openssh-arm64-9.5.0.0p1-Beta.zip"
	// OpenSSHReleaseURL is Microsoft's signed ARM64 release.
	OpenSSHReleaseURL = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/" +
		OpenSSHVersion + "/OpenSSH-ARM64.zip"
)

First-logon bootstrap.

All first-logon provisioning lives in one generated PowerShell script shipped on the answer volume, not in inline FirstLogonCommands: a script file has no XML/cmd quoting hazards (a multi-line SSH key broke the inline form), is unit-testable, and can report its own failures. An inline CommandLine that fails does so silently — and silent guest failures have each cost a multi-hour run to notice.

Failure reporting goes to two host-readable channels:

  • the virtio-serial progress port (lands in Spec.GuestProgressLogPath on the host, live)
  • a Start-Transcript log on the answer volume, read back with isokit.ReadFileFromFAT after the run
View Source
const (
	// GuestDiagnosticsScriptName is the script placed on the answer volume.
	GuestDiagnosticsScriptName = "devcell-diag.ps1"
	// GuestDiagnosticsLogName is where that script writes its report, on the
	// same volume so the host can read it back.
	GuestDiagnosticsLogName = "devcell-diag.log"
)

Guest-side diagnostics.

Most of what goes wrong in a Windows cell is invisible from the host: the host can see that SSH does not answer, but not whether the NIC driver bound, which drive letter a device got, or whether a first-logon command failed. SSH would answer all of that, but SSH is usually the thing that is broken.

So the guest writes its own report to the answer volume — removable, FAT, already attached, and readable from the host with ReadFileFromFAT. No network, no agent, and no keyboard automation, which under TCG proved unreliable: injected keystrokes auto-repeated and latched modifiers down.

View Source
const DefaultSessionUser = "devcell"

DefaultSessionUser is used when the host provides no $USER.

View Source
const StartupNSH = `` /* 497-byte string literal not displayed */

StartupNSH is the UEFI shell startup script that boots the Windows installer. UEFI ignores BIOS-style `-boot d`, so we need this to chain-load the Windows EFI boot loader. Uses sequential if-exist checks (not a for loop) because UEFI Shell %var expansion inside path strings is unreliable across EDK II builds.

Variables

This section is empty.

Functions

func AnswerVolumeConsumed

func AnswerVolumeConsumed(readBytes int64) bool

AnswerVolumeConsumed reports whether the guest's cumulative reads from the answer volume prove Setup consumed the answer file. This is the earliest host-visible signal that an install is unattended rather than sitting at the interactive language screen — the two are indistinguishable on screen ratios alone.

func BuildAnswerVolume

func BuildAnswerVolume(cfg Config, destPath string) error

BuildAnswerVolume renders the answer file and the first-logon bootstrap from one config and writes the complete answer volume. This is the entry point for building a real install volume — the XML's FirstLogonCommands launcher expects the bootstrap script to ship next to it, and taking the config here makes it impossible to build a volume where they disagree.

func GenerateBootstrapScript

func GenerateBootstrapScript(cfg Config) []byte

GenerateBootstrapScript renders the first-logon bootstrap for a config.

func GenerateGuestDiagnosticsScript

func GenerateGuestDiagnosticsScript() []byte

GenerateGuestDiagnosticsScript returns the PowerShell run at first logon.

It locates its own volume by looking for the answer file rather than assuming a drive letter — Windows assigns those dynamically, and a wrong guess is exactly what made the NetKVM failure so hard to pin down.

func GenerateXML

func GenerateXML(cfg Config) []byte

GenerateXML produces a Windows unattended install XML.

func PadForFAT

func PadForFAT(data []byte) []byte

PadForFAT appends newlines until the payload is an exact multiple of the cluster size. go-diskfs v1.9.4 mis-records the directory-entry size of files ending near a cluster boundary — first measured as the last 63 bytes (6129-byte file), later disproven by a 14270-byte file corrupting 66 bytes short — so no partial-cluster tail is trusted; cluster-aligned files are the one class that has never mis-recorded. Trailing whitespace is legal after an XML root element, in PowerShell, and in UEFI .nsh scripts, so the padding is safe for every file we write. isokit.CreateFATImage still verifies the round-trip, so if even this assumption falls it fails loudly rather than silently shipping a corrupt image.

func ReadGuestDiagnostics

func ReadGuestDiagnostics(answerImagePath string) (string, error)

ReadGuestDiagnostics reads the report the guest wrote to the answer volume. A missing log is an error rather than an empty string: it means the guest never got as far as running the script, which is itself the finding.

func SessionUsername

func SessionUsername() string

SessionUsername returns the account name to create in the guest: the host's $USER, mirroring devcell's HOST_USER model (Docker's entrypoint and the tart engine derive their session user the same way), so a Windows cell has the same account as every other engine.

func Validate

func Validate(answerFile []byte) []error

Validate checks an answer file against the documented placement of the settings devcell relies on. It returns every problem found rather than stopping at the first, so one run surfaces all of them.

Unknown elements are ignored: this validates the settings we depend on, and does not attempt to be a complete schema.

func WriteISO deprecated

func WriteISO(xmlBytes []byte, destPath string) error

WriteISO creates a small ISO image containing autounattend.xml.

Deprecated: unusable for driving Windows Setup. CreateSimpleISO writes ISO 9660 Level 1 names, so the file lands as "AUTOUNAT.XML" and Setup — which searches for "autounattend.xml" and does not read the Rock Ridge extension that preserves the real name — silently ignores it. Use WriteImage; the FAT writer stores a long-filename entry.

func WriteImage

func WriteImage(xmlBytes []byte, destPath string) error

WriteImage creates a FAT32 disk image containing autounattend.xml and startup.nsh from pre-rendered XML. Use BuildAnswerVolume for real install volumes — it also ships the first-logon bootstrap the XML launcher expects; this raw-XML variant exists for validation tests.

Types

type Config

type Config struct {
	Username      string
	Password      string
	Locale        string
	Hostname      string
	VirtIODrivers []VirtIODriver
	SSHPubKey     string
	TimeZone      string
	// EnableRDP turns on Remote Desktop and disables the Windows firewall so
	// the forwarded RDP port is reachable. Gate this on Spec.RDPPort > 0 so
	// cells that never expose RDP do not get it.
	EnableRDP bool
	// WinPEAgent ships the WinPE control agent on the answer volume and adds
	// the windowsPE launcher that starts it. The agent snapshots Setup's
	// Panther logs to the volume every few seconds and executes commands the
	// host drops there — the only look inside a failing windowsPE phase.
	WinPEAgent bool
	// OpenSSHPayload is the filename of the Win32-OpenSSH release shipped on
	// the answer volume, empty when none was fetched.
	//
	// OpenSSH Server cannot be installed from our media: the capability is
	// present in the manifest but its payload is not, so Add-WindowsCapability
	// fails 0x80070002 with the capability stuck `Staged` — even with Windows
	// Update reachable and permitted (DISM logged LimitAccess:0). The UUP
	// package carries only OpenSSH-Client; the Server FoD ships on a separate
	// build-matched ISO we do not have. Shipping the standalone release side-
	// steps Windows servicing entirely.
	OpenSSHPayload string
	// OpenSSHPayloadData is the payload's bytes, written to the answer volume
	// by BuildAnswerVolume.
	OpenSSHPayloadData []byte
	// OpenSSHPayloadSize is the payload's true length. PadForFAT cluster-aligns
	// every file with trailing newlines, which is harmless for text but can
	// break a zip: readers locate the End-of-Central-Directory record by
	// scanning back from the end, and unexpected trailing bytes make that
	// inconsistent. The guest truncates to this length before extracting.
	OpenSSHPayloadSize int
	// PwshFiles is the extracted PowerShell 7 directory, keyed by answer-
	// volume path (e.g. "/pwsh/pwsh.exe"). Stock WinPE has no powershell.exe;
	// these files provide pwsh.exe which the bootstrap.cmd shim and the
	// windowsPE agent launcher probe for at runtime.
	PwshFiles map[string][]byte
	// ImageName selects which image in install.wim to install. The Windows 11
	// ARM64 media carries three (Home, Home Single Language, Pro); without a
	// choice Setup stops to ask. Defaults to "Windows 11 Pro".
	ImageName string
	// InstallWimPath, when set, tells Windows Setup to install from a custom
	// WIM at this path (e.g. "X:\devcell-install.wim") instead of the stock
	// install.wim inside the ISO. The path must be reachable from WinPE — a
	// drive letter assigned to a USB volume the VM mounts.
	InstallWimPath string
	// EFIBootLoader is the raw bytes of the Windows EFI bootloader
	// (BOOTAA64.EFI), extracted from the installer ISO at build time. When
	// set, BuildAnswerVolume writes it to /EFI/BOOT/BOOTAA64.EFI on the
	// answer FAT volume. This lets startup.nsh chainload the installer from
	// the USB volume (FS0) instead of the CD (FS1), working around QEMU
	// 11/HVF where the firmware can enumerate CD files but cannot execute
	// PE binaries from them (CELL-427).
	EFIBootLoader []byte
	// AnswerDrivers are driver files BuildAnswerVolume ships on the answer
	// volume, keyed by volume path (see LoadWinPEStorageDrivers). Every
	// .inf among them gets a windowsPE RunSynchronous drvload command —
	// see WinPEDriverLoads — which runs just before Modern Setup searches
	// for install media. ARM64 WinPE has no inbox vioscsi, so without this
	// the virtio-scsi installer CD is invisible and Setup stops at "a media
	// driver your computer needs is missing" (CELL-429).
	//
	// The files ship byte-exact, not PadForFAT-padded: Setup's driver
	// import validates them against the catalog's hashes.
	AnswerDrivers map[string][]byte
	// AgentCommand, when WinPEAgent is set, is pre-baked into the agent's
	// command file (winpe.AgentCommandFile) so the agent executes it on its first
	// poll and writes the combined output to devcell-out.txt — a one-shot
	// diagnostic channel into WinPE, which has no network and no QGA.
	AgentCommand string
}

Config holds parameters for generating an autounattend.xml file.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns sensible defaults for a devcell Windows VM.

func (Config) AgentLauncherOrder

func (c Config) AgentLauncherOrder() int

AgentLauncherOrder is the <Order> of the agent launcher command.

func (Config) SpecializeBootstrapCopyOrder

func (c Config) SpecializeBootstrapCopyOrder() int

SpecializeBootstrapCopyOrder returns the <Order> for the specialize command that copies the bootstrap script from the answer volume to C:\. It runs after all VirtIODriver installs.

func (Config) WinPEDriverLoads

func (c Config) WinPEDriverLoads() []WinPEDriverLoad

WinPEDriverLoads returns one drvload command per .inf in AnswerDrivers, numbered contiguously after the fixed commands and the agent launcher. One command per INF keeps each identical to the shape Setup is known to accept — see winpe.DriverLoadCommand.

type VirtIODriver

type VirtIODriver struct {
	// INFRelPath is the INF's path relative to the root of whatever volume
	// carries it (the virtio driver CD), without a drive letter — the letter
	// is probed at runtime. E.g. `NetKVM\w11\ARM64\netkvm.inf`.
	INFRelPath  string
	Description string
}

VirtIODriver describes a driver to install during Windows setup.

The driver is installed with pnputil in the specialize pass, not injected in windowsPE: a PnpCustomizationsWinPE DriverPaths entry whose path does not resolve ABORTS Setup (0x80070001 - 0x40030, run 20260729T172019), and WinPE drive letters are unpredictable, so no letter-based path is safe there. specialize runs in the full OS, where letters can be probed harmlessly with `if exist`.

func NetKVMDriverPaths

func NetKVMDriverPaths() []VirtIODriver

NetKVMDriverPaths returns the virtio-win NetKVM network driver for Windows ARM64.

Storage needs no injection — NVMe and USB CD are inbox — but the NIC is virtio-net-pci, for which Windows ARM64 has no inbox driver. Without it the installed guest has no network: no SSH, no winget, no WSL distro download. NetKVM is not boot-critical, so installing it post-apply in specialize is safe.

func VioserialDriverPaths

func VioserialDriverPaths() []VirtIODriver

VioserialDriverPaths returns the virtio-win vioserial driver for Windows ARM64.

The vioserial driver makes the virtio-serial port visible to Windows as \\.\Global\devcell.progress.0. Without it the bootstrap's Send-Progress writes to nothing: PL011 UART does not register as a COMx port on ARM64, and virtio-serial needs this driver. The WinPE phase loads it transiently via drvload, but that does not survive into the installed OS.

type WinPEDriverLoad

type WinPEDriverLoad struct {
	Order       int
	Path        string
	Description string
}

WinPEDriverLoad is one drvload RunSynchronousCommand.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL