Skip to content
Merged
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
15 changes: 15 additions & 0 deletions docs/phase4-platform-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ site-equivalent path without touching the public website. It creates an
unrelated WSL control distribution and proves uninstall retains it. It never
claims Windows artifact creation or signing.

### Retained host provisioning

`ops/platform-acceptance/windows-host/` tracks the bootstrap media for this host:
the answer file, the first-boot driver/guest-agent/OpenSSH setup, and a
reproducible ISO build. Its README records two host constraints that are easy to
get wrong. Proxmox always creates the TPM state volume as raw, and a raw volume
on directory storage blocks snapshots for the entire VM even when every other
disk is qcow2 — so the acceptance host cannot honour "restore the accepted clean
snapshot before routing" unless that small volume sits on snapshot-capable
storage. Verify with `qm snapshot <vmid> probe` before provisioning. Separately,
Windows 11 25H2 (build 26200) does not auto-apply an answer file from secondary
media and ignores `setup.exe /unattend:`; the README documents the offline
`dism /apply-image` path that still runs the specialize, oobeSystem and
auditUser passes.

## Recovery and teardown

First disable the applicable repository variable so new jobs cannot route.
Expand Down
42 changes: 24 additions & 18 deletions ops/platform-acceptance/macos.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

# Preserve exact artifact checks while making silent `[[ ... ]]` failures
# diagnosable from the job log. Without this, a failed launch/version/state
# assertion surfaces only as "exit code 1" after minutes of signature output.
trap 'status=$?; echo "::error::macOS acceptance failed at line ${LINENO} (exit ${status}): ${BASH_COMMAND}" >&2' ERR

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DOWNLOAD="${HELM_MAC_CANDIDATE_DOWNLOAD:?exact Mac candidate directory is required}"
OUTPUT="${HELM_ACCEPTANCE_OUTPUT:?acceptance output is required}"
Expand All @@ -27,6 +32,22 @@ node "$ROOT/scripts/pending-acceptance-evidence.mjs"
artifact_field() {
node -p 'const m=JSON.parse(require("fs").readFileSync(process.argv[1])); const a=m.artifacts.find(x=>x.role===process.argv[2]); if(!a)process.exit(2); a[process.argv[3]]' "$MANIFEST" "$1" "$2"
}
wait_for_setup_health() {
local output="$1" port
for _ in {1..180}; do
while IFS= read -r port; do
[[ "$port" =~ ^[0-9]+$ ]] || continue
if curl -fsS "http://127.0.0.1:$port/api/setup/status" >"$output.tmp"; then
mv "$output.tmp" "$output"
return 0
fi
rm -f -- "$output.tmp"
done < <(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null \
| awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]}' || true)
sleep 1
done
return 1
}
Comment on lines +35 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n ops/platform-acceptance/macos.sh | sed -n '1,90p'
printf '%s\n' '--- function usages ---'
rg -n -C 3 'wait_for_setup_health|setup/status|curl ' ops/platform-acceptance --glob '*.sh' || true
printf '%s\n' '--- shell metadata ---'
head -n 5 ops/platform-acceptance/macos.sh

Repository: gitcommit90/1Helm

Length of output: 12552


🏁 Script executed:

#!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
cat >"$tmp/lsof" <<'EOF'
#!/bin/sh
printf '1Helm 123 user 10u IPv4 0x0 0t0 TCP 127.0.0.1:45678 (LISTEN)\n'
EOF
cat >"$tmp/curl" <<'EOF'
#!/bin/sh
sleep 10
EOF
chmod +x "$tmp/lsof" "$tmp/curl"

cat >"$tmp/probe.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
wait_for_setup_health() {
  local output="$1" port
  for _ in {1..180}; do
    while IFS= read -r port; do
      [[ "$port" =~ ^[0-9]+$ ]] || continue
      if curl -fsS "http://127.0.0.1:$port/api/setup/status" >"$output.tmp"; then
        mv "$output.tmp" "$output"
        return 0
      fi
      rm -f -- "$output.tmp"
    done < <(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null \
      | awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]}' || true)
    sleep 1
  done
  return 1
}
wait_for_setup_health "$1"
EOF
chmod +x "$tmp/probe.sh"

