Skip to content

staging: add vm-lab - Photon OS VM provisioning for VMware Workstation - #316

Open
dcasota wants to merge 2 commits into
masterfrom
staging/add-vm-lab
Open

staging: add vm-lab - Photon OS VM provisioning for VMware Workstation#316
dcasota wants to merge 2 commits into
masterfrom
staging/add-vm-lab

Conversation

@dcasota

@dcasota dcasota commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Adds staging/vm-lab/ — everything needed to go from nothing to a running,
verified, SSH-reachable Photon OS VM on VMware Workstation.

What is in it

config/vm-lab.env one file holding every path, size, IP and MAC
config/spagat-smoke.vmx.template VMX with each non-default key annotated with the failure it prevents
kickstart/ byte-exact copy of the ISO build's kickstart template + a drift check
scripts/00-preflight.sh can this host do the job?
scripts/10-create-vm.ps1 VM dir + boot VMDK + VMX from the template
scripts/20-make-ssh-key.sh keypair + the exports the ISO build needs
scripts/30-install-from-iso.sh unattended install
scripts/40-check-staging.sh did the rootfs / ISO / guest actually get what I think?
scripts/50-verify-boot.sh is it alive, and did this boot do anything?
scripts/60-ssh.sh connect, and explain failures instead of just refusing
scripts/90-teardown.ps1 back to a fresh disk — stashes, never deletes

Defaults: 2 vCPU, 4 GB RAM, 50 GB thin disk in a SINGLE FILE.

The disk is monolithicSparse (vmware-vdiskmanager -t 0), so 50 GB is a
ceiling rather than an allocation. Verified by creating one: a fresh 50 GB
disk is one file of 6,619,136 bytes, with the extent line
RW 104857600 SPARSE "single.vmdk" referencing itself. -t 1 would store the
same data across 2 GB extents (-s001.vmdk, -s002.vmdk, ...) - more files
for the same bytes, and the split form is the one that tends to leave orphaned
extents behind after a teardown.

Every value is overridable per run:

GUEST_VCPUS=4 powershell -File scripts/10-create-vm.ps1

The two things most likely to cost a day

SSH access is decided at ISO build time. SPAGAT_OPERATOR_AUTHORIZED_KEY
and IPHASE6_TEST_SSH_PUBKEY both default to empty, and the root password
hash defaults to *. A build where neither was exported ships an appliance
with correct 0600 permissions on an empty authorized_keys and no root
password — the serial console is the only way in, and there is no post-install
fix. 40-check-staging.sh --iso <path> extracts the kickstart out of the
finished ISO and prints the keys it actually contains, so this is verifiable
rather than hopeful.

The install CDROM must be on SATA. The linux-mok kernel has no IDE CDROM
driver, so on ide1:0 the installer boots but userspace mount /mnt/media
finds no /dev/sr0 and fails looking for the volume label — which reads like
a corrupt ISO.

The VMX template also pins the BIOS UUID next to the MAC on purpose: with
ethernet0.addressType="generated" VMware derives the MAC from the UUID, so
dropping it regenerates both and moves the guest's IP.

Verification

Every script was executed, not just written:

  • bash -n clean on all 7 shell scripts; both .ps1 parse and run under
    Windows PowerShell 5.1
  • 00-preflight.sh, 50-verify-boot.sh, 40-check-staging.sh and
    check-drift.sh run against real artifacts
  • config parses identically in bash and in the PowerShell parser; no parsed
    value contains a stray #
  • every VMX placeholder has a matching substitution
  • run from /tmp with no way to find its config, a script exits 78 with
    instructions rather than half-running

Four defects were found and fixed during that testing, all of the same family
— a check that looked like it passed while proving nothing:

  1. The .ps1 files were unrunnable under Windows PowerShell 5.1. They
    parsed fine under pwsh 7, which decodes UTF-8; 5.1 reads .ps1 as ANSI
    without a BOM, so an em-dash corrupted a string literal and produced a
    cascade of misleading brace errors. Both files are now ASCII-only.
  2. The env parser kept trailing comments, so BOOT_DISK_ADAPTER resolved
    to "lsilogic" # what the initrd expects and would have been handed to
    vmware-vdiskmanager verbatim. bash strips those for free, which is why
    the WSL-side test passed and hid it.
  3. A config that could not be loaded did not stop the script — it printed
    a healthy-looking first section and then died on unbound variables. Now
    fails closed.
  4. grep -c PAT || echo 0 emitted "0\n0" (grep prints 0 and exits
    1), so counters rendered as two lines. Replaced with n=$(grep -c …) || n=0.

Provenance

