feat(bastion): unattended VyOS network install with HA (bond + VRRP)
Some checks failed
CI/CD / lint (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 23s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 23s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
VyOS ships no unattended installer (install_image() is unconditionally interactive; --no-prompt is wired only to 'add'), so the automation is injected through live-config's hooks component: iPXE boots the live kernel with fetch= and live-config.hooks=, the hook fetches a generated per-MAC Python driver, and the driver builds config.boot, stages the rootfs, and drives the interactive installer over a pty. Bastion: - vyos-boot.ipxe template (no 'nonetworking' — breaks the hook fetch; no console=ttyS0 — 30s/systemd-phase on UART-less boards) - /vyos/autoinstall.sh + /vyos/install.py routes (per-MAC driver with the config spec baked in as base64) - vyos-config-spec: bond0 (802.3ad) + tagged VLANs + VRRP groups (vrid = VLAN id) + sync group + hw-id pinning by MAC + SSH keys; config built from the image's own config.boot.default via vyos.configtree, version footer reattached via component_version - prepareVyosArtifacts: extract kernel/initrd/squashfs from the nightly ISO with xorriso; initrd picked by size from regular files only; ISO URL "latest" resolves the newest vyos-nightly-build GH release (downloads.vyos.io no longer serves direct ISOs) Verified end-to-end in a libvirt VM against the real nightly ISO — installed system boots with bond/VRRP/hw-id config applied and no migrations. Fixes found by the VM run, encoded in code comments: config.boot.default lives at /usr/share/vyos at hook time; fetch= boot has no medium so the rootfs is symlinked to the installer's expected path; reboot must be --force (the hook is a child of the still-starting live-config unit); installer disk answers are full /dev paths; zram0 passes the 2GB min-disk filter so the disk is always pinned. CLI/labd: vyos spec threaded through provision install (--vyos-* and --vlan/--vlan-vip flags with guards), labd install route, protocol command-install, and the bastion's direct /api/install. 268 unit tests pass; no new lint errors in touched files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
This commit is contained in:
@@ -8,6 +8,8 @@ export type {
|
||||
DebugConfig,
|
||||
BastionState,
|
||||
BastionConfig,
|
||||
VyosVlanSpec,
|
||||
VyosInstallSpec,
|
||||
} from "./types/index.js";
|
||||
|
||||
export { SUPPORTED_OS, SUPPORTED_ROLES, ROLE_REGISTRY, isValidOsId } from "./types/index.js";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Protocol types for agent-labd WebSocket communication.
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { VyosInstallSpec } from "../types/state.js";
|
||||
|
||||
// --- Agent -> labd messages ---
|
||||
|
||||
@@ -108,7 +109,7 @@ export type BastionMessage =
|
||||
export type LabdBastionMessage =
|
||||
| { type: "bastion-enrolled"; bastionId: string }
|
||||
| { type: "bastion-heartbeat-ack"; serverTime: string }
|
||||
| { type: "command-install"; requestId: string; mac: string; hostname: string; disk?: string; role: string; os: string }
|
||||
| { type: "command-install"; requestId: string; mac: string; hostname: string; disk?: string; role: string; os: string; vyos?: VyosInstallSpec }
|
||||
| { type: "command-forget"; requestId: string; mac: string }
|
||||
| { type: "command-role-update"; requestId: string; mac: string; role: string }
|
||||
| { type: "command-debug"; requestId: string; mac: string; pxeBoot?: boolean }
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface BastionConfig {
|
||||
// Ubuntu support
|
||||
ubuntuVersion: string;
|
||||
ubuntuMirror: string;
|
||||
// VyOS support — netboot artifacts are extracted from the ISO at startup.
|
||||
// LTS ISOs are subscription-only, so this defaults to a rolling release.
|
||||
vyosIsoUrl: string;
|
||||
vyosDefaultPassword: string;
|
||||
// Syslog listener for install logs (Anaconda logging --host)
|
||||
syslogPort: number;
|
||||
// Flags
|
||||
|
||||
@@ -7,6 +7,8 @@ export type {
|
||||
InstalledInfo,
|
||||
DebugConfig,
|
||||
BastionState,
|
||||
VyosVlanSpec,
|
||||
VyosInstallSpec,
|
||||
} from "./state.js";
|
||||
|
||||
export { SUPPORTED_OS, SUPPORTED_ROLES, ROLE_REGISTRY, isValidOsId } from "./state.js";
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
export type ProvisionStackType = "dhcpproxy" | "iso" | "cloud-init";
|
||||
|
||||
export type OsId = "fedora-43" | "ubuntu-26.04";
|
||||
export type OsId = "fedora-43" | "ubuntu-26.04" | "vyos-rolling";
|
||||
export type Arch = "x86_64" | "aarch64";
|
||||
|
||||
export const SUPPORTED_OS: readonly OsId[] = ["fedora-43", "ubuntu-26.04"] as const;
|
||||
export const SUPPORTED_OS: readonly OsId[] = ["fedora-43", "ubuntu-26.04", "vyos-rolling"] as const;
|
||||
|
||||
export function isValidOsId(value: string): value is OsId {
|
||||
return (SUPPORTED_OS as readonly string[]).includes(value);
|
||||
@@ -75,11 +75,73 @@ export interface ProgressLogEntry {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/** A tagged VLAN sub-interface on the bond (or on the mgmt NIC when unbonded). */
|
||||
export interface VyosVlanSpec {
|
||||
id: number;
|
||||
address: string; // CIDR, e.g. "10.0.10.1/24"
|
||||
description?: string;
|
||||
/**
|
||||
* VRRP virtual address (CIDR) floated on this VLAN. Emitted as a
|
||||
* high-availability vrrp group with vrid = VLAN id, so the same spec on both
|
||||
* HA peers (with different priorities) produces a matching group pair.
|
||||
*/
|
||||
vrrp?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* VyOS-specific install parameters. Rendered into the config.boot that the
|
||||
* installer adopts, so the router comes up already configured.
|
||||
*
|
||||
* NOTE: bondMembers must NOT include the interface PXE booted from. Firmware
|
||||
* PXE cannot run over LACP, so the install-time NIC has to stay unbonded.
|
||||
*/
|
||||
export interface VyosInstallSpec {
|
||||
/** Interfaces aggregated into bond0 with LACP (802.3ad). Omit for no bond. */
|
||||
bondMembers?: string[];
|
||||
/** CIDR address on bond0 itself — the switch trunk's native/untagged VLAN. */
|
||||
bondAddress?: string;
|
||||
/** VRRP virtual address (CIDR) floated on the untagged bond (vrid 1). */
|
||||
bondVrrp?: string;
|
||||
/**
|
||||
* VRRP priority for every group on this box. Higher wins mastership.
|
||||
* The HA pair differs ONLY here (e.g. 200 on the primary, 100 on the
|
||||
* standby) — addresses differ per box, VIPs and vrids match.
|
||||
*/
|
||||
vrrpPriority?: number;
|
||||
/** Tagged VLAN sub-interfaces, created on bond0 when bonded, else on mgmtInterface. */
|
||||
vlans?: VyosVlanSpec[];
|
||||
/** Untagged interface the machine PXE booted from. Defaults to "eth0". */
|
||||
mgmtInterface?: string;
|
||||
/** CIDR address for mgmtInterface, or "dhcp". Defaults to "dhcp". */
|
||||
mgmtAddress?: string;
|
||||
/**
|
||||
* Tagged management VLAN on mgmtInterface, separate from the routed VLANs
|
||||
* carried by the bond.
|
||||
*
|
||||
* Needed when the PXE port is a trunk: it boots untagged on the VLAN the
|
||||
* bastion's proxy DHCP serves, and carries the management VLAN tagged so the
|
||||
* router stays reachable there without giving up reinstallability.
|
||||
*/
|
||||
mgmtVlan?: VyosVlanSpec;
|
||||
/** Password for the "vyos" user. Falls back to the bastion default. */
|
||||
password?: string;
|
||||
/**
|
||||
* VyOS interface name -> MAC, emitted as `hw-id` so names bind deterministically.
|
||||
*
|
||||
* Discovery runs under Fedora and reports predictable names (enp2s0,
|
||||
* enp1s0f0np0), but VyOS enumerates its own eth<N> names, so a name observed
|
||||
* during discovery cannot be used directly. Pinning by MAC removes the guess
|
||||
* about which physical port a given eth<N> is.
|
||||
*/
|
||||
hwIds?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface InstallConfig {
|
||||
hostname: string;
|
||||
disk: string;
|
||||
role: Role;
|
||||
os?: OsId; // defaults to "fedora-43" for backward compat
|
||||
vyos?: VyosInstallSpec; // only consulted when os is "vyos-rolling"
|
||||
arch?: Arch; // detected from HardwareInfo or overridden
|
||||
queued_at: string;
|
||||
progress?: string;
|
||||
|
||||
Reference in New Issue
Block a user