start="$(date +%s)"
set +e
PATH="$tmp:$PATH" timeout 3 "$tmp/probe.sh" "$tmp/output"
status=$?
elapsed=$(( $(date +%s) - start ))
set -e
printf 'status=%s elapsed_seconds=%s\n' "$status" "$elapsed"
test "$status" -eq 124
test "$elapsed" -lt 5
printf '%s\n' 'A stalled curl prevents the function from reaching its polling loop timeout.'

Repository: gitcommit90/1Helm

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
cat >"$tmp/lsof" <<'EOF'
#!/bin/sh
printf '1Helm 123 user 10u IPv4 0x0 0t0 TCP 127.0.0.1:45678 (LISTEN)\n'
EOF
cat >"$tmp/curl" <<'EOF'
#!/bin/sh
sleep 10
EOF
chmod +x "$tmp/lsof" "$tmp/curl"

cat >"$tmp/probe.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
wait_for_setup_health() {
  local output="$1" port
  for _ in {1..180}; do
    while IFS= read -r port; do
      [[ "$port" =~ ^[0-9]+$ ]] || continue
      if curl -fsS "http://127.0.0.1:$port/api/setup/status" >"$output.tmp"; then
        mv "$output.tmp" "$output"
        return 0
      fi
      rm -f -- "$output.tmp"
    done < <(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null \
      | awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]}' || true)
    sleep 1
  done
  return 1
}
wait_for_setup_health "$1"
EOF
chmod +x "$tmp/probe.sh"

set +e
PATH="$tmp:$PATH" "$tmp/probe.sh" "$tmp/output" &
pid=$!
sleep 1
if kill -0 "$pid" 2>/dev/null; then
  kill "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true
  set -e
  printf '%s\n' 'The function remains blocked in curl after one second.'
  exit 0
fi
wait "$pid"
status=$?
set -e
printf 'Unexpected completion status: %s\n' "$status"
exit 1

Repository: gitcommit90/1Helm

Length of output: 263


Bound each health probe and enforce the polling deadline.