vm-lab/ is a snapshot of deploy/vm-lab/ from
dcasota/SpagatLibrarian-Appliance @ b7ec6954. PROVENANCE.md records the
commit and the kickstart hash.

kickstart/check-drift.sh works in both homes and says which mode it used: it
diffs against the live canonical template when a SPAGAT checkout is reachable
(SPAGAT_REPO=…), and otherwise falls back to the recorded EXPECTED-SHA256
— which catches an edited local copy but cannot see upstream moving, a limit
it states rather than glossing over. If neither is available it exits non-zero,
because "cannot check" must never read as "fine".

🤖 Generated with Claude Code

@dcasota
dcasota force-pushed the staging/add-vm-lab branch from b55c253 to 5c597ee Compare August 31, 2026 10:36
End-to-end: VMX template, boot VMDK, kickstart, unattended install, SSH
access, teardown, and the verification scripts that say whether each stage
actually did what it claims.

Defaults: 2 vCPU, 4 GB RAM, 50 GB thin disk in a SINGLE FILE
(monolithicSparse). Verified by creating one - a fresh 50 GB disk is one file
of 6,619,136 bytes with the extent line RW 104857600 SPARSE referencing
itself. The size is a ceiling, not an allocation.

Two things the README documents because each has cost a day elsewhere:

  * SSH access is decided at ISO BUILD time. SPAGAT_OPERATOR_AUTHORIZED_KEY
    and IPHASE6_TEST_SSH_PUBKEY both default to empty and the root hash to
    '*', so a build where neither was exported ships an empty
    authorized_keys and a locked root - the serial console is the only way
    in, with no post-install fix.
  * The install CDROM must be on SATA: the linux-mok kernel has no IDE CDROM
    driver, so on ide1:0 the installer boots but mount /mnt/media finds no
    /dev/sr0, which reads like a corrupt ISO.

Every script was executed, not just written. Four defects were found and
fixed in the process, all the same family - a check that looked like it
passed while proving nothing:

  1. Both .ps1 files were unrunnable under Windows PowerShell 5.1. They
     parsed under pwsh 7 (UTF-8); 5.1 reads .ps1 as ANSI without a BOM, so an
     em-dash corrupted a string literal. Now ASCII-only.
  2. The env parser kept trailing comments, so BOOT_DISK_ADAPTER resolved to
     'lsilogic"  # what the initrd expects' and would have been passed to
     vmware-vdiskmanager verbatim. bash strips those for free, which is why
     the WSL-side test passed and hid it.
  3. A config that could not be loaded did not stop the script - it printed a
     healthy-looking first section then died on unbound variables. Now fails
     closed with exit 78.
  4. grep -c PAT || echo 0 emitted "0\n0" (grep prints 0 AND exits 1).
     Replaced with n=$(grep -c ...) || n=0.

Snapshot of deploy/vm-lab/ from dcasota/SpagatLibrarian-Appliance @ b7ec6954;
see staging/vm-lab/PROVENANCE.md. kickstart/check-drift.sh works in both homes
and states which mode it used.
@dcasota
dcasota force-pushed the staging/add-vm-lab branch from 5c597ee to afad115 Compare August 31, 2026 10:36
The documented per-run overrides did not work.
  config/vm-lab.env assigned every variable unconditionally
  (VM_NAME="spagat-smoke"), and every .sh script sources it after startup, so
  an exported value was overwritten. README's own examples - VM_NAME=my-lab
  ./scripts/00-preflight.sh and GUEST_VCPUS=4 powershell -File
  scripts/10-create-vm.ps1 - therefore did nothing. Converted all 24
  assignments to : "${VAR:=default}" form; values are byte-identical to
  before, including the backslash-bearing Windows paths.

The PowerShell side could not be overridden at all.
  Both .ps1 files parse the .env into a hashtable and never consulted $env:.
  Their shared parser now accepts the new form (and still the old one, so a
  stale copy keeps working) and prefers an environment value. Verified by
  running the parser: VM_NAME=perm-p03 now yields
  VM_DIR_WIN=C:\spagat-iso-build\vm\perm-p03.

The BIOS UUID was hardcoded in the VMX template, not a placeholder.
  Every VM built from the template shared one UUID - and because
  ethernet0.addressType is "generated", VMware derives the MAC from it, so
  they also shared one MAC and one DHCP lease. It is now @@UUID_BIOS@@ fed
  from GUEST_UUID_BIOS, defaulting to the previous value so existing VMs are
  unaffected, and 10-create-vm.ps1 warns when the last three bytes of MAC and
  UUID disagree.

90-teardown.ps1 enumerated the snapshot chain by name.
  The fixed list covered exactly two deltas and two .vmsn files, so a VM that
  had reached -000003.vmdk left an orphan - which defeats the file's own
  stated rationale, since UEFI's removable-media fallback then finds the
  surviving ESP and boots the previous image. Now globbed by pattern.

