Commit Graph

1606 Commits

Author SHA1 Message Date
Julian Sommer 23de2ad85d feat: ModifyDBInstance and the shared VM-replace helper (#597) 2026-07-29 15:07:06 +10:00
Josh Siviour 7d2106bfb5 fix(test): reclaim fixture scratch dirs stranded in /tmp (#598)
* fix(test): reclaim fixture scratch dirs stranded in /tmp

The shared predastore fixture and the integration suite's JetStream store both
outlive any individual test, so neither can use t.TempDir() - the first test to
finish would delete files the daemon still has open. Both used os.MkdirTemp
with nothing removing the directory afterwards, so every run stranded one, and
a run killed by a timeout panic or the OOM killer stranded it regardless. /tmp
is a tmpfs on the dev boxes, so the leak is charged to RAM and swap, and
swapped tmpfs pages have no backing store to be dropped to.

Add a shared sweep that reclaims a directory only when it matches both the
prefix and an age no live run can reach. The NATS store also has a clean
shutdown point, so TestMain now removes it outright and the sweep is left to
cover the killed-run case. The predastore daemon runs until the process exits
and has no such point, so the sweep is its only mechanism.

* chore: repin viperblock to the dev tip carrying the scratch-dir fix

Test-only upstream change, so the build output is unchanged - this keeps
go.mod aligned with the submodule pointer rather than delivering a fix.
2026-07-29 14:57:50 +10:00
Julian Sommer fc6e055ca0 feat: RDS lifecycle core — reboot, start, stop, delete, events (#596) 2026-07-29 14:01:02 +10:00
Julian Sommer 6eb76b439b feat: RDS tag actions, --tags on create, and the ARN parser (#595) 2026-07-29 12:07:32 +10:00
tomnewton-mulga 51ecddbf81 routeTree file 2026-07-29 11:51:46 +10:00
tomnewton-mulga 27aa2394b3 chore: Updated RTX-6000 RA Doc 2026-07-29 11:51:46 +10:00
Julian Sommer 2569601e3d feat: implement RDS CreateDBInstance, reconciler and endpoint DNS (#594) 2026-07-29 11:32:59 +10:00
Julian Sommer 32d80b473a feat: RDS v1 foundations (#592) 2026-07-29 10:38:22 +10:00
Josh Siviour 6cf9ee9d03 fix(dhcp): retransmit within a DORA transaction, resolve the chaddr on every path, stop masking IPAM errors (#593)
* fix(dhcp): retransmit within a DORA transaction instead of restarting it

nclient4's retryFn re-sends the same packet under the same xid with a
doubling read deadline, which is RFC 2131 4.1 retransmission. WithRetry(1)
runs that loop exactly once, so the client transmitted a single DISCOVER
per attempt and never retransmitted it. socketTimeout then handed that
lone transmission the caller's whole budget, so it sat silent for 4s,
then 8s, then 16s, then 32s.

Manager does retry, on that schedule, but each attempt builds a fresh
client and therefore a fresh xid. Every attempt is a new transaction
rather than a retransmission of the previous one, so an OFFER answering
an earlier exchange is discarded as belonging to an unknown transaction.

Send six transmissions per exchange starting from a 1s read, spanning a
63s ladder — longer than the widest window Manager schedules, so an
attempt ends on the caller's context rather than on an exhausted ladder.
socketTimeout now clamps to the caller's remaining budget instead of
consuming it in one silent read. Observed live recovering an exchange
whose OFFER arrived on the third transmission, which a single-shot
client would have missed.

Both allocation sites also stop replacing the allocator's error with a
bare InsufficientAddressCapacity. ResolveErrorCode unwraps, so a
genuinely exhausted static pool still surfaces that code while a DHCP
fault reports as itself. The flat mapping is why every failure on this
path read as a capacity problem regardless of cause.

* chore(dhcptest): add --broadcast so the probe can mirror vpcd's Acquire path

* fix(dhcp): resolve the chaddr on renew and release, not just on acquire

Acquire derived a chaddr when the request carried none. Renew and Release
read lease.HWAddr straight out of the store and handed it to nclient4, so
a lease persisted before the chaddr was carried renewed with all zeros in
chaddr and option 61 fell back to its untyped form. The lease then lands
upstream with no hardware address against it — the outcome option 61 was
fixed to stop, reached by a different route — and because the server sees
an unknown client each time, every renewal takes a new lease.

resolveHWAddr now serves all three paths and treats all-zeros as unusable
rather than merely empty: six zero bytes passes a width check yet is the
exact value that produces the unattributable entry.

* test(dhcp): cover chaddr resolution and the unmasked allocation errors
2026-07-29 10:13:51 +10:00
Julian Sommer 84418a218c refactor: extract the managed system-VPC builder out of EKS (#590) 2026-07-29 08:59:28 +10:00
tomnewton-mulga a1331b3785 release: update go.mod for v1.14.0 v1.14.0 2026-07-28 16:14:13 +10:00
Julian Sommer 341f36a432 chore: bump alpine image to 3.24.1 2026-07-28 11:05:50 +10:00
Julian Sommer 5800f39e42 fix(ec2): drain a snapshot's volume on the node hosting it (#589) 2026-07-27 15:40:29 +10:00
Julian Sommer 7e356b5b9b test: cover DescribeInstanceStatus and the SDK instance-status-ok waiter (#588) 2026-07-27 13:46:16 +10:00
Julian Sommer 331e46951f chore: rebuild frontend with new deps 2026-07-27 12:51:36 +10:00
dependabot[bot] 6ba9e11461 chore: bump the npm-dependencies group (#587) 2026-07-27 12:49:48 +10:00
dependabot[bot] 1d1b1f6a51 chore: bump the go-dependencies group with 3 updates (#586) 2026-07-27 12:48:17 +10:00
Julian Sommer 5b8f20b89f test: assert ModifyVolume resize reaches the guest block device (#585) 2026-07-27 12:47:42 +10:00
tomnewton-mulga 6389b37433 fix(gpu): Blackwell GPU support fixes for EKS/ECS image builds and VFIO (#559)
* fix(eks): GPU node AMI declares uefi boot mode

The spinifex-eks-node-gpu catalog entry declared BootMode: bios, but
the Ubuntu 26.04 GPU appliance image is built GPT/EFI-only. Under
legacy BIOS no OVMF/pflash firmware is attached, so the guest hangs at
firmware with zero serial console output.

* fix(images): install open-kernel NVIDIA DKMS driver for Blackwell GPUs

Blackwell-generation GPUs (e.g. RTX Pro 6000 Blackwell) refuse to
initialize under the closed/proprietary kernel module: dmesg shows
RmInitAdapter failing with "requires use of the NVIDIA open kernel
modules" (NVRM error 0x22). Switch from nvidia-dkms-<ver>-server to
nvidia-dkms-<ver>-server-open in both the shared ubuntu-gpu-nvidia
recipe and its verbatim duplicate in eks-node-gpu. nvidia-utils has no
separate open variant (userspace tools only), so it is unchanged.

* fix(eks): tolerate CP and GPU taints in ebs-csi-controller/lbc addons

A cluster with a GPU-only nodegroup has no untainted node: the CP
carries CriticalAddonsOnly (EKS parity keeps user workloads off it)
and every worker carries nvidia.com/gpu. Without tolerations for both,
ebs-csi-controller and aws-load-balancer-controller have nowhere to
schedule and stay Pending forever. Real EKS never needs this since its
control plane isn't a schedulable node, so the stock upstream
manifests carry no such toleration.

* fix(images): give the libguestfs appliance a working resolver

The guest's /etc/resolv.conf symlinks to systemd-resolved's stub
(127.0.0.53), which isn't running inside the appliance chroot, so DNS
is dead until package installs need it. Point it at a real resolver
before package installs, then restore the stub symlink afterward so
the shipped AMI still gets its DNS from systemd-resolved at boot like
a stock cloud image.

* fix(gpu): skip upstream PCIe bridge devices during VFIO bind/unbind

A GPU's IOMMU group can include an upstream PCIe bridge when the
platform lacks ACS isolation between them. vfio-pci refuses to bind
non-endpoint bridge devices, so Claim/ReclaimByAddress must filter
bridge-class (0x06) members out of the group before touching driver
bindings, rather than failing the whole claim or leaving the bridge
rebound incorrectly.

* fix(ecs): GPU node AMI declares uefi boot mode

The spinifex-ecs-node-gpu catalog entry declared BootMode: bios, but the
Ubuntu 26.04 GPU appliance image is built GPT/EFI-only. Under legacy BIOS
no OVMF/pflash firmware is attached, so the guest hangs at firmware with
zero serial console output.

Same defect and same fix as the eks-node-gpu entry: both images are built
by the same script from the same stock Ubuntu cloudimg, so both were
EFI-only and only one was corrected.

* fix(images): open-kernel NVIDIA module on ecs-agent-gpu, pin driver branch

ecs-agent-gpu still installed nvidia-dkms-<N>-server, the proprietary
kernel module. NVIDIA ships no proprietary module for Blackwell (GB202)
and later, so ECS GPU container-instances could not bind those cards on
any driver branch. It is the third verbatim copy of the routine and was
missed when the other two were fixed.

Also pins the driver branch across all three copies. The branch was
resolved at build time via apt-cache search | sort -rn | head -1, which
sorts on the number in the package name — but that number is a branch
alias, not a driver version (nvidia-dkms-535-server installs 580.126.20),
so the ordering key need not match what installs. Resolving also let
identical source produce different drivers on different days.

The pin lives in setup.sh rather than manifest.conf because manifest
variables are not passed into the guest: the setup script is uploaded and
executed by virt-customize --run, with no environment carried across.

* fix(eks): tolerate CP and GPU taints in argocd addon

All six argocd Deployments and its StatefulSet carried no tolerations. A
cluster with a GPU-only nodegroup has no untainted node to place them on:
the control plane carries CriticalAddonsOnly and every worker carries
nvidia.com/gpu, so the pods stay Pending forever.

Same defect the ebs-csi-controller and load-balancer-controller addons
already had. Real EKS never hits it because its control plane is not a
schedulable node, so the stock upstream manifests carry no such
toleration and the divergence is ours to maintain.

---------

Co-authored-by: joshsiv-mulga <mrk@mulgadc.com>
2026-07-27 12:11:18 +10:00
Julian Sommer 0708ac4816 chore: setup react-you-might-not-need-an-effect 2026-07-27 11:36:16 +10:00
Julian Sommer 9cec5a50c6 fix: pin isos to /latest so they stop getting pruned 2026-07-27 09:33:44 +10:00
Julian Sommer d96bc3c095 fix: bump grpc 2026-07-27 09:15:36 +10:00
Julian Sommer 04c7efde19 chore: lint config 2026-07-27 09:08:53 +10:00
Julian Sommer 95973eeb3c refactor: migrate spinifex off legacy nats.JetStreamContext onto nats.go/jetstream (#584) 2026-07-24 16:40:57 +10:00
Julian Sommer e79ca59d4b fix: configure host DNS from spx admin init/join (#582) 2026-07-24 10:02:11 +10:00
Josh Siviour 61319ba4a4 fix: terminate/detach volume-wedge — detach-before-delete, bounded detach, vanished-QEMU + stuck-terminate backstops (#583)
* fix: detach before delete on instance terminate to stop root-volume leak

TerminateStoppedInstance and the running-instance terminate path both
deleted a DeleteOnTermination root volume without first clearing its
attachment, so DeleteVolume's in-use guard rejected it and the error was
swallowed (stopped path) or only alarmed (running path), leaking the
volume permanently.

Add VolumeServiceImpl.DeleteVolumeOnTerminate, which clears the stale
attachment via UpdateVolumeState (terminate implies detach) before
deleting, and route both terminate paths through it. Stamp
Teardown[TeardownVolumes] on the stopped path so a delete failure is
visible to the volume leak reaper instead of being lost.

* fix: detach non-DeleteOnTermination root on terminate; cover teardown self-heal

A DeleteOnTermination=false root volume stayed attached to the terminated
instance on both terminate paths: Stop's Unmount deliberately skips Boot
volumes, so the root kept its attachment across stop and neither terminate
path detached it, stranding it attached to an instance that no longer
exists. Add VolumeServiceImpl.DetachVolumeOnTerminate (metadata-only
attachment clear, no delete) and route the DeleteOnTermination=false Boot
volume through it on both paths, matching AWS terminate-implies-detach.

Also add a regression test proving the TerminatedTeardownReaper re-drives a
failed volumes-teardown through the now-detach-first cleaner and self-heals
the leak, so a transient delete failure at terminate does not strand a
volume permanently.

* fix: bound detach chain and reconcile vanished-QEMU wedge

DetachVolume gains a bounded aggregate context deadline over the QMP
hot-unplug chain and a confirmed-dead-QEMU short-circuit that skips the
QMP steps (which would only hang on the wedged process) and seals
directly, so a detach can no longer pin attachMu forever when nbdkit or
QEMU die mid-flight.

OrphanQEMUReaper now also finalizes shutting-down instances whose QEMU
has vanished: a terminate that wedged downstream and never reached
finalizeTerminated is driven to terminated, with outstanding teardown
stamped failed so TerminatedTeardownReaper completes it idempotently.
Gated on a dead process, so a live in-progress terminate is never
stolen.

* feat: add delete-authorized stuck-terminate backstop

A terminate that wedges with QEMU still alive is invisible to the
OrphanQEMUReaper dead-QEMU reconcile and would sit shutting-down forever.
StuckTerminateReaper force-completes it: past stuckTerminateTimeout it
force-kills the wedged process, reclaims DeleteOnTermination volume space
through the cleaner, stamps remaining teardown for TerminatedTeardownReaper,
and drives the record to terminated.

It is the one reaper explicitly authorized to delete volume data, but only
for a DeleteOnTermination volume on an already-terminating instance — a far
narrower licence than ADR-0005 §3 denies VolumeLeakReaper, which stays
mark-and-alarm-only. A new ShuttingDownAt timestamp, stamped once on the
transition, bounds how long a terminate may wedge; a missing timestamp reads
as not-yet-stuck so the backstop never acts on an unbounded record.
2026-07-23 23:30:12 +10:00
joshsiv-mulga 6e13a9d9f4 test: add continuous single-open churn e2e for viperblock 503/GC
TestNPassOverwrite detaches after every pass, so viperblock's drain and GC
only ever run at reopen — it cannot show whether GC reclaims superseded
chunks while a volume stays open, and its serial write rate never nears the
predastore rate limiter's burst. TestContinuousChurn attaches once and drives
concurrent 4 MiB-aligned raw overwrites of per-writer regions for several
minutes without detaching, so a sustained superseded-chunk backlog builds
under one long open, spanning multiple GC ticks.

The workload is bounded and self-contained: each dd writes one chunk and the
clock is re-checked after every write, the loop runs as root under a single
sudo bash with a process-group kill trap, and it is wrapped in a guest-side
timeout. Root ownership is load-bearing — the writers open a raw device, so
only a root-owned process group can reap them; a best-effort guest pkill runs
before detach as a backstop so a stray writer cannot wedge the detach.
2026-07-23 14:49:01 +10:00
Julian Sommer 12085f029c feat: render import progress bars in human-readable units (#581) 2026-07-23 14:40:37 +10:00
Julian Sommer 4803469037 fix: e2e test assertion 2026-07-23 13:56:01 +10:00
joshsiv-mulga eae776b2f8 chore: bump viperblock to dev 8a2df56 (503-not-ErrNoSpace + latch logging) 2026-07-23 13:14:04 +10:00
Julian Sommer f50f69caff chore: fix viperblock logs to not log the data 2026-07-23 12:17:22 +10:00
Julian Sommer f682d09a72 fix: nil panics 2026-07-23 12:04:59 +10:00
Julian Sommer 1d7d5e1750 fix: disable debug log 2026-07-23 11:56:41 +10:00
joshsiv-mulga e26a480e54 chore: repin viperblock to dev with chunk GC on its own goroutine
Picks up viperblock 3dcfbb7 (#49): the chunk GC sweep now runs on a
dedicated goroutine instead of sharing the chunk-uploader select, so
sustained overwrite churn no longer starves GC and wedges the volume at
the internal space cap.
2026-07-23 11:41:18 +10:00
Julian Sommer b505887b36 chore: add nilaway 2026-07-23 11:28:02 +10:00
Julian Sommer 1d55404a73 revert: host DNS setup on resolved/resolvconf hosts (#580)
This reverts commit f9c9f0b38d.

#580 switched setup.sh to read the northstar :53 listener and zones from
northstar.toml, so the resolved stage now defers at install time (before
formation) and must be re-run afterward. Only firstboot re-runs it; the
manual setup.sh + spx admin init install and CI never do, so those hosts
get no host DNS and cluster-zone names (ELB/EKS) fail to resolve from the node.

Reverting restores the pre-#580 behaviour: systemd-resolved is configured
at install time from the WAN IP, with no post-formation re-run needed. This
reintroduces the original gap where resolvconf hosts get no host DNS. The
proper fix configures host DNS from spx admin init/join for both resolver
managers (mulga-x30yv).
2026-07-23 11:23:21 +10:00
Julian Sommer 5001a04b6a chore: lint config 2026-07-23 10:50:18 +10:00
Julian Sommer f595129c7a chore: enable SA1019 2026-07-23 10:26:03 +10:00
Julian Sommer a36c6d819e chore: bump image versions 2026-07-23 10:19:18 +10:00
Josh Siviour 528bb1e483 fix(lbagent): quiet expected startup heartbeat failures below ERROR (#579)
A freshly launched LB agent begins heartbeating the instant the VM boots,
but the gateway node fielding that first heartbeat may not yet have the LB
record — it is still replicating from the node that created it — so the
gateway answers 400 LoadBalancerNotFound. The IMDS credential chain is
likewise not always warm on the first tick. Both self-heal within a few
ticks, yet every early failure was logged at ERROR, flooding the console
telemetry with startup noise (46 such lines across 8 agents in one run).

Bound these to a registration grace window: before any heartbeat has ever
succeeded, and only within the window, a failure logs at INFO. Past the
window with no success — the fingerprint of a genuinely blackholed agent —
or after any prior success, a failure logs at ERROR as before, so the
serial-console telemetry still surfaces a real blackhole. The only change
for a persistent failure is a bounded delay before it escalates.
2026-07-23 10:07:06 +10:00
Josh Siviour 8989fc9ba8 fix: make the seal-failure unmount test deterministic (#578)
* fix: make the seal-failure unmount test deterministic

The test forced the seal to fail by pointing S3Host at an unreachable
endpoint, so the handler only returned once the S3 client had exhausted
its jittered backoff. Measured handler time spanned 1.8s to 5.5s against
a 5s request deadline, which failed 2-3 runs in 10 on an idle machine and
worse on a loaded CI runner.

Config gains a sealVolume seam, nil meaning the real seal, and the test
injects an immediate failure instead. It now runs in 0.53s, 50 for 50,
and still asserts what it was written to assert -- plus that the handler
actually attempted the seal, which the old form could not distinguish.

* chore: bump viperblock to dev tip (GC observability + scratch sweep)

Picks up viperblock #47 (every chunk GC sweep logs its outcome at Info)
and #48 (TestMain reclaims scratch dirs abandoned by a panicking run).
2026-07-23 10:02:26 +10:00
Julian Sommer a55544b7d4 chore: remove integration from e2e 2026-07-23 09:49:51 +10:00
Julian Sommer f9c9f0b38d fix: configure host DNS for Spinifex zones on resolved and resolvconf hosts (#580) 2026-07-23 09:43:38 +10:00
joshsiv-mulga d6b356b658 Stop GPU E2E on push and merge 2026-07-23 09:14:20 +10:00
joshsiv-mulga e0259b7638 update preflight 2026-07-23 08:03:08 +10:00
Josh Siviour 88b57b1805 chore: give the integration tier its own CI job and drop it from preflight (#577)
The tier ran inside the Build and Test job, serialised ahead of test-cover, and
again in preflight. Neither placement bought signal that a parallel job does not:
it provisions nothing and finishes in seconds, so the only effect was wall time
on the two paths developers wait for most.

It stays PR-blocking as its own job, which is what distinguishes it from
e2e.yml's front gate — that one is push-triggered on a self-hosted runner and so
never gates a PR before merge.
2026-07-23 07:56:07 +10:00
Josh Siviour 318e43c66f fix: allocate mgmt IPs cluster-wide via NATS KV CAS (#576)
MgmtIPAllocator allocated from a process-local map, but br-mgmt is
trunked onto a shared physical NIC making the /24 one flat L2 segment
across the whole cluster. Every node independently allocated from
.10, causing collisions and ARP-race losers being blackholed.

Rewrite the allocator on the spinifex-cluster-state NATS KV bucket
using compare-and-swap, keyed by mgmt-ipam.<subnet>, modeled on IPAM.
Bind the KV handle in startCluster (not startLocal) to respect the
DDIL boundary. Serve already-allocated instance IDs from the local
cache when KV is unhealthy, but refuse new allocations until it
recovers. Rebuild now reconciles this node's running VMs into the
cluster-wide record instead of just refreshing the local cache.
2026-07-22 22:27:07 +10:00
Josh Siviour 7e8cbeef6e feat(config): enable viperblock chunk GC in rendered node configs (#563)
* feat(config): enable viperblock chunk GC in rendered node configs

The gc_enabled toggle was plumbed through every VB construction site but
nothing ever set it. GCEnabled is a *bool that every consumer treats as
off when nil, and the config template rendered shardwal while omitting
gc_enabled, so no deployed node ever carried the key and chunk GC was off
everywhere by construction. Superseded chunks were never reclaimed under
overwrite churn.

Render gc_enabled = true alongside shardwal, and cover it with a test that
loads the rendered file through config.LoadConfig rather than matching the
template text. A string match would still pass if the value failed to parse
into a non-nil pointer, which is the exact way the setting reaches its
consumers.

Existing deployments keep the old rendering until their config is
regenerated; the template governs install and bootstrap only.

* chore: pin viperblock to the build with the cross-process GC snapshot marker

Enabling chunk GC in rendered node configs is only safe once the sweeping
engine can see a snapshot taken by another process. viperblock 3b56d61 adds the
per-volume snapshot marker that closes that window, and 2a34add fixes the
concurrent-stop panic in the background goroutine lifecycle that GC's own ticker
would otherwise widen. Pin both before turning GC on.
2026-07-22 22:12:13 +10:00
Josh Siviour d2efbee043 fix(daemon): fail the system-instance launch when the VM never reaches running (#574)
LaunchRunInstances is a best-effort batch launcher that reports failure only
through VM state: volume preparation, GPU claim and vmMgr.Run errors each call
MarkFailed and continue. launchAMISystemInstance ignored that and always
returned a SystemInstanceOutput, so an EKS control-plane server, ELB or Bedrock
serving VM whose launch failed handed its caller an instance ID for a VM that
was already being torn down.

Check the instance reached running state before returning. Cleanup stays with
the MarkFailed chain LaunchRunInstances has already run.
2026-07-22 21:08:08 +10:00
Josh Siviour 344a490a70 fix(vm): give relaunched data volumes named block nodes so detach can complete (#575)
buildDrives emitted every non-boot data volume as a bare -drive with no
id= and no node-name=, so a stop/start (or reboot/resize) relaunch left
the volume anonymous: device_del/blockdev-del had nothing to address,
QEMU kept the NBD socket open, and DetachVolume stalled through
nbdkit's full kill grace before giving up.

Add -blockdev support to the QEMU config builder and use it for
non-boot, non-EFI volumes, sharing the node-name/device-id/iothread-id
naming and virtio-blk-pci device arguments with AttachVolume's hot-plug
path so cold-boot and hot-attach can no longer drift apart. A data
volume with no persisted HotplugPort now gets one allocated and written
back during buildDrives instead of landing on an unplugged bus.

Also raise unmountSealTimeout past viperblockd's kill grace (it runs
the seal after the SIGKILL, so matching the grace meant an unmount
that used the whole grace could never be observed as successful), and
bound the e2e harness's forced teardown detach by the teardown
deadline so a slow detach is reported as slow rather than as an
unexplained leak.
2026-07-22 20:38:44 +10:00