Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ you supply, and minimizes the size of the installation disk once setup is
complete. You can then upload the installation disk to an Oxide rack and attach
it to a VM or use it as the source disk for a new disk image.

> [!NOTE]
> On Linux hosts, this repository also provides a [Packer
> template](packer/README.md) that reproduces the `wimsy` build flow using
> HashiCorp Packer. Building images on illumos hosts is still done with
> `wimsy`.

`wimsy` runs on Linux (tested on Ubuntu 20.04) and illumos systems and supports
creating Windows Server 2019 and Windows Server 2022 images. Windows Server
2016 is not yet fully supported (but it's on the roadmap). Earlier versions of
Expand Down
6 changes: 6 additions & 0 deletions packer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
output/
output-*/
efivars.fd
packer_cache/
crash.log
*.auto.pkrvars.hcl
86 changes: 86 additions & 0 deletions packer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Packer template for Oxide Windows images

This directory contains a [Packer](https://developer.hashicorp.com/packer)
template that builds a generalized Windows Server image suitable for import
into an Oxide rack. It is the successor to the `wimsy` Rust tool in this
repository and reproduces its build flow:

1. Create a blank raw disk and boot Windows Setup in a QEMU/KVM guest that
matches what an Oxide rack presents: an NVMe boot disk (512-byte sectors)
and a virtio-net NIC.
2. Drive an unattended installation via a rendered `Autounattend.xml`
(delivered on a virtual floppy), staging the virtio NetKVM/viostor drivers
from the driver ISO during the `offlineServicing` pass.
3. Provision over WinRM: enable the EMS serial console, ping, and RDP;
install OpenSSH and the Oxide fork of cloudbase-init; run disk cleanup and
shrink the OS partition.
4. De-provision (reset WinRM to defaults, remove autologon credentials,
scramble the build password) and generalize with sysprep, which disables
the Administrator account on first boot and hands configuration over to
cloudbase-init.
5. Trim the raw image down to the end of the OS partition, rebuild the
secondary GPT, and sparsify. The result is `output/windows-server.raw`
plus a `.sha256` checksum ready for `oxide disk import`.

## Prerequisites

The build must run on a Linux host with KVM. Required tools:

* `packer` (>= 1.10) with the QEMU plugin (installed by `packer init`,
which `build.sh` runs for you)
* `qemu-system-x86_64` and `qemu-img`
* `sgdisk` (from `gdisk`) to trim the output image
* OVMF UEFI firmware (`edk2-ovmf` / `ovmf` package)

You also need:

* A Windows Server ISO (2016/2019/2022/2025). See the repository README for
ISO requirements.
* A virtio driver ISO using the Fedora directory layout
(`NetKVM/<version>/amd64`, `viostor/<version>/amd64`), e.g.
[virtio-win.iso](https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/).

The guest needs outbound Internet access (Packer's user-mode networking) to
download OpenSSH and cloudbase-init.

## Usage

```sh
./build.sh \
-var windows_iso_path=/path/to/windows_server_2022.iso \
-var virtio_iso_path=/path/to/virtio-win.iso
```

Commonly overridden variables (see [variables.pkr.hcl](variables.pkr.hcl) for
the full list):

| Variable | Default | Purpose |
| --- | --- | --- |
| `windows_iso_path` | (required) | Windows Server installation ISO |
| `virtio_iso_path` | (required) | virtio driver ISO |
| `windows_iso_checksum` | `none` | ISO checksum verification (`sha256:...`) |
| `windows_version` | `2k22` | virtio driver directory (`2k16`/`2k19`/`2k22`/`2k25`) |
| `image_index` | `2` | Windows edition index in the ISO |
| `ovmf_code_path` / `ovmf_vars_path` | Arch paths | OVMF firmware location |
| `headless` | `true` | Set `false` to watch the installer |
| `winrm_password` | `Packer!build0` | Build-time Administrator password (scrambled before capture) |

To debug a failing build, pass `-var headless=false` to watch the console,
and add `-on-error=ask` to keep the VM around on failure. EMS serial output
is forwarded to Packer's stdout during the build.

## Notes and differences from wimsy

* The final image contains no build credentials or build machinery: the
Administrator password is scrambled, autologon/WinRM build settings are
reset, and the sysprep scheduled task and provisioner temp files are
removed by [scripts/sysprep.ps1](scripts/sysprep.ps1) before sysprep runs.
A `C:\Users\Administrator` profile folder exists in the image, as it did
in wimsy-built images (wimsy's audit-mode session signed in as
Administrator too); the account itself is disabled on first boot in both
flows.
* The unattend collateral in [../unattend](../unattend) (`specialize-unattend.xml`,
cloudbase-init configs) is shared with wimsy and delivered on the build
floppy.
* Building images on illumos hosts (wimsy's `build-installation-disk` /
Propolis flow) is not supported by this template; use wimsy for that.
171 changes: 171 additions & 0 deletions packer/answer_files/Autounattend.pkrtpl.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DiskConfiguration>
<Disk wcm:action="add">
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Size>500</Size>
<Type>Primary</Type>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Size>100</Size>
<Type>EFI</Type>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Size>128</Size>
<Type>MSR</Type>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>4</Order>
<Extend>true</Extend>
<Type>Primary</Type>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Format>NTFS</Format>
<Label>Recovery</Label>
<TypeID>DE94BBA4-06D1-4D40-A16A-BFD50179D6AC</TypeID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>2</PartitionID>
<Format>FAT32</Format>
<Label>System</Label>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>3</Order>
<PartitionID>3</PartitionID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>4</Order>
<PartitionID>4</PartitionID>
<Format>NTFS</Format>
<Letter>C</Letter>
<Label>OS</Label>
</ModifyPartition>
</ModifyPartitions>
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
</Disk>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/INDEX</Key>
<Value>${image_index}</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>4</PartitionID>
</InstallTo>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<ProductKey>
<WillShowUI>Never</WillShowUI>
</ProductKey>
</UserData>
</component>
</settings>
<settings pass="offlineServicing">
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>D:\NetKVM\${windows_version}\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>D:\viostor\${windows_version}\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
<Path>E:\NetKVM\${windows_version}\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>E:\viostor\${windows_version}\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>F:\NetKVM\${windows_version}\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
<Path>F:\viostor\${windows_version}\amd64</Path>
</PathAndCredentials>
</DriverPaths>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ComputerName>*</ComputerName>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>1</ProtectYourPC>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>${admin_password}</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Password>
<Value>${admin_password}</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>1</LogonCount>
<Username>Administrator</Username>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<CommandLine>cmd /c powershell.exe -NoProfile -ExecutionPolicy Bypass -File A:\setup-winrm.ps1</CommandLine>
<Order>1</Order>
<Description>Configure WinRM for Packer provisioning</Description>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>
10 changes: 10 additions & 0 deletions packer/build.sh

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's how I ran this. Just noting this for future me to document it.

Download the virtio ISO.

curl -LO https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso

Download the Windows Server 2022 ISO: https://go.microsoft.com/fwlink/p/?linkid=2195686&clcid=0x409&culture=en-us&country=us

Run Packer.

./build.sh \
    -var "windows_iso_path=$HOME/Documents/windows-server-2022-evaluation.iso" \
    -var "virtio_iso_path=$HOME/Documents/virtio-win.iso" \
    -var "headless=false"

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

packer init .

# Forward all arguments to packer build.
exec packer build "$@" .
49 changes: 49 additions & 0 deletions packer/scripts/install-cloudbase-init.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
$ErrorActionPreference = 'Stop'

function RetryWithBackoff {
param (
[Parameter(Mandatory=$True)]
[scriptblock]$ScriptBlock,

[Parameter(Mandatory=$False)]
[int]$MaxAttempts = 5,

[Parameter(Mandatory=$False)]
[int]$InitialBackoffDelayMs = 1000,

[Parameter(Mandatory=$False)]
[int]$MaxBackoffDelayMs = 30000
)

$cmd = $ScriptBlock.ToString()
$cnt = 0
$delay = $InitialBackoffDelayMs
do {
$cnt++
try {
Invoke-Command -Command $ScriptBlock
return
} catch {
Write-Host "Command $cmd failed, will retry after $delay ms; error: " $_.Exception.InnerException.Message
Start-Sleep -Milliseconds $delay
$delay = [math]::Min($delay * 2, $MaxBackoffDelayMs)
}
} while ($cnt -lt $MaxAttempts)

Write-Error -Message "Command $cmd failed after $MaxAttempts attempts" -ErrorAction Stop
}

Write-Host "Installing cloudbase-init"
RetryWithBackoff -ScriptBlock { Invoke-WebRequest -Uri https://oxide-omicron-build.s3.amazonaws.com/CloudbaseInitSetup.msi -OutFile C:\Windows\Temp\CloudbaseInitSetup.msi | Out-Null }
Start-Process msiexec.exe -ArgumentList "/i C:\Windows\Temp\CloudbaseInitSetup.msi /qn /norestart RUN_SERVICE_AS_LOCAL_SYSTEM=1" -Wait
Remove-Item C:\Windows\Temp\CloudbaseInitSetup.msi

# Copy cloudbase-init configuration from the floppy drive (A:\).
$confPath = "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\"
Copy-Item "A:\cloudbase-init.conf" -Destination "$confPath\cloudbase-init.conf"
Copy-Item "A:\cloudbase-init-unattend.conf" -Destination "$confPath\cloudbase-init-unattend.conf"
Remove-Item "$confPath\Unattend.xml"

# Disable the service so it doesn't run on first boot and contend with the
# unattend first pass. Re-enabled during the specialize phase.
Set-Service -Name cloudbase-init -StartupType Disabled
Loading
Loading