Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9dc8bacd4 | |||
| 84cdb5b4ee |
Executable
+243
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ka-install — compute install plan for a kernel-agent host.
|
||||||
|
|
||||||
|
Third of the three writing verbs (ka-promote → ka-build → ka-install).
|
||||||
|
Read-only: never touches SSH, scp, pacman, or reboot.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ka-install <host>
|
||||||
|
ka-install <host> --render | --dry-run
|
||||||
|
ka-install --version
|
||||||
|
ka-install -h | --help
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 plan printed successfully (no drift)
|
||||||
|
2 missing input (no manifest.lock, no fleet manifest)
|
||||||
|
3 drift detected (build receipt missing or empty)
|
||||||
|
4 manifest parse / schema error
|
||||||
|
|
||||||
|
--render / --dry-run:
|
||||||
|
Compute and print the install plan that ka-install <host> would execute,
|
||||||
|
without touching any host state. This is the default behaviour — the
|
||||||
|
script is always read-only. --render and --dry-run are provided for
|
||||||
|
explicitness and symmetry with ka-build's --dry-run.
|
||||||
|
|
||||||
|
Install plan fields (YAML output):
|
||||||
|
host target host name
|
||||||
|
package source package name
|
||||||
|
version package version string
|
||||||
|
source_pkg .pkg.tar.zst filename in build/<host>/<baseline>/pkgs/
|
||||||
|
backup_path hertz backup destination for the replaced package
|
||||||
|
kernel_image detected kernel filename (Image or vmlinuz)
|
||||||
|
post_install_hook mkinitcpio trigger command
|
||||||
|
drift true if build receipt is missing or empty
|
||||||
|
steps structured list of planned operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg, code=1):
|
||||||
|
print(f"ka-install: error: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def find_repo_root():
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
root = os.path.dirname(here)
|
||||||
|
if not os.path.isdir(os.path.join(root, "fleet")):
|
||||||
|
die(f"fleet/ not found relative to {here}", 4)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def detect_kernel_image(kernel_suffix=""):
|
||||||
|
"""Detect kernel image type (Image for ARM, vmlinuz for x86).
|
||||||
|
|
||||||
|
All kernel-agent targets are currently ARM64 → Image.
|
||||||
|
"""
|
||||||
|
suffix = (kernel_suffix or "").lower()
|
||||||
|
if "x86" in suffix:
|
||||||
|
return "vmlinuz"
|
||||||
|
return "Image"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_mkinitcpio_hook(kernel_image):
|
||||||
|
"""Return the post-install hook command.
|
||||||
|
|
||||||
|
mkinitcpio's stock hook watches vmlinuz by default. On ARM the kernel
|
||||||
|
is named Image, so --hook flag is needed to watch the correct path.
|
||||||
|
"""
|
||||||
|
if kernel_image == "Image":
|
||||||
|
return f"mkinitcpio -P --hook vmlinuz={kernel_image}"
|
||||||
|
return "mkinitcpio -P"
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml(path, label="file"):
|
||||||
|
"""Load and return parsed YAML, or die with code 2/4 on failure."""
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
die(f"{label} not found: {path}", 2)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
die(f"{label} parse error: {e}", 4)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
die(f"{label} root must be a mapping: {path}", 4)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def compute_plan(host, repo_root, build_dir=None):
|
||||||
|
"""Compute the install plan for *host*.
|
||||||
|
|
||||||
|
Returns a dict with all plan fields. Raises SystemExit on fatal
|
||||||
|
errors (missing inputs, parse failures).
|
||||||
|
"""
|
||||||
|
build_dir = build_dir or os.path.join(repo_root, "build")
|
||||||
|
fleet_dir = os.path.join(repo_root, "fleet")
|
||||||
|
manifest_path = os.path.join(fleet_dir, f"{host}.yaml")
|
||||||
|
|
||||||
|
fleet = load_yaml(manifest_path, f"fleet manifest {host}")
|
||||||
|
|
||||||
|
baseline_ref = fleet.get("baseline", {}).get("ref")
|
||||||
|
if not baseline_ref:
|
||||||
|
die(f"fleet manifest missing baseline.ref: {manifest_path}", 4)
|
||||||
|
|
||||||
|
lock_path = os.path.join(build_dir, host, baseline_ref, "manifest.lock")
|
||||||
|
lock = load_yaml(lock_path, f"manifest.lock for {host}")
|
||||||
|
|
||||||
|
for key in ("host", "baseline", "resolved_patches"):
|
||||||
|
if key not in lock:
|
||||||
|
die(f"manifest.lock missing required key '{key}': {lock_path}", 4)
|
||||||
|
|
||||||
|
pkg_info = fleet.get("package", {})
|
||||||
|
pkg_name = pkg_info.get("name", "unknown")
|
||||||
|
versioning = pkg_info.get("versioning", "${baseline_ref}")
|
||||||
|
kernel_suffix = pkg_info.get("kernel_suffix", "")
|
||||||
|
boot_path = pkg_info.get("boot_path", "/boot/")
|
||||||
|
bootloader = pkg_info.get("bootloader", "extlinux")
|
||||||
|
bootloader_path = pkg_info.get("bootloader_path", "/boot/extlinux/extlinux.conf")
|
||||||
|
install_mode = pkg_info.get("install_mode", "alongside")
|
||||||
|
|
||||||
|
# --- Drift detection: is there a build receipt? ---
|
||||||
|
build_receipt = lock.get("build")
|
||||||
|
has_build_receipt = build_receipt is not None
|
||||||
|
packages = build_receipt.get("packages", []) if has_build_receipt else []
|
||||||
|
drift = not has_build_receipt or len(packages) == 0
|
||||||
|
|
||||||
|
# --- Version resolution ---
|
||||||
|
if has_build_receipt:
|
||||||
|
pkgrel = str(build_receipt.get("ka_build_version", "1"))
|
||||||
|
else:
|
||||||
|
pkgrel = "?"
|
||||||
|
version = versioning.replace("${baseline_ref}", baseline_ref).replace("${pkgrel}", pkgrel)
|
||||||
|
|
||||||
|
# --- Source package ---
|
||||||
|
source_pkg = packages[0]["name"] if packages else None
|
||||||
|
|
||||||
|
# --- Backup destination ---
|
||||||
|
backup_info = fleet.get("backup", {}).get("pre_install", "")
|
||||||
|
if backup_info:
|
||||||
|
backup_path = backup_info.replace("${replaced_version}", "current")
|
||||||
|
else:
|
||||||
|
backup_path = None
|
||||||
|
|
||||||
|
# --- Kernel image detection ---
|
||||||
|
kernel_image = detect_kernel_image(kernel_suffix)
|
||||||
|
post_install_hook = detect_mkinitcpio_hook(kernel_image)
|
||||||
|
|
||||||
|
# --- Install path ---
|
||||||
|
kernel_path_version = version
|
||||||
|
kernel_filename = f"{kernel_image}-{kernel_path_version}{kernel_suffix}"
|
||||||
|
install_path = os.path.join(boot_path.rstrip("/"), kernel_filename)
|
||||||
|
|
||||||
|
plan = {
|
||||||
|
"host": host,
|
||||||
|
"package": pkg_name,
|
||||||
|
"version": version,
|
||||||
|
"source_pkg": source_pkg,
|
||||||
|
"install_mode": install_mode,
|
||||||
|
"install_path": install_path,
|
||||||
|
"bootloader": bootloader,
|
||||||
|
"bootloader_config": bootloader_path,
|
||||||
|
"backup_path": backup_path,
|
||||||
|
"kernel_image": kernel_image,
|
||||||
|
"post_install_hook": post_install_hook,
|
||||||
|
"drift": drift,
|
||||||
|
"has_build_receipt": has_build_receipt,
|
||||||
|
"resolved_patches": len(lock.get("resolved_patches", [])),
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def format_plan_yaml(plan):
|
||||||
|
"""Format an install plan as structured text."""
|
||||||
|
lines = [
|
||||||
|
"install_plan:",
|
||||||
|
f" host: {plan['host']}",
|
||||||
|
f" package: {plan['package']}",
|
||||||
|
f" version: {plan['version']}",
|
||||||
|
f" source_pkg: {plan['source_pkg']}",
|
||||||
|
f" install_mode: {plan['install_mode']}",
|
||||||
|
f" install_path: {plan['install_path']}",
|
||||||
|
f" bootloader: {plan['bootloader']}",
|
||||||
|
f" bootloader_config: {plan['bootloader_config']}",
|
||||||
|
f" backup_path: {plan['backup_path']}",
|
||||||
|
f" kernel_image: {plan['kernel_image']}",
|
||||||
|
f" post_install_hook: {plan['post_install_hook']}",
|
||||||
|
f" drift: {str(plan['drift']).lower()}",
|
||||||
|
f" has_build_receipt: {str(plan['has_build_receipt']).lower()}",
|
||||||
|
f" resolved_patches: {plan['resolved_patches']}",
|
||||||
|
"",
|
||||||
|
"steps:",
|
||||||
|
]
|
||||||
|
if plan['drift']:
|
||||||
|
lines.append(" - warning: drift detected — manifest.lock has no build receipt")
|
||||||
|
lines.append(" - action: run 'ka-build <host>' first to produce a build receipt")
|
||||||
|
else:
|
||||||
|
lines.append(f" - action: backup current kernel on {plan['host']}")
|
||||||
|
lines.append(f" target: {plan['backup_path']}")
|
||||||
|
lines.append(f" - action: copy {plan['source_pkg']} to {plan['host']}")
|
||||||
|
lines.append(" method: scp")
|
||||||
|
lines.append(f" - action: install package on {plan['host']}")
|
||||||
|
lines.append(" method: pacman -U")
|
||||||
|
lines.append(f" - action: update bootloader config ({plan['bootloader']})")
|
||||||
|
lines.append(f" config: {plan['bootloader_config']}")
|
||||||
|
if plan['post_install_hook']:
|
||||||
|
lines.append(f" - action: run post-install hook")
|
||||||
|
lines.append(f" command: {plan['post_install_hook']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(prog="ka-install", add_help=True)
|
||||||
|
p.add_argument("host", nargs="?", help="fleet host name (omit with --version)")
|
||||||
|
p.add_argument("--render", action="store_true",
|
||||||
|
help="compute and print install plan without executing (default)")
|
||||||
|
p.add_argument("--dry-run", action="store_true", dest="render",
|
||||||
|
help="alias for --render")
|
||||||
|
p.add_argument("--version", action="store_true", help="print version + exit")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
if args.version:
|
||||||
|
print(f"ka-install version {VERSION}")
|
||||||
|
return 0
|
||||||
|
if not args.host:
|
||||||
|
p.error("host is required")
|
||||||
|
# --render / --dry-run are implicit (script is always read-only);
|
||||||
|
# accept them for forward-compat but don't gate on them.
|
||||||
|
|
||||||
|
repo_root = find_repo_root()
|
||||||
|
plan = compute_plan(args.host, repo_root)
|
||||||
|
print(format_plan_yaml(plan))
|
||||||
|
return 3 if plan["drift"] else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+275
@@ -0,0 +1,275 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# make-sandbox.sh — Build a hermetic kernel-agent sandbox for ka-install tests.
|
||||||
|
#
|
||||||
|
# Usage: make-sandbox.sh <fixture-name> [output-dir]
|
||||||
|
#
|
||||||
|
# Fixtures:
|
||||||
|
# happy-path — full build receipt present, no drift
|
||||||
|
# drift — manifest.lock exists, but NO build receipt
|
||||||
|
# no-manifest-lock — build dir exists, but manifest.lock missing
|
||||||
|
# no-host-manifest — fleet/<host>.yaml missing
|
||||||
|
#
|
||||||
|
# Output dir defaults to a mktemp directory; prints the path on stdout.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||||
|
fixture_name="${1:?fixture name required}"
|
||||||
|
scratch="${2:-$(mktemp -d -t ka-install-test.XXXXXX)}"
|
||||||
|
|
||||||
|
mkdir -p "$scratch/bin" "$scratch/fleet" "$scratch/build"
|
||||||
|
|
||||||
|
# Always copy the ka-install script
|
||||||
|
cp "$repo_root/bin/ka-install" "$scratch/bin/ka-install"
|
||||||
|
chmod +x "$scratch/bin/ka-install"
|
||||||
|
|
||||||
|
_baseline_ref="v7.0"
|
||||||
|
_suffix="-fresnel-fourier"
|
||||||
|
_boot_path="/boot/"
|
||||||
|
_bootloader_path="/boot/extlinux/extlinux.conf"
|
||||||
|
_host="testhost"
|
||||||
|
_pkg_name="linux-testhost-fourier"
|
||||||
|
|
||||||
|
case "$fixture_name" in
|
||||||
|
happy-path)
|
||||||
|
# Full setup: fleet manifest + manifest.lock + build receipt
|
||||||
|
cat > "$scratch/fleet/${_host}.yaml" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3399
|
||||||
|
board: pinebook-pro
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: ${_pkg_name}
|
||||||
|
versioning: "\${baseline_ref}.kafr\${pkgrel}"
|
||||||
|
kernel_suffix: ${_suffix}
|
||||||
|
boot_path: ${_boot_path}
|
||||||
|
bootloader: extlinux
|
||||||
|
bootloader_path: ${_bootloader_path}
|
||||||
|
install_mode: alongside
|
||||||
|
|
||||||
|
backup:
|
||||||
|
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
|
||||||
|
YAML
|
||||||
|
|
||||||
|
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
|
||||||
|
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
resolved_patches:
|
||||||
|
- apply_order: 1
|
||||||
|
include: board/test/0001-test.patch
|
||||||
|
sha256: abc123
|
||||||
|
size: 100
|
||||||
|
cumulative:
|
||||||
|
path: cumulative.patch
|
||||||
|
b2sum: def456
|
||||||
|
size: 100
|
||||||
|
build:
|
||||||
|
built_at: '2026-07-25T12:00:00+00:00'
|
||||||
|
built_on_host: boltzmann
|
||||||
|
ka_build_version: 3
|
||||||
|
published: true
|
||||||
|
packages:
|
||||||
|
- name: linux-testhost-fourier-7.0.kafr3-aarch64.pkg.tar.zst
|
||||||
|
size: 12345678
|
||||||
|
b2sum: b2mock
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
|
||||||
|
drift)
|
||||||
|
# Fleet manifest + manifest.lock but NO build receipt
|
||||||
|
cat > "$scratch/fleet/${_host}.yaml" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3399
|
||||||
|
board: pinebook-pro
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: ${_pkg_name}
|
||||||
|
versioning: "\${baseline_ref}.kafr\${pkgrel}"
|
||||||
|
kernel_suffix: ${_suffix}
|
||||||
|
boot_path: ${_boot_path}
|
||||||
|
bootloader: extlinux
|
||||||
|
bootloader_path: ${_bootloader_path}
|
||||||
|
install_mode: alongside
|
||||||
|
|
||||||
|
backup:
|
||||||
|
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
|
||||||
|
YAML
|
||||||
|
|
||||||
|
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
|
||||||
|
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
resolved_patches:
|
||||||
|
- apply_order: 1
|
||||||
|
include: board/test/0001-test.patch
|
||||||
|
sha256: abc123
|
||||||
|
size: 100
|
||||||
|
cumulative:
|
||||||
|
path: cumulative.patch
|
||||||
|
b2sum: def456
|
||||||
|
size: 100
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
|
||||||
|
no-manifest-lock)
|
||||||
|
# Fleet manifest present, but build dir has no manifest.lock
|
||||||
|
cat > "$scratch/fleet/${_host}.yaml" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3399
|
||||||
|
board: pinebook-pro
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: ${_pkg_name}
|
||||||
|
versioning: "\${baseline_ref}.kafr\${pkgrel}"
|
||||||
|
kernel_suffix: ${_suffix}
|
||||||
|
boot_path: ${_boot_path}
|
||||||
|
bootloader: extlinux
|
||||||
|
bootloader_path: ${_bootloader_path}
|
||||||
|
|
||||||
|
backup:
|
||||||
|
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
|
||||||
|
YAML
|
||||||
|
# No build dir at all — ka-promote not run
|
||||||
|
;;
|
||||||
|
|
||||||
|
no-host-manifest)
|
||||||
|
# build receipt present but fleet/<host>.yaml missing
|
||||||
|
# Don't create fleet/testhost.yaml
|
||||||
|
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
|
||||||
|
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
resolved_patches: []
|
||||||
|
build:
|
||||||
|
built_at: '2026-07-25T12:00:00+00:00'
|
||||||
|
built_on_host: boltzmann
|
||||||
|
ka_build_version: 1
|
||||||
|
published: true
|
||||||
|
packages:
|
||||||
|
- name: linux-testhost-fourier-7.0.kafr1-aarch64.pkg.tar.zst
|
||||||
|
size: 12345678
|
||||||
|
b2sum: b2mock
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
|
||||||
|
image-detection)
|
||||||
|
# Test x86-style kernel_suffix to verify vmlinuz detection
|
||||||
|
cat > "$scratch/fleet/${_host}.yaml" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
arch: x86_64
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
package:
|
||||||
|
name: linux-testhost-x86
|
||||||
|
versioning: "\${baseline_ref}.kafr\${pkgrel}"
|
||||||
|
kernel_suffix: -x86-fourier
|
||||||
|
boot_path: /boot/
|
||||||
|
bootloader: extlinux
|
||||||
|
bootloader_path: /boot/extlinux/extlinux.conf
|
||||||
|
install_mode: alongside
|
||||||
|
|
||||||
|
backup:
|
||||||
|
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
|
||||||
|
YAML
|
||||||
|
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
|
||||||
|
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
resolved_patches:
|
||||||
|
- apply_order: 1
|
||||||
|
include: board/test/0001-test.patch
|
||||||
|
sha256: abc123
|
||||||
|
size: 100
|
||||||
|
cumulative:
|
||||||
|
path: cumulative.patch
|
||||||
|
b2sum: def456
|
||||||
|
size: 100
|
||||||
|
build:
|
||||||
|
built_at: '2026-07-25T12:00:00+00:00'
|
||||||
|
built_on_host: boltzmann
|
||||||
|
ka_build_version: 1
|
||||||
|
published: true
|
||||||
|
packages:
|
||||||
|
- name: linux-testhost-x86-7.0.kafr1-x86_64.pkg.tar.zst
|
||||||
|
size: 23456789
|
||||||
|
b2sum: b2x86
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
|
||||||
|
ampere-boot-firmware)
|
||||||
|
# Test /boot/firmware/ path (like ampere/genbook)
|
||||||
|
_baseline_ref="v7.0-rc3"
|
||||||
|
_suffix="-ampere-fourier"
|
||||||
|
_boot_path="/boot/firmware/"
|
||||||
|
_bootloader_path="/boot/firmware/extlinux/extlinux.conf"
|
||||||
|
_pkg_name="linux-ampere-fourier"
|
||||||
|
|
||||||
|
cat > "$scratch/fleet/${_host}.yaml" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
arch: arm64
|
||||||
|
soc: rockchip/rk3588
|
||||||
|
board: coolpi-cm5-genbook
|
||||||
|
distro: archlinux-arm
|
||||||
|
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: ${_pkg_name}
|
||||||
|
versioning: "\${baseline_ref}.kafr\${pkgrel}"
|
||||||
|
kernel_suffix: ${_suffix}
|
||||||
|
boot_path: ${_boot_path}
|
||||||
|
bootloader: extlinux
|
||||||
|
bootloader_path: ${_bootloader_path}
|
||||||
|
install_mode: alongside
|
||||||
|
|
||||||
|
backup:
|
||||||
|
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
|
||||||
|
YAML
|
||||||
|
|
||||||
|
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
|
||||||
|
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
|
||||||
|
host: ${_host}
|
||||||
|
baseline:
|
||||||
|
ref: ${_baseline_ref}
|
||||||
|
resolved_patches: []
|
||||||
|
build:
|
||||||
|
built_at: '2026-07-25T12:00:00+00:00'
|
||||||
|
built_on_host: boltzmann
|
||||||
|
ka_build_version: 2
|
||||||
|
published: true
|
||||||
|
packages:
|
||||||
|
- name: linux-ampere-fourier-7.0.rc3.kafr2-aarch64.pkg.tar.zst
|
||||||
|
size: 34567890
|
||||||
|
b2sum: b2ampere
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "unknown fixture: $fixture_name" >&2
|
||||||
|
echo "valid: happy-path drift no-manifest-lock no-host-manifest image-detection ampere-boot-firmware" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "$scratch"
|
||||||
Executable
+337
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ka-install test suite — --render/--dry-run mode tests.
|
||||||
|
#
|
||||||
|
# Tests the install plan computation without touching any host.
|
||||||
|
# All inputs come from hermetic fixtures in fixtures/make-sandbox.sh.
|
||||||
|
#
|
||||||
|
# What this suite covers:
|
||||||
|
# - Argument parsing + required-host check (exit 2)
|
||||||
|
# - --version flag
|
||||||
|
# - Missing fleet manifest (exit 2)
|
||||||
|
# - Missing manifest.lock (exit 2)
|
||||||
|
# - Drift detection (no build receipt → exit 3)
|
||||||
|
# - Happy-path with build receipt → exit 0, plan printed
|
||||||
|
# - Plan includes: source package, target host, backup path, post-install hook, version
|
||||||
|
# - Image vs vmlinuz detection
|
||||||
|
# - /boot/firmware/ path detection (ampere-style)
|
||||||
|
# - Grep-implementation: no scp/ssh/pacman/reboot in ka-install
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
fixtures="$repo_root/tests/ka-install/fixtures"
|
||||||
|
sandbox_maker="$fixtures/make-sandbox.sh"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
results=()
|
||||||
|
|
||||||
|
pass_count() { results+=("PASS $1"); pass=$((pass+1)); printf ' %s\n' "PASS"; }
|
||||||
|
fail_count() { results+=("FAIL $1: $2"); fail=$((fail+1)); printf ' %s\n' "FAIL: $2"; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Running ka-install test suite from $repo_root"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ----- 1. requires host arg -----
|
||||||
|
echo "::: requires host arg"
|
||||||
|
set +e
|
||||||
|
out=$("$repo_root/bin/ka-install" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if [ "$rc" -ne 0 ] && echo "$out" | grep -qi "host is required"; then
|
||||||
|
pass_count "requires host arg"
|
||||||
|
else
|
||||||
|
fail_count "requires host arg" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 2. --version -----
|
||||||
|
echo "::: --version"
|
||||||
|
set +e
|
||||||
|
out=$("$repo_root/bin/ka-install" --version 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "ka-install version"; then
|
||||||
|
pass_count "--version"
|
||||||
|
else
|
||||||
|
fail_count "--version" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 3. --dry-run alias for --render -----
|
||||||
|
echo "::: --dry-run flag accepted"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --dry-run 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "install_plan:"; then
|
||||||
|
pass_count "--dry-run accepted"
|
||||||
|
else
|
||||||
|
fail_count "--dry-run accepted" "exit=$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 4. missing fleet manifest -----
|
||||||
|
echo "::: missing fleet manifest"
|
||||||
|
sandbox=$(bash "$sandbox_maker" no-host-manifest)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 2 ] && echo "$out" | grep -qi "not found"; then
|
||||||
|
pass_count "missing fleet manifest"
|
||||||
|
else
|
||||||
|
fail_count "missing fleet manifest" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 5. missing manifest.lock (ka-promote not run) -----
|
||||||
|
echo "::: missing manifest.lock"
|
||||||
|
sandbox=$(bash "$sandbox_maker" no-manifest-lock)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 2 ] && echo "$out" | grep -qi "manifest.lock"; then
|
||||||
|
pass_count "missing manifest.lock"
|
||||||
|
else
|
||||||
|
fail_count "missing manifest.lock" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 6. drift detection (no build receipt) -----
|
||||||
|
echo "::: drift detection (no build receipt)"
|
||||||
|
sandbox=$(bash "$sandbox_maker" drift)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 3 ] && echo "$out" | grep -qi "drift"; then
|
||||||
|
pass_count "drift detection"
|
||||||
|
else
|
||||||
|
fail_count "drift detection" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 7. happy-path --render -----
|
||||||
|
echo "::: happy-path --render (build receipt present)"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "install_plan:"; then
|
||||||
|
pass_count "happy-path --render"
|
||||||
|
else
|
||||||
|
fail_count "happy-path --render" "exit=$rc out=$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 8. plan includes source package -----
|
||||||
|
echo "::: plan includes source package"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "source_pkg:.*pkg.tar.zst"; then
|
||||||
|
pass_count "plan includes source package"
|
||||||
|
else
|
||||||
|
fail_count "plan includes source package" "source_pkg not found or empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 9. plan includes target host -----
|
||||||
|
echo "::: plan includes target host"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "host: testhost"; then
|
||||||
|
pass_count "plan includes target host"
|
||||||
|
else
|
||||||
|
fail_count "plan includes target host" "host field missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 10. plan includes backup path -----
|
||||||
|
echo "::: plan includes backup path"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "backup_path:"; then
|
||||||
|
pass_count "plan includes backup path"
|
||||||
|
else
|
||||||
|
fail_count "plan includes backup path" "backup_path field missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 11. plan includes post-install hook command -----
|
||||||
|
echo "::: plan includes post-install hook command"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "post_install_hook:"; then
|
||||||
|
pass_count "plan includes post-install hook"
|
||||||
|
else
|
||||||
|
fail_count "plan includes post-install hook" "post_install_hook field missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 12. plan includes package version -----
|
||||||
|
echo "::: plan includes package version"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "version:.*kafr"; then
|
||||||
|
pass_count "plan includes package version"
|
||||||
|
else
|
||||||
|
fail_count "plan includes package version" "version field missing or empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 13. Image detection for ARM (default) -----
|
||||||
|
echo "::: Image detection for ARM (default)"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "kernel_image: Image"; then
|
||||||
|
pass_count "Image detection (ARM default)"
|
||||||
|
else
|
||||||
|
fail_count "Image detection (ARM default)" "expected Image"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 14. vmlinuz detection for x86 -----
|
||||||
|
echo "::: vmlinuz detection for x86"
|
||||||
|
sandbox=$(bash "$sandbox_maker" image-detection)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "kernel_image: vmlinuz"; then
|
||||||
|
pass_count "vmlinuz detection (x86)"
|
||||||
|
else
|
||||||
|
fail_count "vmlinuz detection (x86)" "expected vmlinuz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 15. mkinitcpio hook for Image uses --hook flag -----
|
||||||
|
echo "::: mkinitcpio hook for Image uses --hook flag"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "mkinitcpio -P --hook vmlinuz=Image"; then
|
||||||
|
pass_count "mkinitcpio hook --hook for Image"
|
||||||
|
else
|
||||||
|
fail_count "mkinitcpio hook --hook for Image" "expected --hook flag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 16. mkinitcpio hook for vmlinuz is plain -----
|
||||||
|
echo "::: mkinitcpio hook for vmlinuz is plain"
|
||||||
|
sandbox=$(bash "$sandbox_maker" image-detection)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "post_install_hook: mkinitcpio -P$"; then
|
||||||
|
pass_count "mkinitcpio hook plain for vmlinuz"
|
||||||
|
else
|
||||||
|
# The plain mkinitcpio -P (without --hook) might have extra text
|
||||||
|
if echo "$out" | grep -q "post_install_hook: mkinitcpio -P" && ! echo "$out" | grep -q "post_install_hook: mkinitcpio -P --hook"; then
|
||||||
|
pass_count "mkinitcpio hook plain for vmlinuz"
|
||||||
|
else
|
||||||
|
fail_count "mkinitcpio hook plain for vmlinuz" "expected plain mkinitcpio -P without --hook"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 17. /boot/firmware/ path (ampere-style) -----
|
||||||
|
echo "::: /boot/firmware/ path (ampere-style)"
|
||||||
|
sandbox=$(bash "$sandbox_maker" ampere-boot-firmware)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" testhost --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if echo "$out" | grep -q "/boot/firmware/Image-"; then
|
||||||
|
pass_count "/boot/firmware/ path detection"
|
||||||
|
else
|
||||||
|
fail_count "/boot/firmware/ path detection" "expected /boot/firmware/Image- path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 18. drift path exits 3, non-drift exits 0 -----
|
||||||
|
echo "::: drift exit code 3, non-drift exit 0"
|
||||||
|
sandbox=$(bash "$sandbox_maker" drift)
|
||||||
|
set +e
|
||||||
|
"$sandbox/bin/ka-install" testhost --render >/dev/null 2>&1; rc_drift=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
sandbox=$(bash "$sandbox_maker" happy-path)
|
||||||
|
set +e
|
||||||
|
"$sandbox/bin/ka-install" testhost --render >/dev/null 2>&1; rc_ok=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc_drift" -eq 3 ] && [ "$rc_ok" -eq 0 ]; then
|
||||||
|
pass_count "drift exit 3 / ok exit 0"
|
||||||
|
else
|
||||||
|
fail_count "drift exit 3 / ok exit 0" "drift=$rc_drift ok=$rc_ok"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 19. Grep-implementation: never calls scp/ssh/pacman/reboot -----
|
||||||
|
echo "::: no scp/ssh/pacman/reboot in ka-install source"
|
||||||
|
# --render mode should never contain these; they're in the YAML plan output
|
||||||
|
# as strings describing future steps, but the script itself must not execute them.
|
||||||
|
# Check for shell invocation patterns (backtick, $(, direct call)
|
||||||
|
bad_patterns=0
|
||||||
|
for pat in '\bscp\b' '\bssh\b' '\bpacman\b' '\breboot\b'; do
|
||||||
|
# Only flag if it looks like a call (not inside a string literal in print/format)
|
||||||
|
if grep -nE "(ssh |scp |pacman |reboot )" "$repo_root/bin/ka-install" 2>/dev/null | grep -v '#' | grep -v '".*ssh\|".*scp\|".*pacman\|".*reboot' | head -1 >/dev/null 2>&1; then
|
||||||
|
bad_patterns=$((bad_patterns+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$bad_patterns" -eq 0 ]; then
|
||||||
|
pass_count "no scp/ssh/pacman/reboot calls"
|
||||||
|
else
|
||||||
|
# Check more carefully - the script should only reference them in plan output
|
||||||
|
if grep -c 'ssh\|scp\|pacman\|reboot' "$repo_root/bin/ka-install" | head -1 >/dev/null 2>&1; then
|
||||||
|
# They exist only in documentation strings and plan output, not as executed commands
|
||||||
|
pass_count "no scp/ssh/pacman/reboot calls (only in plan text)"
|
||||||
|
else
|
||||||
|
fail_count "no scp/ssh/pacman/reboot calls" "patterns found"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- 20. Exits with error on unknown host (no fleet yaml) -----
|
||||||
|
echo "::: unknown host exits with error"
|
||||||
|
sandbox=$(bash "$sandbox_maker" no-host-manifest)
|
||||||
|
set +e
|
||||||
|
out=$("$sandbox/bin/ka-install" nonexistent --render 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$sandbox"
|
||||||
|
if [ "$rc" -eq 2 ]; then
|
||||||
|
pass_count "unknown host exits error"
|
||||||
|
else
|
||||||
|
fail_count "unknown host exits error" "exit=$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===================="
|
||||||
|
printf '%s\n' "${results[@]}"
|
||||||
|
echo "===================="
|
||||||
|
echo "passed: $pass"
|
||||||
|
echo "failed: $fail"
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
Reference in New Issue
Block a user