Line 40 calls curl without --connect-timeout or --max-time. A listening but unresponsive process can block indefinitely and prevent the function from reaching its timeout. Use a finite per-port timeout and an absolute deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ops/platform-acceptance/macos.sh` around lines 35 - 50, Update
wait_for_setup_health to apply finite curl connection and total-request timeouts
for each port probe, and enforce an absolute deadline across the polling loop so
an unresponsive listener cannot block beyond the function’s overall timeout.
Preserve the existing successful-response handling and temporary-file cleanup.

DMG_SHA="$(artifact_field mac_dmg sha256)"; ZIP_SHA="$(artifact_field mac_updater_zip sha256)"
[[ "$(shasum -a 256 "$DMG" | awk '{print $1}')" == "$DMG_SHA" ]]
[[ "$(shasum -a 256 "$ZIP" | awk '{print $1}')" == "$ZIP_SHA" ]]
Expand Down Expand Up @@ -68,12 +89,7 @@ spctl --assess --type execute --verbose=4 "$work/update/1Helm.app"
mkdir -p "$DATA_ROOT"
printf '%s\n' server >"$DATA_ROOT/desktop-mode"
open -n "$installed" --args --1helm-background
for _ in {1..180}; do
PORT="$(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null | awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]; exit}')"
[[ "$PORT" =~ ^[0-9]+$ ]] && curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >"$work/clean-health.json" && break
sleep 1
done
[[ -s "$work/clean-health.json" ]]
wait_for_setup_health "$work/clean-health.json"
osascript -e 'tell application id "com.gitcommit90.1helm" to quit' || true
for _ in {1..30}; do pgrep -x -U "$(id -u)" 1Helm >/dev/null || break; sleep 1; done
! pgrep -x -U "$(id -u)" 1Helm >/dev/null
Expand Down Expand Up @@ -122,25 +138,15 @@ printf '%s\n' server >"$DATA_ROOT/desktop-mode"
printf '%s\n' "phase4-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" >"$DATA_ROOT/phase4-acceptance-state"
STATE_BEFORE="$(shasum -a 256 "$DATA_ROOT/phase4-acceptance-state" | awk '{print $1}')"
open -n "$installed" --args --1helm-background
for _ in {1..180}; do
PORT="$(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null | awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]; exit}')"
[[ "$PORT" =~ ^[0-9]+$ ]] && curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >"$work/prior-health.json" && break
sleep 1
done
[[ -s "$work/prior-health.json" ]]
wait_for_setup_health "$work/prior-health.json"
osascript -e 'tell application id "com.gitcommit90.1helm" to quit' || true
for _ in {1..30}; do pgrep -x -U "$(id -u)" 1Helm >/dev/null || break; sleep 1; done
! pgrep -x -U "$(id -u)" 1Helm >/dev/null
rm -rf -- "$installed"
ditto "$work/update/1Helm.app" "$installed"
[[ "$(defaults read "$installed/Contents/Info" CFBundleShortVersionString)" == "$VERSION" ]]
open -n "$installed" --args --1helm-background
for _ in {1..180}; do
PORT="$(lsof -nP -a -u "$(id -un)" -c 1Helm -iTCP -sTCP:LISTEN 2>/dev/null | awk '/127\.0\.0\.1:/ {split($9,a,":"); print a[length(a)]; exit}')"
[[ "$PORT" =~ ^[0-9]+$ ]] && curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >"$work/update-health.json" && break
sleep 1
done
[[ -s "$work/update-health.json" ]]
wait_for_setup_health "$work/update-health.json"
Comment on lines +141 to +149

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- script header and relevant section ---'
sed -n '1,35p;125,160p' ops/platform-acceptance/macos.sh

printf '%s\n' '--- shell options and related process checks ---'
rg -n 'set -|errexit|pgrep|wait_for_setup_health|1Helm' ops/platform-acceptance/macos.sh

printf '%s\n' '--- shellcheck availability and targeted diagnostic ---'
if command -v shellcheck >/dev/null 2>&1; then
  shellcheck --version
  shellcheck --shell=bash --enable=all ops/platform-acceptance/macos.sh 2>&1 | sed -n '1,120p'
else
  echo 'shellcheck: unavailable'
fi

Repository: gitcommit90/1Helm

Length of output: 13139


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- wait_for_setup_health implementation ---'
sed -n '35,60p' ops/platform-acceptance/macos.sh

printf '%s\n' '--- standalone errexit probe ---'
bash -c '
  set -euo pipefail
  trap '\''status=$?; printf "ERR trap: status=%s command=%s\n" "$status" "$BASH_COMMAND" >&2'\'' ERR
  pgrep() { return 0; }
  printf "before\n"
  ! pgrep -x -U "$(id -u)" 1Helm >/dev/null
  printf "after negated check: status=%s\n" "$?"
  printf "replacement would run\n"
'

Repository: gitcommit90/1Helm

Length of output: 1277


Fail when the 1Helm process is still running.

When pgrep finds 1Helm, Bash does not apply errexit to the negated command. The script then replaces the app while the old process is still running. Use an explicit conditional that exits with an error.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 144-144: This ! is not on a condition and skips errexit. Use && exit 1 instead, or make sure $? is checked.

(SC2251)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ops/platform-acceptance/macos.sh` around lines 141 - 149, Update the 1Helm
process-wait logic in the macOS acceptance flow to use an explicit conditional
after the polling loop: if pgrep still finds the process, emit an error and exit
nonzero before removing or replacing the installed app. Preserve the existing
wait loop and continue only when 1Helm has stopped.

Source: Linters/SAST tools