50-verify-boot.sh:87 had a broken ANSI strip.
  s/\x1b\[[0-9;]*g//g - trailing g where line 83 correctly uses m, so SGR
  sequences were not stripped from the failed-units section.

20-make-ssh-key.sh did not notice a failed ssh-keygen.
  No set -e, so it fell through to cat "$KEY.pub" and reported a missing
  file instead of the real cause. Now exits 5.

Also documented, not changed: GUEST_GATEWAY is read by nothing, because the
gateway and DNS are hardcoded inside the kickstart template even though the
address is templated. That file is byte-exact and policed by check-drift.sh,
so the fix belongs upstream; the variable now says so.

Verified: bash -n clean on all 7 shell scripts, both .ps1 parse under pwsh
and the shared parser was executed against the real .env, kickstart sha256
unchanged so check-drift.sh still passes, both .ps1 still ASCII-only with
CRLF endings.

Signed-off-by: Daniel Casota <dcasota@gmail.com>
@dcasota

dcasota commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Pushed 001d26b — six defects found while reusing this directory as the basis for a Photon permutation matrix. Each was verified in the tree before being changed, and the whole set re-verified after.

The documented per-run overrides did not work

config/vm-lab.env assigned unconditionally (VM_NAME="spagat-smoke"), and every .sh sources it after startup — so an exported value was overwritten. The README's own examples were therefore inert:

VM_NAME=my-lab BOOT_DISK_SIZE=30GB ./scripts/00-preflight.sh   # README:326
GUEST_VCPUS=4 powershell -File scripts/10-create-vm.ps1        # README:341

All 24 assignments converted to : "${VAR:=default}". Values are byte-identical to before — I diffed every one, including the backslash-bearing Windows paths, since moving 'C:\...' from single into double quotes is exactly where that would break.

The PowerShell side could not be overridden at all

Both .ps1 parse the .env into a hashtable and never consulted $env:. Their shared parser now accepts the new form (and still the old one, so a stale copy keeps working) and prefers an environment value. Not just parse-checked — I ran it:

defaults:                 VM_NAME = spagat-smoke   VM_DIR_WIN = C:\spagat-iso-build\vm\spagat-smoke
VM_NAME=perm-p03 ...:     VM_NAME = perm-p03       VM_DIR_WIN = C:\spagat-iso-build\vm\perm-p03

The BIOS UUID was hardcoded, not a placeholder

uuid.bios/uuid.location were literals in the template, so every VM built from it shared one UUID — and since ethernet0.addressType="generated", VMware derives the MAC from the UUID, so they shared one MAC and one DHCP lease too. That is the failure the README's own "BIOS UUID must stay next to the MAC" note warns about, reachable just by using the template twice.

Now @@UUID_BIOS@@ fed from a new GUEST_UUID_BIOS, defaulting to the previous value so existing VMs are unaffected. 10-create-vm.ps1 warns when the last three bytes of MAC and UUID disagree, since that coupling is easy to break silently.

90-teardown.ps1 enumerated the snapshot chain by name

The fixed list covered exactly two deltas and two .vmsn files. A VM that reached -000003.vmdk left an orphan — defeating the rationale stated at the top of that file, because UEFI's removable-media fallback then finds the surviving ESP and boots the previous image. Replaced with a pattern glob.

Two smaller ones

50-verify-boot.sh:87s/\x1b\[[0-9;]*g//g, trailing g where line 83 correctly uses m, so SGR sequences were never stripped from the failed-units section.

20-make-ssh-key.sh — no set -e, so a failed ssh-keygen fell through to cat "$KEY.pub" and reported a missing file instead of the real cause. Now exits 5.

Documented, deliberately not changed

GUEST_GATEWAY is read by nothing: the gateway and DNS are hardcoded inside photon-appliance.ks.template.json's networkd unit even though the address itself is templated. That file is byte-exact and policed by check-drift.sh, so correcting it here would fail the drift check — the fix belongs upstream in iso-phase6-kickstart-template.cfg. The variable now records that instead of looking dead.

Also left alone by choice: 40-check-staging.sh never exits non-zero. That reads as deliberate for a print-only inspection tool, and changing its exit semantics could break callers. My matrix runner adds structured pass/fail on top rather than altering it.

Verification

bash -n clean on all 7 shell scripts · both .ps1 parse under pwsh · the shared parser executed against the real .env · kickstart sha256 unchanged (ac5a3a5c…) so check-drift.sh still passes · both .ps1 still ASCII-only with CRLF, per the PowerShell 5.1 constraint in the README.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant