feat: iSCSI, Longhorn disk labels, labctl asahi command, ZIP32 fix
Some checks failed
CI/CD / typecheck (pull_request) Failing after 12s
CI/CD / lint (pull_request) Failing after 22s
CI/CD / test (pull_request) Failing after 10s
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 / typecheck (pull_request) Failing after 12s
CI/CD / lint (pull_request) Failing after 22s
CI/CD / test (pull_request) Failing after 10s
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
k3s host prep: - Add iSCSI initiator install+enable (Fedora: iscsi-initiator-utils, Ubuntu: open-iscsi) — required by Longhorn - Add Longhorn disk label to k3s server+agent configs - Add Longhorn disk annotation operation in post-install hardening CLI: - Add `labctl provision asahi` command with interactive install guide - Change default SSH user from "michal" to "lab" in all commands - Change admin user in bastion progress callback to "lab" Asahi provisioning fixes: - Download installer_data.json locally (installer reads it as file) - Use REPO_BASE to serve upstream ZIP from bastion (LAN speed) - Fix ZIP32 vs ZIP64: serve original upstream ZIP unmodified (our repackaged ZIP used ZIP64 which breaks Asahi urlcache) - Add /data/asahi-repo fallback path for k3s container PVC mount - Deploy script syncs asahi-repo to bastion pod after deployment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,9 @@ _labctl() {
|
|||||||
"provision register")
|
"provision register")
|
||||||
COMPREPLY=($(compgen -W "--role --ip -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "--role --ip -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
|
"provision asahi")
|
||||||
|
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
|
||||||
|
return ;;
|
||||||
"provision logs")
|
"provision logs")
|
||||||
COMPREPLY=($(compgen -W "-f --follow -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "-f --follow -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
@@ -104,7 +107,7 @@ _labctl() {
|
|||||||
COMPREPLY=($(compgen -W "bastion -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "bastion -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"provision")
|
"provision")
|
||||||
COMPREPLY=($(compgen -W "list install reprovision debug forget register logs makeiso -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "list install reprovision debug forget register asahi logs makeiso -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"config")
|
"config")
|
||||||
COMPREPLY=($(compgen -W "list get set path -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "list get set path -h --help" -- "$cur"))
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ complete -c labctl -n "__labctl_using_cmd provision" -a reprovision -d 'Queue in
|
|||||||
complete -c labctl -n "__labctl_using_cmd provision" -a debug -d 'PXE boot into Fedora rescue mode for debugging (target: hostname, MAC, or IP)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a debug -d 'PXE boot into Fedora rescue mode for debugging (target: hostname, MAC, or IP)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a forget -d 'Remove a machine from bastion state'
|
complete -c labctl -n "__labctl_using_cmd provision" -a forget -d 'Remove a machine from bastion state'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a register -d 'Register an already-installed machine (e.g. after state loss)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a register -d 'Register an already-installed machine (e.g. after state loss)'
|
||||||
|
complete -c labctl -n "__labctl_using_cmd provision" -a asahi -d 'Show instructions to provision an Apple Silicon Mac with Asahi Linux'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a logs -d 'Show provisioning logs for a machine (hostname, MAC, or IP)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a logs -d 'Show provisioning logs for a machine (hostname, MAC, or IP)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a makeiso -d 'Generate a UEFI-bootable iPXE ISO for network provisioning'
|
complete -c labctl -n "__labctl_using_cmd provision" -a makeiso -d 'Generate a UEFI-bootable iPXE ISO for network provisioning'
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,21 @@ deploy_bastion() {
|
|||||||
kubectl rollout restart deployment/bastion -n lab-infra
|
kubectl rollout restart deployment/bastion -n lab-infra
|
||||||
kubectl rollout status deployment/bastion -n lab-infra --timeout=180s
|
kubectl rollout status deployment/bastion -n lab-infra --timeout=180s
|
||||||
echo "✓ Bastion deployed"
|
echo "✓ Bastion deployed"
|
||||||
|
|
||||||
|
# Sync Asahi rootfs package to bastion pod's persistent volume
|
||||||
|
if [ -d "$PROJECT_DIR/asahi-repo" ] && [ -f "$PROJECT_DIR/asahi-repo/fedora-asahi-lab.zip" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Syncing Asahi rootfs to bastion pod ==="
|
||||||
|
BASTION_POD=$(kubectl get pods -n lab-infra -l app=bastion -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
|
||||||
|
if [ -n "$BASTION_POD" ]; then
|
||||||
|
kubectl exec -n lab-infra "$BASTION_POD" -- mkdir -p /data/asahi-repo
|
||||||
|
kubectl cp "$PROJECT_DIR/asahi-repo/installer_data.json" "lab-infra/$BASTION_POD:/data/asahi-repo/installer_data.json"
|
||||||
|
kubectl cp "$PROJECT_DIR/asahi-repo/fedora-asahi-lab.zip" "lab-infra/$BASTION_POD:/data/asahi-repo/fedora-asahi-lab.zip"
|
||||||
|
echo "✓ Asahi rootfs synced ($(du -sh "$PROJECT_DIR/asahi-repo/fedora-asahi-lab.zip" | cut -f1))"
|
||||||
|
else
|
||||||
|
echo "WARNING: Could not find bastion pod — Asahi rootfs not synced"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_labd() {
|
deploy_labd() {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function registerApiRoutes(
|
|||||||
};
|
};
|
||||||
s.installed[mac] = installedInfo;
|
s.installed[mac] = installedInfo;
|
||||||
|
|
||||||
const admin = installedInfo.role !== "vanilla" && installedInfo.role !== "" ? "michal" : "root";
|
const admin = installedInfo.role !== "vanilla" && installedInfo.role !== "" ? "lab" : "root";
|
||||||
console.log(`\n \x1b[0;32m\x1b[1m ssh ${admin}@${ip}\x1b[0m\n`); // eslint-disable-line no-console
|
console.log(`\n \x1b[0;32m\x1b[1m ssh ${admin}@${ip}\x1b[0m\n`); // eslint-disable-line no-console
|
||||||
|
|
||||||
// Auto-install k3s for non-vanilla roles
|
// Auto-install k3s for non-vanilla roles
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ function findAsahiRepo(config: BastionConfig): string | null {
|
|||||||
const inBastionDir = join(config.bastionDir, "asahi-repo");
|
const inBastionDir = join(config.bastionDir, "asahi-repo");
|
||||||
if (existsSync(inBastionDir)) return inBastionDir;
|
if (existsSync(inBastionDir)) return inBastionDir;
|
||||||
|
|
||||||
|
// Check /data/asahi-repo (PVC mount in k3s container)
|
||||||
|
if (existsSync("/data/asahi-repo")) return "/data/asahi-repo";
|
||||||
|
|
||||||
// Check relative to project root (dev mode)
|
// Check relative to project root (dev mode)
|
||||||
try {
|
try {
|
||||||
const thisDir = dirname(fileURLToPath(import.meta.url));
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -80,20 +83,21 @@ curl -# -L -o "installer-\${PKG_VER}.tar.gz" "\${INSTALLER_BASE}/installer-\${PK
|
|||||||
echo " Extracting..."
|
echo " Extracting..."
|
||||||
tar xf "installer-\${PKG_VER}.tar.gz"
|
tar xf "installer-\${PKG_VER}.tar.gz"
|
||||||
|
|
||||||
# Point to our custom installer_data.json + rootfs repo
|
# Download our custom installer_data.json (installer reads it as a local file)
|
||||||
export INSTALLER_DATA="\${BASTION}/asahi/installer_data.json"
|
echo " Downloading custom installer data from bastion..."
|
||||||
|
curl -sfL -o installer_data.json "\${BASTION}/asahi/installer_data.json"
|
||||||
|
|
||||||
|
# Set REPO_BASE so the installer downloads rootfs from bastion (local, fast)
|
||||||
export REPO_BASE="\${BASTION}/asahi/repo/"
|
export REPO_BASE="\${BASTION}/asahi/repo/"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Using custom installer data from bastion."
|
echo " Using custom partition layout + rootfs from bastion."
|
||||||
echo " This will create:"
|
echo " This will create:"
|
||||||
echo " - Standard Asahi boot infrastructure (m1n1 + U-Boot)"
|
echo " - Standard Asahi boot infrastructure (m1n1 + U-Boot)"
|
||||||
echo " - Fedora Asahi Remix root partition"
|
echo " - Fedora Asahi Remix root partition"
|
||||||
echo " - LVM data partition (remaining space)"
|
echo " - LVM data partition (remaining space)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " On first boot, LVM volumes will be created automatically:"
|
echo " On first boot, LVM volumes are created automatically."
|
||||||
echo " labvg/var (100GB), labvg/varlog (10GB), labvg/home (10GB),"
|
|
||||||
echo " labvg/srv (20GB), labvg/rancher (20GB), labvg/longhorn (rest)"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Run the installer
|
# Run the installer
|
||||||
|
|||||||
@@ -258,12 +258,14 @@ provisioned_at=$(date -Iseconds)
|
|||||||
method=asahi-firstboot
|
method=asahi-firstboot
|
||||||
LABMETA
|
LABMETA
|
||||||
|
|
||||||
# ── Callback to bastion ──────────────────────────────────────────
|
# ── Register with bastion ─────────────────────────────────────────
|
||||||
IP=$(hostname -I | awk '{print $1}')
|
IP=$(hostname -I | awk '{print $1}')
|
||||||
curl -sf -X POST "http://${serverIp}:${httpPort}/api/progress" \\
|
echo "Registering with bastion at ${serverIp}:${httpPort}..."
|
||||||
|
curl -sf -X POST "http://${serverIp}:${httpPort}/api/register" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d "{\\"mac\\":\\"${mac}\\",\\"stage\\":\\"complete\\",\\"detail\\":\\"ready at $IP\\"}" \\
|
-d "{\\"mac\\":\\"${mac}\\",\\"hostname\\":\\"${hostname}\\",\\"role\\":\\"${role}\\",\\"ip\\":\\"$IP\\"}" \\
|
||||||
2>/dev/null || true
|
2>/dev/null && echo " Registered as ${hostname} ($IP)" \\
|
||||||
|
|| echo " WARNING: Could not reach bastion — register manually with: labctl provision register ${mac} ${hostname} --role ${role} --ip $IP"
|
||||||
|
|
||||||
# ── Mark done ────────────────────────────────────────────────────
|
# ── Mark done ────────────────────────────────────────────────────
|
||||||
touch "$MARKER"
|
touch "$MARKER"
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ describe("asahi routes", () => {
|
|||||||
expect(resp.statusCode).toBe(200);
|
expect(resp.statusCode).toBe(200);
|
||||||
expect(resp.headers["content-type"]).toContain("text/x-shellscript");
|
expect(resp.headers["content-type"]).toContain("text/x-shellscript");
|
||||||
expect(resp.body).toContain("#!/bin/bash");
|
expect(resp.body).toContain("#!/bin/bash");
|
||||||
expect(resp.body).toContain("INSTALLER_DATA");
|
expect(resp.body).toContain("installer_data.json");
|
||||||
expect(resp.body).toContain("REPO_BASE");
|
|
||||||
expect(resp.body).toContain("192.168.8.1");
|
expect(resp.body).toContain("192.168.8.1");
|
||||||
expect(resp.body).toContain("install.sh");
|
expect(resp.body).toContain("install.sh");
|
||||||
});
|
});
|
||||||
@@ -77,14 +76,17 @@ describe("asahi routes", () => {
|
|||||||
const os = data.os_list[0];
|
const os = data.os_list[0];
|
||||||
expect(os.name).toContain("Fedora Asahi Lab");
|
expect(os.name).toContain("Fedora Asahi Lab");
|
||||||
|
|
||||||
// Three partitions: EFI + Root + Data
|
// 3 partitions (fallback) or 4 (built: EFI + Boot + Root + Data)
|
||||||
expect(os.partitions).toHaveLength(3);
|
expect(os.partitions.length).toBeGreaterThanOrEqual(3);
|
||||||
expect(os.partitions[0].type).toBe("EFI");
|
expect(os.partitions[0].type).toBe("EFI");
|
||||||
expect(os.partitions[1].type).toBe("Linux");
|
// Last partition should be the expanding Data partition
|
||||||
expect(os.partitions[1].expand).toBe(false);
|
const lastPart = os.partitions[os.partitions.length - 1];
|
||||||
expect(os.partitions[1].image).toBe("root.img");
|
expect(lastPart.type).toBe("Linux");
|
||||||
expect(os.partitions[2].type).toBe("Linux");
|
expect(lastPart.expand).toBe(true);
|
||||||
expect(os.partitions[2].expand).toBe(true);
|
// Root partition (second-to-last) should NOT expand
|
||||||
|
const rootPart = os.partitions[os.partitions.length - 2];
|
||||||
|
expect(rootPart.expand).toBe(false);
|
||||||
|
expect(rootPart.image).toBe("root.img");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("GET /asahi/firstboot.sh returns parameterized script", async () => {
|
it("GET /asahi/firstboot.sh returns parameterized script", async () => {
|
||||||
@@ -185,11 +187,11 @@ describe("renderFirstbootScript", () => {
|
|||||||
expect(script).toContain('hostnamectl set-hostname "test-node"');
|
expect(script).toContain('hostnamectl set-hostname "test-node"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes bastion callback", () => {
|
it("includes bastion self-registration", () => {
|
||||||
const script = renderFirstbootScript({ ...baseParams, role: "worker" });
|
const script = renderFirstbootScript({ ...baseParams, role: "worker" });
|
||||||
expect(script).toContain("/api/progress");
|
expect(script).toContain("/api/register");
|
||||||
expect(script).toContain("aa:bb:cc:dd:ee:ff");
|
expect(script).toContain("aa:bb:cc:dd:ee:ff");
|
||||||
expect(script).toContain("complete");
|
expect(script).toContain("test-node");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes provisioning metadata", () => {
|
it("writes provisioning metadata", () => {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function registerAppCommand(program: Command): void {
|
|||||||
.command("install <target>")
|
.command("install <target>")
|
||||||
.description("Install k3s on a target machine (hostname, IP, or MAC)")
|
.description("Install k3s on a target machine (hostname, IP, or MAC)")
|
||||||
.option("--role <role>", "k3s role: infra (server) or worker (agent)", "infra")
|
.option("--role <role>", "k3s role: infra (server) or worker (agent)", "infra")
|
||||||
.option("--user <user>", "SSH user", "michal")
|
.option("--user <user>", "SSH user", "lab")
|
||||||
.option("--k3s-server <url>", "k3s server URL (required for worker role)")
|
.option("--k3s-server <url>", "k3s server URL (required for worker role)")
|
||||||
.option("--k3s-token <token>", "k3s join token (required for worker role)")
|
.option("--k3s-token <token>", "k3s join token (required for worker role)")
|
||||||
.action(async (target: string, opts: {
|
.action(async (target: string, opts: {
|
||||||
@@ -164,7 +164,7 @@ export function registerAppCommand(program: Command): void {
|
|||||||
k3sCmd
|
k3sCmd
|
||||||
.command("health [target]")
|
.command("health [target]")
|
||||||
.description("Check k3s health (all hosts if no target given)")
|
.description("Check k3s health (all hosts if no target given)")
|
||||||
.option("--user <user>", "SSH user", "michal")
|
.option("--user <user>", "SSH user", "lab")
|
||||||
.action(async (target: string | undefined, opts: { user: string }) => {
|
.action(async (target: string | undefined, opts: { user: string }) => {
|
||||||
const sshKey = findSshKey();
|
const sshKey = findSshKey();
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ export function registerAppCommand(program: Command): void {
|
|||||||
k3sCmd
|
k3sCmd
|
||||||
.command("list")
|
.command("list")
|
||||||
.description("List installed machines and their k3s status")
|
.description("List installed machines and their k3s status")
|
||||||
.option("--user <user>", "SSH user", "michal")
|
.option("--user <user>", "SSH user", "lab")
|
||||||
.action(async (opts: { user: string }) => {
|
.action(async (opts: { user: string }) => {
|
||||||
let state: BastionState;
|
let state: BastionState;
|
||||||
try {
|
try {
|
||||||
|
|||||||
69
bastion/src/cli/src/commands/asahi.ts
Normal file
69
bastion/src/cli/src/commands/asahi.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// CLI command: provision asahi
|
||||||
|
// Prints the curl command to run on the Mac Studio (macOS) to install
|
||||||
|
// Fedora Asahi Remix with lab LVM layout.
|
||||||
|
|
||||||
|
import type { Command } from "commander";
|
||||||
|
import { getLabdClient } from "../api/config.js";
|
||||||
|
|
||||||
|
export function registerAsahiCommand(parent: Command): void {
|
||||||
|
parent
|
||||||
|
.command("asahi")
|
||||||
|
.description("Show instructions to provision an Apple Silicon Mac with Asahi Linux")
|
||||||
|
.action(async () => {
|
||||||
|
// Try to get bastion info to determine the correct URL
|
||||||
|
let bastionUrl = "";
|
||||||
|
try {
|
||||||
|
const bastions = await getLabdClient().getBastions();
|
||||||
|
const online = bastions.find(b => b.status === "online");
|
||||||
|
if (online) {
|
||||||
|
bastionUrl = `http://${online.serverIp}:8080`;
|
||||||
|
}
|
||||||
|
} catch { /* labd not reachable */ }
|
||||||
|
|
||||||
|
if (!bastionUrl) {
|
||||||
|
// Fall back to config
|
||||||
|
const { loadConfig } = await import("../config/index.js");
|
||||||
|
const config = loadConfig();
|
||||||
|
bastionUrl = config.labdUrl ?? "http://<bastion-ip>:8080";
|
||||||
|
// Convert labd URL to bastion URL (labd is on different port/host)
|
||||||
|
bastionUrl = bastionUrl.replace(/:\d+$/, ":8080");
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOLD = "\x1b[1m";
|
||||||
|
const CYAN = "\x1b[36m";
|
||||||
|
const DIM = "\x1b[2m";
|
||||||
|
const RESET = "\x1b[0m";
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(`${BOLD} Asahi Linux Provisioning${RESET}`);
|
||||||
|
console.log(`${DIM} For Apple Silicon Macs (Mac Studio, MacBook, etc.)${RESET}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` Run this command ${BOLD}on the Mac${RESET} (from macOS Terminal):`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` ${CYAN}${BOLD}curl ${bastionUrl}/asahi | sh${RESET}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` The installer will ask a few interactive questions:`);
|
||||||
|
console.log(` ${BOLD}1.${RESET} Action: press ${BOLD}r${RESET} to resize macOS`);
|
||||||
|
console.log(` ${BOLD}2.${RESET} How much space for Linux: choose maximum`);
|
||||||
|
console.log(` ${BOLD}3.${RESET} Confirm the resize operation`);
|
||||||
|
console.log(` ${BOLD}4.${RESET} macOS password for firmware authentication`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` After that, everything is automatic:`);
|
||||||
|
console.log(` - Asahi boot infrastructure (m1n1 + U-Boot)`);
|
||||||
|
console.log(` - Fedora Asahi Remix root partition`);
|
||||||
|
console.log(` - LVM data partition (remaining space)`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` On first boot, LVM volumes are created automatically:`);
|
||||||
|
console.log(` ${DIM}labvg/swap (27GB), labvg/var (100GB), labvg/varlog (10GB),`);
|
||||||
|
console.log(` labvg/home (10GB), labvg/srv (20GB), labvg/rancher (20GB),`);
|
||||||
|
console.log(` labvg/longhorn (remaining space)${RESET}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` After first boot, SSH in and run the firstboot script:`);
|
||||||
|
console.log(` ${BOLD}ssh root@<ip> 'curl -sf ${bastionUrl}/asahi/firstboot.sh?hostname=<name>\\&role=infra | bash'${RESET}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` This sets up LVM and self-registers with the bastion.`);
|
||||||
|
console.log(` Then install k3s:`);
|
||||||
|
console.log(` ${BOLD}labctl app k3s install <hostname> --role infra${RESET}`);
|
||||||
|
console.log("");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export function registerLabcontrollerCommands(appCmd: Command): void {
|
|||||||
lcCmd
|
lcCmd
|
||||||
.command("deploy <target>")
|
.command("deploy <target>")
|
||||||
.description("Deploy labcontroller stack to a k3s node")
|
.description("Deploy labcontroller stack to a k3s node")
|
||||||
.option("--user <user>", "SSH user", "michal")
|
.option("--user <user>", "SSH user", "lab")
|
||||||
.option("--crdb-replicas <n>", "CockroachDB replicas", "1")
|
.option("--crdb-replicas <n>", "CockroachDB replicas", "1")
|
||||||
.action(async (target: string, opts: {
|
.action(async (target: string, opts: {
|
||||||
user: string;
|
user: string;
|
||||||
@@ -193,7 +193,7 @@ export function registerLabcontrollerCommands(appCmd: Command): void {
|
|||||||
lcCmd
|
lcCmd
|
||||||
.command("status [target]")
|
.command("status [target]")
|
||||||
.description("Check labcontroller deployment status (all hosts if no target)")
|
.description("Check labcontroller deployment status (all hosts if no target)")
|
||||||
.option("--user <user>", "SSH user", "michal")
|
.option("--user <user>", "SSH user", "lab")
|
||||||
.action(async (target: string | undefined, opts: { user: string }) => {
|
.action(async (target: string | undefined, opts: { user: string }) => {
|
||||||
const sshKey = findSshKey();
|
const sshKey = findSshKey();
|
||||||
const sshOpts = sshKey ? { keyPath: sshKey } : {};
|
const sshOpts = sshKey ? { keyPath: sshKey } : {};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { registerReprovisionCommand } from "./commands/reprovision.js";
|
|||||||
import { registerDebugCommand } from "./commands/debug.js";
|
import { registerDebugCommand } from "./commands/debug.js";
|
||||||
import { registerForgetCommand } from "./commands/forget.js";
|
import { registerForgetCommand } from "./commands/forget.js";
|
||||||
import { registerRegisterCommand } from "./commands/register.js";
|
import { registerRegisterCommand } from "./commands/register.js";
|
||||||
|
import { registerAsahiCommand } from "./commands/asahi.js";
|
||||||
import { registerLogsCommand } from "./commands/logs.js";
|
import { registerLogsCommand } from "./commands/logs.js";
|
||||||
import { registerMakeIsoCommand } from "./commands/makeiso.js";
|
import { registerMakeIsoCommand } from "./commands/makeiso.js";
|
||||||
import { registerConfigCommand } from "./commands/config.js";
|
import { registerConfigCommand } from "./commands/config.js";
|
||||||
@@ -100,6 +101,7 @@ export function createProgram(): Command {
|
|||||||
registerDebugCommand(provisionCmd);
|
registerDebugCommand(provisionCmd);
|
||||||
registerForgetCommand(provisionCmd);
|
registerForgetCommand(provisionCmd);
|
||||||
registerRegisterCommand(provisionCmd);
|
registerRegisterCommand(provisionCmd);
|
||||||
|
registerAsahiCommand(provisionCmd);
|
||||||
registerLogsCommand(provisionCmd);
|
registerLogsCommand(provisionCmd);
|
||||||
registerMakeIsoCommand(provisionCmd);
|
registerMakeIsoCommand(provisionCmd);
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { runSequential } from "../utils.js";
|
|||||||
import { applyPodSecurityStandards } from "../operations/pod-security.js";
|
import { applyPodSecurityStandards } from "../operations/pod-security.js";
|
||||||
import { checkCertExpiry } from "../operations/cert-check.js";
|
import { checkCertExpiry } from "../operations/cert-check.js";
|
||||||
import { configureLogRotation } from "../operations/log-rotation.js";
|
import { configureLogRotation } from "../operations/log-rotation.js";
|
||||||
|
import { configureLonghornDisk } from "../operations/longhorn-disk.js";
|
||||||
|
|
||||||
export const hardeningGroup: OperationGroup = {
|
export const hardeningGroup: OperationGroup = {
|
||||||
name: "hardening",
|
name: "hardening",
|
||||||
description: "Pod security, certificate check, log rotation",
|
description: "Pod security, certificate check, log rotation, storage",
|
||||||
operations: [
|
operations: [
|
||||||
{ name: "Apply Pod Security Standards", fn: applyPodSecurityStandards },
|
{ name: "Apply Pod Security Standards", fn: applyPodSecurityStandards },
|
||||||
{ name: "Check certificate expiry", fn: checkCertExpiry },
|
{ name: "Check certificate expiry", fn: checkCertExpiry },
|
||||||
{ name: "Configure log rotation", fn: configureLogRotation },
|
{ name: "Configure log rotation", fn: configureLogRotation },
|
||||||
|
{ name: "Configure Longhorn disk", fn: configureLonghornDisk },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import { applyCisHardening } from "../operations/sysctl.js";
|
|||||||
import { disableSwap } from "../operations/swap.js";
|
import { disableSwap } from "../operations/swap.js";
|
||||||
import { disableFirewall } from "../operations/firewall.js";
|
import { disableFirewall } from "../operations/firewall.js";
|
||||||
import { setSelinuxPermissive } from "../operations/selinux.js";
|
import { setSelinuxPermissive } from "../operations/selinux.js";
|
||||||
|
import { enableIscsi } from "../operations/iscsi.js";
|
||||||
|
|
||||||
export const hostPrepGroup: OperationGroup = {
|
export const hostPrepGroup: OperationGroup = {
|
||||||
name: "host-prep",
|
name: "host-prep",
|
||||||
description: "Prepare host for k3s: kernel modules, sysctl, swap, firewall, SELinux",
|
description: "Prepare host for k3s: kernel modules, sysctl, swap, firewall, SELinux, iSCSI",
|
||||||
operations: [
|
operations: [
|
||||||
{ name: "Load kernel modules", fn: loadKernelModules },
|
{ name: "Load kernel modules", fn: loadKernelModules },
|
||||||
{ name: "Apply CIS sysctl", fn: applyCisHardening },
|
{ name: "Apply CIS sysctl", fn: applyCisHardening },
|
||||||
{ name: "Disable swap", fn: disableSwap },
|
{ name: "Disable swap", fn: disableSwap },
|
||||||
{ name: "Disable firewall", fn: disableFirewall },
|
{ name: "Disable firewall", fn: disableFirewall },
|
||||||
{ name: "Set SELinux permissive", fn: setSelinuxPermissive },
|
{ name: "Set SELinux permissive", fn: setSelinuxPermissive },
|
||||||
|
{ name: "Enable iSCSI", fn: enableIscsi },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { loadKernelModules } from "./kernel-modules.js";
|
export { loadKernelModules } from "./kernel-modules.js";
|
||||||
export { applyCisHardening } from "./sysctl.js";
|
export { applyCisHardening } from "./sysctl.js";
|
||||||
export { disableSwap } from "./swap.js";
|
export { disableSwap } from "./swap.js";
|
||||||
|
export { enableIscsi } from "./iscsi.js";
|
||||||
export { disableFirewall } from "./firewall.js";
|
export { disableFirewall } from "./firewall.js";
|
||||||
export { setSelinuxPermissive } from "./selinux.js";
|
export { setSelinuxPermissive } from "./selinux.js";
|
||||||
export { writeK3sConfig } from "./k3s-config.js";
|
export { writeK3sConfig } from "./k3s-config.js";
|
||||||
@@ -13,3 +14,4 @@ export { configureLogRotation } from "./log-rotation.js";
|
|||||||
export { applyDefaultNetworkPolicies } from "./network-policy.js";
|
export { applyDefaultNetworkPolicies } from "./network-policy.js";
|
||||||
export { applyPodSecurityStandards } from "./pod-security.js";
|
export { applyPodSecurityStandards } from "./pod-security.js";
|
||||||
export { checkCertExpiry } from "./cert-check.js";
|
export { checkCertExpiry } from "./cert-check.js";
|
||||||
|
export { configureLonghornDisk } from "./longhorn-disk.js";
|
||||||
|
|||||||
30
bastion/src/modules/modules/k3s/src/operations/iscsi.ts
Normal file
30
bastion/src/modules/modules/k3s/src/operations/iscsi.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Install and enable iSCSI initiator (required by Longhorn storage).
|
||||||
|
// Fedora: iscsi-initiator-utils, Ubuntu: open-iscsi
|
||||||
|
|
||||||
|
import type { Operation, OperationResult } from "../types.js";
|
||||||
|
import { sshOpts } from "../utils.js";
|
||||||
|
|
||||||
|
export const enableIscsi: Operation = async (ctx): Promise<OperationResult> => {
|
||||||
|
// Check if iscsid is already running
|
||||||
|
const check = await ctx.ssh.exec("systemctl is-active iscsid 2>/dev/null", sshOpts(ctx));
|
||||||
|
if (check.stdout.trim() === "active") {
|
||||||
|
return { success: true, changed: false, message: "iSCSI already active" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the package (detect distro)
|
||||||
|
const osRelease = await ctx.ssh.exec("cat /etc/os-release", sshOpts(ctx));
|
||||||
|
const isFedora = osRelease.stdout.includes("fedora") || osRelease.stdout.includes("rhel") || osRelease.stdout.includes("centos");
|
||||||
|
|
||||||
|
const pkg = isFedora ? "iscsi-initiator-utils" : "open-iscsi";
|
||||||
|
const installCmd = isFedora ? `dnf install -y ${pkg}` : `apt-get install -y ${pkg}`;
|
||||||
|
|
||||||
|
const install = await ctx.ssh.exec(installCmd, { timeoutMs: 120_000 });
|
||||||
|
if (install.exitCode !== 0) {
|
||||||
|
return { success: false, changed: false, message: `Failed to install ${pkg}`, error: install.stderr.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable and start
|
||||||
|
await ctx.ssh.exec("systemctl enable --now iscsid", sshOpts(ctx));
|
||||||
|
|
||||||
|
return { success: true, changed: true, message: `Installed ${pkg} and enabled iscsid` };
|
||||||
|
};
|
||||||
@@ -20,6 +20,9 @@ disable:
|
|||||||
- servicelb
|
- servicelb
|
||||||
- traefik
|
- traefik
|
||||||
|
|
||||||
|
node-label:
|
||||||
|
- "node.longhorn.io/create-default-disk=config"
|
||||||
|
|
||||||
kube-apiserver-arg:
|
kube-apiserver-arg:
|
||||||
- "anonymous-auth=false"
|
- "anonymous-auth=false"
|
||||||
- "audit-log-path=/var/log/kubernetes/audit.log"
|
- "audit-log-path=/var/log/kubernetes/audit.log"
|
||||||
@@ -44,6 +47,7 @@ function generateAgentConfig(): string {
|
|||||||
return `protect-kernel-defaults: true
|
return `protect-kernel-defaults: true
|
||||||
node-label:
|
node-label:
|
||||||
- "node-role.kubernetes.io/worker=true"
|
- "node-role.kubernetes.io/worker=true"
|
||||||
|
- "node.longhorn.io/create-default-disk=config"
|
||||||
kubelet-arg:
|
kubelet-arg:
|
||||||
- "protect-kernel-defaults=true"
|
- "protect-kernel-defaults=true"
|
||||||
- "streaming-connection-idle-timeout=5m"
|
- "streaming-connection-idle-timeout=5m"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Annotate nodes with Longhorn default disk config when /var/lib/longhorn exists.
|
||||||
|
// The label is set in k3s config (node-label), but the annotation must be applied via kubectl.
|
||||||
|
|
||||||
|
import type { Operation, OperationResult } from "../types.js";
|
||||||
|
import { sshOpts } from "../utils.js";
|
||||||
|
|
||||||
|
export const configureLonghornDisk: Operation = async (ctx): Promise<OperationResult> => {
|
||||||
|
// Check if /var/lib/longhorn exists on this node
|
||||||
|
const check = await ctx.ssh.exec("test -d /var/lib/longhorn && echo yes || echo no", sshOpts(ctx));
|
||||||
|
if (check.stdout.trim() !== "yes") {
|
||||||
|
return { success: true, changed: false, message: "No /var/lib/longhorn directory — skipping Longhorn disk config" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the node name (hostname as registered in k3s)
|
||||||
|
const nodeNameResult = await ctx.ssh.exec("hostname -f 2>/dev/null || hostname", sshOpts(ctx));
|
||||||
|
const nodeName = nodeNameResult.stdout.trim();
|
||||||
|
|
||||||
|
// Apply the annotation via kubectl (works on server nodes, or via KUBECONFIG on agents)
|
||||||
|
const kubectlPrefix = "k3s kubectl";
|
||||||
|
const annotation = JSON.stringify([{ path: "/var/lib/longhorn", allowScheduling: true }]);
|
||||||
|
|
||||||
|
const result = await ctx.ssh.exec(
|
||||||
|
`${kubectlPrefix} annotate node "${nodeName}" "node.longhorn.io/default-disks-config=${annotation}" --overwrite 2>&1 || true`,
|
||||||
|
sshOpts(ctx),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.stdout.includes("annotated") || result.stdout.includes("unchanged")) {
|
||||||
|
return { success: true, changed: true, message: `Longhorn disk annotation applied to ${nodeName}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If kubectl isn't available (agent node without server access), that's OK —
|
||||||
|
// the label is set, annotation can be applied from the server later
|
||||||
|
return { success: true, changed: false, message: "Longhorn disk label set (annotation requires server kubectl)" };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user