Files
lab/bastion
Michal 5d00c42f5a
Some checks failed
CI/CD / typecheck (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 9s
CI/CD / lint (pull_request) Failing after 24s
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
feat(bastion): bring VyOS provisioning to Fedora-grade quality
Ports the Fedora provisioning features that matter for a router onto the
VyOS path, and adds the libvirt integration test that proves them.

- Live install logs: the driver streams the installer pty (ANSI-stripped,
  batched, best-effort) to POST /api/log, so `labctl provision logs -f`
  works during a VyOS install the way Anaconda's syslog does for Fedora.
- installed.ip: report "ready at <ip>" -- the exact detail format
  routes/api.ts parses -- using the static mgmt address when known, else
  the live DHCP address. Without it VyOS machines landed with an empty IP,
  breaking provision list, logs-by-IP, recheck and reprovision.
  api.ts also guards the complete handler: VyOS boxes get the "vyos" SSH
  hint and never trigger the k3s post-provision.
- EFI network-first boot order: port of the Fedora %post efibootmgr step,
  run from the live env after install (NVRAM, not disk). Best-effort.
- Reinstall semantics: VyOS's installer already carries the previous
  config and SSH host keys forward -- the analog of Fedora's LV
  preservation -- so that stays the default. New --vyos-fresh-config
  overwrites the installed config.boot with the generated one instead,
  via a post-install target mount that also writes /config/lab-provisioned
  (mirrors Fedora's /etc/lab-provisioned, survives image upgrades).
- reprovision/recheck default to the "vyos" SSH user for VyOS machines.

Two hangs found by the VM test and fixed:
- On reinstall the installer asks "Would you like to copy data to the new
  image?" (search_previous_installation). Unanswered, the driver blocked
  on stdin until its stall timeout -- a silent 15-minute hang.
- The RAID regex missed "Would you like to choose two disks for RAID-1
  mirroring?", which would wedge any multi-disk box. Both prompts default
  to yes, so a miss also risks an unwanted mirror.

Both are now covered by a unit test asserting all 17 installer prompts
match exactly one rule -- verified to fail against the unfixed code, so
this class of bug is caught in a second instead of a 45-minute VM run.

tests/integration/vyos-provision.test.ts: fresh install, reinstall
preserves config + /config data, and freshConfig override. All 8 pass
against the real nightly ISO (EXIT=0). 273 unit tests pass; no new lint
errors in touched files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
2026-08-11 11:16:25 +01:00
..

labctl

Infrastructure management platform for bare-metal servers, Kubernetes clusters, and cloud resources.

Install

# From Gitea packages (Fedora/RHEL)
sudo dnf config-manager --add-repo https://mysources.co.uk/michal/-/packages/rpm/
sudo dnf install labctl

# From source
cd bastion && pnpm install && pnpm build
bun build src/cli/src/index.ts --compile --outfile dist/labctl
sudo cp dist/labctl /usr/bin/labctl

Quick Start

# Start the bastion (PXE provisioning server)
sudo labctl init bastion standalone start

# PXE boot a machine — it gets discovered automatically
labctl provision list

# Install Fedora on a discovered machine
labctl provision install 78:55:36:08:35:14 labmaster --role infra

# Reprovision (SSH reboot into PXE, preserves /home /srv /var/lib/rancher)
labctl provision reprovision 78:55:36:08:35:14 labmaster --role infra

Commands

Bastion (PXE Provisioning)

# Lifecycle
sudo labctl init bastion standalone start              # Start bastion (daemonized)
sudo labctl init bastion standalone start --foreground  # Start in foreground
sudo labctl init bastion standalone stop                # Stop bastion
labctl init bastion standalone status                   # Show status, PID, machine count

# Options
sudo labctl init bastion standalone start \
  --port 8080 \
  --dir /tmp/lab-bastion \
  --domain ad.itaz.eu \
  --dhcp-mode proxy \
  --fedora 43 \
  --timezone Europe/London

Provisioning

# List all machines (discovered, queued, installing, installed)
labctl provision list

# Queue a machine for Fedora install
labctl provision install <mac> <hostname> --role worker   # k3s worker (gets longhorn)
labctl provision install <mac> <hostname> --role infra    # infra node (gets k3s server + /var/lib/rancher)

# Reprovision — queues install, SSHes in, sets PXE boot, reboots
labctl provision reprovision <mac> <hostname> --role infra

# Remove a machine from state
labctl provision forget <mac>

# Options
labctl provision install <mac> <hostname> \
  --role worker \
  --disk nvme0n1 \
  --port 8080

Server Management (planned)

# List servers with filters
labctl get servers
labctl get servers --env production
labctl get servers --cloud baremetal
labctl get servers --cloud aws
labctl get servers --label role=k3s-worker
labctl get servers --label asg=web-servers

# Detailed server info
labctl describe server/puppet
labctl describe server/ser9

Remote Execution (planned)

# Execute commands on servers (audited, RBAC-checked)
labctl exec server/puppet -- whoami
labctl exec server/puppet -- systemctl status k3s
labctl exec server/puppet -it -- bash              # interactive TTY
labctl exec server/puppet --timeout 30s -- long-running-task

Kubernetes (planned)

# Proxied kubectl — audited, RBAC-checked, no kubeconfig needed
labctl kubectl --cluster lab get pods
labctl kubectl --cluster lab get nodes
labctl kubectl --cluster lab logs pod/nginx -f
labctl kubectl --cluster lab exec pod/nginx -- bash
labctl kubectl --cluster lab apply -f deployment.yaml
labctl kubectl --cluster aws-prod get pods --namespace app

# Cluster management
labctl clusters add lab --kubeconfig ~/.kube/config
labctl clusters list
labctl clusters remove staging

Logs (planned)

# Server logs (journalctl passthrough via agent)
labctl logs server/puppet                                    # all journal
labctl logs server/puppet -f                                 # follow (live stream)
labctl logs server/puppet -n 100                             # last 100 lines
labctl logs server/puppet -u k3s                             # specific unit
labctl logs server/puppet -u sshd --since "1h ago"           # time range
labctl logs server/puppet --since "2026-03-17" --until "2026-03-18"
labctl logs server/puppet -k                                 # kernel only
labctl logs server/puppet -p err                             # errors only
labctl logs server/puppet --file /var/log/nginx/error.log    # tail a file
labctl logs server/puppet --file /var/log/nginx/error.log -n 50

# App logs (k8s pod logs)
labctl logs app/bastion
labctl logs app/bastion -f
labctl logs app/labd --container postgres

# Pulumi execution logs
labctl logs pulumi/run-abc123
labctl logs pulumi/run-abc123 -f                             # follow active run

# Bastion logs
labctl logs bastion/lab
labctl logs bastion/lab --mac 78:55:36:08:35:14              # specific machine's install

# Agent daemon logs
labctl logs agent/puppet

# Audit logs
labctl logs audit
labctl logs audit --user michal
labctl logs audit --user michal --since "1h ago"
labctl logs audit/michal-20260317-abc123                     # specific session
labctl logs audit --action kubectl --cluster lab
labctl logs audit --action exec --server puppet

Apps (planned, replaces Helm)

# Install Pulumi-based apps to Kubernetes
labctl apps list                                  # available apps
labctl apps install bastion                       # deploy bastion
labctl apps install bastion --set port=8080       # with overrides
labctl apps install bastion -f values.yaml        # from values file
labctl apps install monitoring                    # Prometheus + Grafana

# Manage deployed apps
labctl apps status bastion                        # health, version, config
labctl apps upgrade bastion                       # rolling upgrade
labctl apps history bastion                       # version history
labctl apps rollback bastion 2                    # rollback to version 2
labctl apps uninstall bastion

Infrastructure as Code (planned)

# Execute Pulumi programs via labd (RBAC-checked)
labctl apply -f infra/k3s-cluster.ts --env lab
labctl plan -f infra/k3s-cluster.ts --env lab     # dry run
labctl destroy -f infra/k3s-cluster.ts --env lab

RBAC (planned)

# Roles and permissions
labctl get roles
labctl get users
labctl create role viewer --allow "read:*:*:*"
labctl create role lab-admin --allow "*:baremetal:lab:*" --deny "destroy:*:*:*"
labctl bind role lab-admin --user michal
labctl unbind role lab-admin --user michal

# Permission model: action:cloud:environment:server
#   read:*:*:*                   — read everything
#   exec:baremetal:lab:*         — exec on any lab server
#   kubectl:*:*:*                — kubectl on any cluster
#   *:baremetal:lab:puppet       — full access to puppet only
#   manage:*:*:*                 — manage apps, clusters, tokens

Environments and Clouds (planned)

labctl get environments
labctl get clouds
labctl create environment staging --cloud aws
labctl create environment lab --cloud baremetal

Partition Layout

Machines installed by the bastion get this LVM layout:

Worker role (k3s worker with Longhorn)

/boot/efi       600MB     EFI
/boot           3GB       ext4
                ── LVM VG: labvg ──
  swap          27GB      (matches RAM)
  /             33GB      xfs
  /var          100GB     xfs
  /var/log      10GB      xfs
  /home         10GB      xfs         ← preserved on reprovision
  /srv          20GB      xfs         ← preserved on reprovision
  /tmp          tmpfs 4GB
  /var/lib/longhorn  rest  xfs        ← preserved on reprovision (Longhorn PVC storage)

Infra role (k3s server, labmaster)

/boot/efi       600MB     EFI
/boot           3GB       ext4
                ── LVM VG: labvg ──
  swap          27GB      (matches RAM)
  /             33GB      xfs
  /var          100GB     xfs
  /var/log      10GB      xfs
  /home         10GB      xfs         ← preserved on reprovision
  /srv          20GB      xfs         ← preserved on reprovision
  /var/lib/rancher  20GB  xfs         ← preserved on reprovision (k3s etcd data)
  /tmp          tmpfs 4GB

On reprovision, OS partitions (/, /var, /var/log, swap) are wiped. Data partitions (/home, /srv, /var/lib/longhorn, /var/lib/rancher) are preserved.

Architecture

┌──────────────────────────────────────────────────────────────┐
│  labctl CLI                                                   │
│  init | provision | get | exec | logs | apply | apps | kubectl│
└───────────────────────────┬──────────────────────────────────┘
                            │ mTLS
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  labd (master daemon — stateless, on k3s)                    │
│  ┌─────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌────────┐ │
│  │ CA  │ │ RBAC │ │ Logs │ │ Pulumi │ │ Apps │ │kubectl │ │
│  │     │ │      │ │relay │ │executor│ │      │ │ proxy  │ │
│  └─────┘ └──────┘ └──────┘ └────────┘ └──────┘ └────────┘ │
│                        CockroachDB                           │
└──────────────┬─────────────────────────┬─────────────────────┘
               │ mTLS                    │ mTLS
    ┌──────────▼───────────┐  ┌──────────▼───────────┐
    │  lab-agent            │  │  lab-agent            │
    │  bare-metal server   │  │  AWS EC2 / cloud VM   │
    │  ┌────────────────┐  │  │  ┌────────────────┐  │
    │  │ heartbeat      │  │  │  │ heartbeat      │  │
    │  │ exec handler   │  │  │  │ exec handler   │  │
    │  │ log streamer   │  │  │  │ log streamer   │  │
    │  │ module runner  │  │  │  │ module runner  │  │
    │  └────────────────┘  │  │  └────────────────┘  │
    └──────────────────────┘  └──────────────────────┘

Technology Stack

Component Technology
Language TypeScript (ESM)
CLI Commander.js
HTTP Server Fastify + WebSocket
Database CockroachDB (PostgreSQL compatible)
ORM Prisma
IaC Pulumi (TypeScript)
k8s CNI Cilium
Auth mTLS (built-in CA)
Packaging nfpm (RPM/DEB), bun compile
Containers Podman + podman-compose
CI/CD Gitea Actions
Testing Vitest

Development

cd bastion

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Run tests (30 tests)
pnpm test:run

# Type check
pnpm typecheck

# Lint
pnpm lint

# Generate shell completions
pnpm completions:generate

# Build standalone binary
bun build src/cli/src/index.ts --compile --outfile dist/labctl

# Build RPM/DEB packages (both architectures)
bash scripts/build-rpm.sh --all

# Build Docker image
bash scripts/build-bastion.sh

# Full release (build + publish + install)
bash scripts/release.sh

Project Structure

bastion/
├── src/
│   ├── shared/          # @lab/shared — types, constants
│   ├── bastion/         # @lab/bastion — PXE provisioning server
│   ├── cli/             # @lab/cli — CLI binary (labctl)
│   ├── labd/            # @lab/labd — master daemon (planned)
│   └── agent/           # @lab/agent — server agent (planned)
├── modules/             # Built-in configuration modules (planned)
├── deploy/
│   └── k3s/             # Kubernetes manifests
├── stack/
│   ├── Dockerfile
│   └── docker-compose.yml
├── scripts/             # Build, publish, release scripts
├── completions/         # Generated shell completions
└── ARCHITECTURE.md

License

MIT