STATE_AFTER="$(shasum -a 256 "$DATA_ROOT/phase4-acceptance-state" | awk '{print $1}')"
[[ "$STATE_BEFORE" == "$STATE_AFTER" ]]
osascript -e 'tell application id "com.gitcommit90.1helm" to quit' || true
Expand Down
2 changes: 2 additions & 0 deletions ops/platform-acceptance/windows-host/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The per-site operator public key is never committed to this public repository.
authorized_key.pub
50 changes: 50 additions & 0 deletions ops/platform-acceptance/windows-host/Autounattend.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<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-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1"><Path>E:\vioscsi\w11\amd64</Path></PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2"><Path>E:\NetKVM\w11\amd64</Path></PathAndCredentials>
</DriverPaths>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DiskConfiguration>
<Disk wcm:action="add"><DiskID>0</DiskID><WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add"><Order>1</Order><Type>EFI</Type><Size>260</Size></CreatePartition>
<CreatePartition wcm:action="add"><Order>2</Order><Type>MSR</Type><Size>16</Size></CreatePartition>
<CreatePartition wcm:action="add"><Order>3</Order><Type>Primary</Type><Extend>true</Extend></CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add"><Order>1</Order><PartitionID>1</PartitionID><Format>FAT32</Format><Label>System</Label></ModifyPartition>
<ModifyPartition wcm:action="add"><Order>2</Order><PartitionID>3</PartitionID><Format>NTFS</Format><Label>Windows</Label></ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<ImageInstall><OSImage><InstallFrom><MetaData wcm:action="add"><Key>/IMAGE/INDEX</Key><Value>6</Value></MetaData></InstallFrom><InstallTo><DiskID>0</DiskID><PartitionID>3</PartitionID></InstallTo></OSImage></ImageInstall>
<UserData><AcceptEula>true</AcceptEula><FullName>1Helm</FullName><Organization>1Helm acceptance</Organization></UserData>
<DynamicUpdate><Enable>false</Enable><WillShowUI>OnError</WillShowUI></DynamicUpdate>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>ONEHELM-WIN</ComputerName><TimeZone>UTC</TimeZone><RegisteredOwner>1Helm</RegisteredOwner><RegisteredOrganization>1Helm acceptance</RegisteredOrganization>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<Reseal><Mode>Audit</Mode></Reseal>
</component>
</settings>
<settings pass="auditUser">
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add"><Order>1</Order><Description>Provision retained 1Helm acceptance host</Description><Path>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$v=(Get-Volume -FileSystemLabel ONEHELM); &amp; ($v.DriveLetter + ':\setup.ps1')"</Path><WillReboot>Never</WillReboot></RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
</unattend>
75 changes: 75 additions & 0 deletions ops/platform-acceptance/windows-host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Retained Windows acceptance-host provisioning

These files build the bootstrap ISO for the dedicated Windows 11 Phase 4
acceptance host described in `docs/phase4-platform-acceptance.md`. They were
previously carried only as a pre-built ISO on the hypervisor, so a rebuild could
not be reviewed or reproduced. They are tracked here for that reason.

Nothing here publishes a release, creates a tag, deploys the website, or touches
production data or services.

## What it does

`Autounattend.xml` partitions the disk (GPT, EFI + MSR + NTFS), installs the Pro
image, sets the computer name and UTC timezone, then reseals into **audit mode**
and runs `setup.ps1` during the `auditUser` pass.

`setup.ps1` installs the VirtIO network and serial drivers plus the QEMU guest
agent, enables OpenSSH Server pinned to the operator's public key (supplied at build
time, see below) with password authentication off, disables sleep and hibernate, and records
`C:\1HelmAcceptance\ready.json`. It is idempotent: an existing `ready.json`
makes it exit 0 immediately.

## Build

```sh
./build-unattend-iso.sh 1helm-windows-unattend.iso /path/to/operator_key.pub
```

The operator public key is **not** committed: this repository is public, and
publishing which key is authorized as Administrator on the acceptance host is
needless disclosure. Pass it at build time (or via
`HELM_ACCEPTANCE_AUTHORIZED_KEY`); the build stages it as `authorized_key.pub`
on the ISO and `setup.ps1` refuses to continue without it. `.gitignore` keeps a
local copy out of git.

Two constraints are load-bearing:

- `-iso-level 4` — at genisoimage's default level the ISO9660 namespace
truncates `Autounattend.xml` to `AUTOUNAT.XML`, which Windows Setup does not
recognize as an answer file.
- Volume label `ONEHELM` — the `auditUser` pass locates `setup.ps1` with
`Get-Volume -FileSystemLabel ONEHELM`.

