Slice 1b of the self-update story. The installer now sets up a versioned
layout — install extracts the resource-manager tarball to a staging dir,
reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/,
and creates /opt/furtka/current as a symlink pointing at it. All runtime
references (Caddy, wrapper, systemd ExecStart) go through /current, so
Phase 2's self-update just flips the symlink atomically.
Systemd units move from hand-written files in /etc/systemd/system/ to
`systemctl link /opt/furtka/current/assets/systemd/*` — one link per
unit, stable across upgrades because the link target is /current. The
furtka-status + furtka-welcome units now ExecStart the shipped scripts
directly from /opt/furtka/current/assets/bin/, which means we no longer
copy those scripts to /usr/local/bin/ at install time.
Runtime JSON (status.json, furtka.json, update-state.json) moves to
/var/lib/furtka/ so self-updates never clobber it. Caddy serves those
three paths from there; everything else from /opt/furtka/current/assets/www/.
The __HOSTNAME__ sed-template hack is gone. At install time we write
/var/lib/furtka/furtka.json with {hostname, install_date, version}, and
the landing page's JS reads it on load to populate the hostname chip
and to build the SMB deep-link for the fileshare tile. First paint gets
a "—" placeholder and hydrates once fetch completes.
Test updates:
- test_webinstaller_assets enforces the new command shape (extract-to-
staging, ln -sfn /opt/furtka/current, systemctl link per unit,
no writes to /srv/furtka/www/).
- test_app's legacy "payload present" / "payload absent" tests match
the new layout too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
7.3 KiB
Python
197 lines
7.3 KiB
Python
from app import (
|
|
build_archinstall_config,
|
|
build_archinstall_creds,
|
|
validate_step1,
|
|
)
|
|
|
|
|
|
def test_validate_step1_accepts_good_input():
|
|
errors, values = validate_step1(
|
|
{
|
|
"hostname": "furtka",
|
|
"username": "daniel",
|
|
"password": "topsecretpw",
|
|
"password2": "topsecretpw",
|
|
"language": "de",
|
|
}
|
|
)
|
|
assert errors == []
|
|
assert values == {
|
|
"hostname": "furtka",
|
|
"username": "daniel",
|
|
"password": "topsecretpw",
|
|
"language": "de",
|
|
}
|
|
|
|
|
|
def test_validate_step1_collects_all_errors():
|
|
errors, _ = validate_step1(
|
|
{
|
|
"hostname": "BAD!",
|
|
"username": "1bad",
|
|
"password": "short",
|
|
"password2": "mismatch",
|
|
"language": "xx",
|
|
}
|
|
)
|
|
assert len(errors) == 5
|
|
|
|
|
|
def test_build_archinstall_config_uses_selected_locale(monkeypatch):
|
|
# build_disk_config imports archinstall lazily; archinstall isn't
|
|
# installed in CI (only runs on the live ISO), so stub it out.
|
|
import app as app_module
|
|
|
|
monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d})
|
|
|
|
cfg = build_archinstall_config(
|
|
{
|
|
"hostname": "h",
|
|
"username": "u",
|
|
"password": "pw12345678",
|
|
"language": "pl",
|
|
"boot_drive": "/dev/sda",
|
|
}
|
|
)
|
|
assert cfg["disk_config"] == {"stubbed_device": "/dev/sda"}
|
|
assert cfg["hostname"] == "h"
|
|
assert cfg["locale_config"]["locale"] == "pl_PL.UTF-8"
|
|
# Users moved out of config into creds once we adopted archinstall 4.x's
|
|
# `!password` sentinel; config only carries a gpasswd in custom_commands
|
|
# so the user lands in the docker group after docker is pacstrapped.
|
|
assert "users" not in cfg
|
|
assert cfg["custom_commands"][0] == "gpasswd -a u docker"
|
|
|
|
|
|
def test_build_archinstall_config_includes_post_install_bootstrap(monkeypatch, tmp_path):
|
|
# The installed system should come up with a Furtka landing page at
|
|
# http://<hostname>.local. That means caddy + avahi pacstrapped, the
|
|
# matching services enabled, a Caddyfile written into the target rootfs,
|
|
# nss-mdns spliced into nsswitch.conf, and the resource-manager tarball
|
|
# extracted into the versioned /opt/furtka/current layout.
|
|
import app as app_module
|
|
|
|
# Fake payload so _resource_manager_commands emits its full cmd tree.
|
|
fake_payload = tmp_path / "payload.tar.gz"
|
|
fake_payload.write_bytes(b"\x1f\x8b\x08\x00fake")
|
|
monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", fake_payload)
|
|
monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d})
|
|
|
|
cfg = build_archinstall_config(
|
|
{
|
|
"hostname": "heimserver",
|
|
"username": "u",
|
|
"password": "pw12345678",
|
|
"language": "en",
|
|
"boot_drive": "/dev/sda",
|
|
}
|
|
)
|
|
|
|
for pkg in ("caddy", "avahi", "nss-mdns"):
|
|
assert pkg in cfg["packages"]
|
|
# Packaged units go in `services` (enabled before custom_commands runs);
|
|
# our own units don't exist at that point, so they must be enabled from
|
|
# within custom_commands after the unit files land on disk.
|
|
for svc in ("caddy", "avahi-daemon"):
|
|
assert svc in cfg["services"]
|
|
assert "furtka-welcome" not in cfg["services"]
|
|
assert "furtka-status.timer" not in cfg["services"]
|
|
|
|
joined = "\n".join(cfg["custom_commands"])
|
|
# Every furtka-* unit is systemctl-linked from the shipped asset tree and
|
|
# then enabled — no hand-written files under /etc/systemd/system/.
|
|
assert "systemctl link /opt/furtka/current/assets/systemd/" in joined
|
|
assert "systemctl enable furtka-api.service" in joined
|
|
for path in (
|
|
"/etc/caddy/Caddyfile",
|
|
"/var/lib/furtka/status.json",
|
|
"/var/lib/furtka/furtka.json",
|
|
):
|
|
assert path in joined, f"expected {path} to be written by custom_commands"
|
|
# /srv/furtka/www/ is retired — no writes should target it anymore.
|
|
assert "/srv/furtka/www" not in joined
|
|
|
|
assert "mdns_minimal" in joined
|
|
assert "nsswitch.conf" in joined
|
|
# Hostname is injected via /var/lib/furtka/furtka.json, not via sed.
|
|
assert "__HOSTNAME__" not in joined
|
|
|
|
|
|
def test_resource_manager_payload_landed_when_present(monkeypatch, tmp_path):
|
|
# When iso/build.sh has staged the resource-manager tarball, the
|
|
# post-install commands should untar it into /opt/furtka/versions/<ver>/,
|
|
# flip the /opt/furtka/current symlink, drop the `furtka` wrapper, and
|
|
# systemctl-link every Furtka unit file.
|
|
import app as app_module
|
|
|
|
fake_payload = tmp_path / "furtka-resource-manager.tar.gz"
|
|
fake_payload.write_bytes(b"\x1f\x8b\x08\x00fake-tarball-bytes")
|
|
monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", fake_payload)
|
|
monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d})
|
|
|
|
cfg = build_archinstall_config(
|
|
{
|
|
"hostname": "heimserver",
|
|
"username": "u",
|
|
"password": "pw12345678",
|
|
"language": "en",
|
|
"boot_drive": "/dev/sda",
|
|
}
|
|
)
|
|
joined = "\n".join(cfg["custom_commands"])
|
|
|
|
# Tarball lands in the versioned slot, not flat /opt/furtka/.
|
|
assert 'tar -xzf - -C "$staging"' in joined
|
|
assert "/opt/furtka/versions/$ver" in joined
|
|
assert 'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current' in joined
|
|
# CLI wrapper lands and references /opt/furtka/current. The wrapper body
|
|
# rides the base64 payload, so assert on the constant directly.
|
|
assert "/usr/local/bin/furtka" in joined
|
|
assert "PYTHONPATH=/opt/furtka/current" in app_module._FURTKA_WRAPPER_SH
|
|
# Every unit is linked + enabled.
|
|
for unit in app_module._FURTKA_UNITS:
|
|
assert f"/opt/furtka/current/assets/systemd/{unit}" in joined
|
|
assert unit in joined
|
|
# python is pacstrapped so the wrapper has an interpreter.
|
|
assert "python" in cfg["packages"]
|
|
|
|
|
|
def test_resource_manager_absent_without_payload(monkeypatch, tmp_path):
|
|
# Dev box / CI without an ISO build: payload doesn't exist. No tarball
|
|
# extract, no unit link, no enable — and no stale references to furtka-*
|
|
# units in custom_commands.
|
|
import app as app_module
|
|
|
|
monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", tmp_path / "does-not-exist.tar.gz")
|
|
monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d})
|
|
|
|
cfg = build_archinstall_config(
|
|
{
|
|
"hostname": "heimserver",
|
|
"username": "u",
|
|
"password": "pw12345678",
|
|
"language": "en",
|
|
"boot_drive": "/dev/sda",
|
|
}
|
|
)
|
|
joined = "\n".join(cfg["custom_commands"])
|
|
|
|
assert "tar -xzf" not in joined
|
|
assert "systemctl link" not in joined
|
|
# The base system bootstrap (caddy, status.json placeholder, furtka.json)
|
|
# is unaffected.
|
|
assert "/etc/caddy/Caddyfile" in joined
|
|
assert "/var/lib/furtka/furtka.json" in joined
|
|
|
|
|
|
def test_build_archinstall_creds_uses_archinstall_sentinel_keys():
|
|
creds = build_archinstall_creds({"username": "u", "password": "pw12345678"})
|
|
assert creds["!root-password"] == "pw12345678"
|
|
assert creds["users"] == [
|
|
{
|
|
"username": "u",
|
|
"!password": "pw12345678",
|
|
"sudo": True,
|
|
"groups": [],
|
|
}
|
|
]
|