## Host VM shape

The acceptance workflow restores an accepted clean snapshot before every job, so
the VM must actually be able to snapshot. Proxmox always creates the **TPM state
volume as raw**, and a raw volume on directory storage blocks snapshots for the
whole VM even when every other disk is qcow2. Place the disks as qcow2 and put
the small `tpmstate0` volume on snapshot-capable storage (thin-LVM), which keeps
TPM 2.0 and Secure Boot intact so no Windows 11 requirement bypass is needed.

Verify before installing anything:

```sh
qm snapshot <vmid> probe && qm delsnapshot <vmid> probe
```

## Windows 11 25H2 answer-file caveat

On build 26200 (25H2) the new setup engine (`setuphost.exe`) does **not**
auto-apply `Autounattend.xml` from a secondary disc, and `setup.exe /unattend:`
is ignored. The answer file is readable from WinPE — it simply is not consumed,
so setup falls through to the interactive product-key page.

Working alternative on affected media: from the WinPE shell (Shift+F10),
partition with `diskpart`, apply the image with
`dism /apply-image /imagefile:<media>:\sources\install.wim /index:6 /applydir:W:\`,
copy `Autounattend.xml` to `W:\Windows\Panther\unattend.xml`, then
`bcdboot W:\Windows /s S: /f UEFI`. First boot still runs `specialize`,
`oobeSystem` and `auditUser`, so the computer name, audit reseal and
`setup.ps1` all still apply.

Note that WinPE on this media has no `curl`, no `taskkill`, and no configured
network, so stage anything you need on the bootstrap ISO itself.
36 changes: 36 additions & 0 deletions ops/platform-acceptance/windows-host/build-unattend-iso.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Build the retained Windows acceptance-host bootstrap ISO.
#
# Attach the result as a second CD-ROM alongside the Windows installation media
# on the dedicated Phase 4 acceptance VM. It carries the answer file and the
# first-boot bootstrap; it publishes nothing and contains no credentials beyond
# the operator's own SSH public key in setup.ps1.
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
out="${1:-1helm-windows-unattend.iso}"
key="${2:-${HELM_ACCEPTANCE_AUTHORIZED_KEY:-}}"

if [ -z "$key" ] || [ ! -f "$key" ]; then
echo "usage: build-unattend-iso.sh [out.iso] <path-to-authorized-key.pub>" >&2
echo " or: HELM_ACCEPTANCE_AUTHORIZED_KEY=/path/to/key.pub build-unattend-iso.sh [out.iso]" >&2
echo "The operator public key is deliberately not committed to this public repository." >&2
exit 2
fi

stage="$(mktemp -d)"
trap 'rm -rf "$stage"' EXIT

cp "$here/Autounattend.xml" "$here/setup.ps1" "$stage/"
printf 'Retained Windows acceptance bootstrap for 1Helm.\n' > "$stage/1helm-acceptance.txt"
install -m 0644 "$key" "$stage/authorized_key.pub"

# -iso-level 4 keeps long file names. At genisoimage's default level the
# ISO9660 namespace truncates Autounattend.xml to AUTOUNAT.XML, which Windows
# Setup does not recognize as an answer file.
#
# The volume label MUST remain ONEHELM: the auditUser pass in Autounattend.xml
# locates setup.ps1 via Get-Volume -FileSystemLabel ONEHELM.
genisoimage -quiet -iso-level 4 -J -r -V ONEHELM -o "$out" "$stage"

echo "wrote $out"
62 changes: 62 additions & 0 deletions ops/platform-acceptance/windows-host/setup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
$ErrorActionPreference = "Stop"
$root = "C:\1HelmAcceptance"
New-Item -ItemType Directory -Force -Path $root | Out-Null
$log = Join-Path $root "bootstrap.log"
Start-Transcript -Path $log -Append
try {
if (Test-Path (Join-Path $root "ready.json")) { exit 0 }

$virtio = Get-Volume | Where-Object { $_.DriveLetter -and (Test-Path ("{0}:\guest-agent\qemu-ga-x86_64.msi" -f $_.DriveLetter)) } | Select-Object -First 1
if (-not $virtio) { throw "VirtIO 0.1.271 media was not found." }
$drive = "{0}:" -f $virtio.DriveLetter
& pnputil.exe /add-driver "$drive\NetKVM\w11\amd64\*.inf" /subdirs /install
if ($LASTEXITCODE -notin 0, 3010) { throw "NetKVM driver install failed: $LASTEXITCODE" }
# The QEMU guest agent reaches the host over a VirtIO serial port. Without
# this driver the QEMU-GA service still starts and reports Running, while the
# host side ("qm agent <vmid> ping") stays dead and there is no guest-exec
# channel to provision the host with.
& pnputil.exe /add-driver "$drive\vioserial\w11\amd64\*.inf" /subdirs /install
if ($LASTEXITCODE -notin 0, 3010) { throw "VirtIO serial driver install failed: $LASTEXITCODE" }
& msiexec.exe /i "$drive\guest-agent\qemu-ga-x86_64.msi" /qn /norestart
if ($LASTEXITCODE -notin 0, 3010) { throw "QEMU guest agent install failed: $LASTEXITCODE" }

$capability = Get-WindowsCapability -Online | Where-Object Name -Like "OpenSSH.Server*" | Select-Object -First 1
if (-not $capability) { throw "Windows did not expose the OpenSSH Server capability." }
if ($capability.State -ne "Installed") { Add-WindowsCapability -Online -Name $capability.Name | Out-Null }

New-Item -ItemType Directory -Force -Path "C:\ProgramData\ssh" | Out-Null
# The authorized key is supplied per site on the bootstrap media rather than
# committed to this public repository. build-unattend-iso.sh stages it.
$keySource = Join-Path $PSScriptRoot "authorized_key.pub"
if (-not (Test-Path $keySource)) { throw "authorized_key.pub is missing from the bootstrap media; see ops/platform-acceptance/windows-host/README.md" }
$authorizedKey = (Get-Content -Raw $keySource).Trim()
if ($authorizedKey -notmatch '^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp[0-9]+) ') { throw "authorized_key.pub is not an OpenSSH public key" }
Set-Content -Encoding ascii -Path "C:\ProgramData\ssh\administrators_authorized_keys" -Value $authorizedKey
& icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F" | Out-Null
$config = "C:\ProgramData\ssh\sshd_config"
if (Test-Path $config) {
$text = Get-Content -Raw $config
$text = [regex]::Replace($text, '(?m)^\s*#?\s*PasswordAuthentication\s+.*$', 'PasswordAuthentication no')
$text = [regex]::Replace($text, '(?m)^\s*#?\s*PubkeyAuthentication\s+.*$', 'PubkeyAuthentication yes')
Set-Content -Encoding ascii -Path $config -Value $text
}
Set-Service sshd -StartupType Automatic
Start-Service sshd
# Add-WindowsCapability already creates OpenSSH-Server-In-TCP, but scoped to
# the Private profile only. A freshly bridged VM is categorized Public, so a
# bare existence check short-circuits and leaves port 22 unreachable even
# though sshd reports Running. Ensure the rule exists AND covers every profile.
if (Get-NetFirewallRule -Name OpenSSH-Server-In-TCP -ErrorAction SilentlyContinue) {
Set-NetFirewallRule -Name OpenSSH-Server-In-TCP -Enabled True -Profile Any
} else {
New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName "OpenSSH Server (sshd)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -Profile Any | Out-Null
}
Set-Service QEMU-GA -StartupType Automatic
Start-Service QEMU-GA
powercfg.exe /change standby-timeout-ac 0 | Out-Null
powercfg.exe /change hibernate-timeout-ac 0 | Out-Null
$record = [ordered]@{ ready = $true; computer = $env:COMPUTERNAME; build = [Environment]::OSVersion.Version.ToString(); at = (Get-Date).ToUniversalTime().ToString("o") }
Set-Content -Encoding utf8 -Path (Join-Path $root "ready.json") -Value ($record | ConvertTo-Json -Compress)
} finally {
Stop-Transcript
}
Loading
Loading