feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 18:29:42 +02:00
|
|
|
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})
|
|
|
|
|
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
cfg = build_archinstall_config(
|
|
|
|
|
{
|
|
|
|
|
"hostname": "h",
|
|
|
|
|
"username": "u",
|
|
|
|
|
"password": "pw12345678",
|
|
|
|
|
"language": "pl",
|
|
|
|
|
"boot_drive": "/dev/sda",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-04-14 18:29:42 +02:00
|
|
|
assert cfg["disk_config"] == {"stubbed_device": "/dev/sda"}
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
assert cfg["hostname"] == "h"
|
|
|
|
|
assert cfg["locale_config"]["locale"] == "pl_PL.UTF-8"
|
2026-04-14 18:29:42 +02:00
|
|
|
# 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
|
2026-04-14 19:51:50 +02:00
|
|
|
assert cfg["custom_commands"][0] == "gpasswd -a u docker"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_archinstall_config_includes_post_install_bootstrap(monkeypatch):
|
|
|
|
|
# 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 + index.html written into the
|
|
|
|
|
# target rootfs, and nss-mdns spliced into nsswitch.conf.
|
|
|
|
|
import app as app_module
|
|
|
|
|
|
|
|
|
|
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"]
|
2026-04-14 20:34:34 +02:00
|
|
|
# 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"):
|
2026-04-14 19:51:50 +02:00
|
|
|
assert svc in cfg["services"]
|
2026-04-14 20:34:34 +02:00
|
|
|
assert "furtka-welcome" not in cfg["services"]
|
|
|
|
|
assert "furtka-status.timer" not in cfg["services"]
|
2026-04-14 19:51:50 +02:00
|
|
|
|
|
|
|
|
joined = "\n".join(cfg["custom_commands"])
|
2026-04-14 20:34:34 +02:00
|
|
|
assert "systemctl enable furtka-welcome.service furtka-status.timer" in joined
|
2026-04-14 19:51:50 +02:00
|
|
|
for path in (
|
|
|
|
|
"/etc/caddy/Caddyfile",
|
|
|
|
|
"/srv/furtka/www/index.html",
|
|
|
|
|
"/srv/furtka/www/style.css",
|
|
|
|
|
"/srv/furtka/www/status.json",
|
|
|
|
|
"/usr/local/bin/furtka-status",
|
|
|
|
|
"/usr/local/bin/furtka-welcome",
|
|
|
|
|
"/etc/systemd/system/furtka-status.service",
|
|
|
|
|
"/etc/systemd/system/furtka-status.timer",
|
|
|
|
|
"/etc/systemd/system/furtka-welcome.service",
|
|
|
|
|
):
|
|
|
|
|
assert path in joined, f"expected {path} to be written by custom_commands"
|
|
|
|
|
|
|
|
|
|
assert "mdns_minimal" in joined
|
|
|
|
|
assert "nsswitch.conf" in joined
|
|
|
|
|
# The chosen hostname is pinned into the static HTML at install-time.
|
|
|
|
|
assert "s/__HOSTNAME__/heimserver/g" in joined
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
|
|
|
|
|
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
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, drop the `furtka` wrapper, and
|
|
|
|
|
# write the reconcile systemd unit. Without the tarball the install still
|
|
|
|
|
# succeeds — the resource manager is just absent (covered implicitly by
|
|
|
|
|
# the default test environment, which has no payload).
|
|
|
|
|
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 expansion happens.
|
|
|
|
|
assert "tar -xzf - -C /opt/furtka" in joined
|
|
|
|
|
# `furtka` CLI wrapper lands on the target.
|
|
|
|
|
assert "/usr/local/bin/furtka" in joined
|
|
|
|
|
# systemd unit is written and conditionally enabled.
|
|
|
|
|
assert "/etc/systemd/system/furtka-reconcile.service" in joined
|
|
|
|
|
assert "furtka-reconcile.service" 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. We should NOT
|
|
|
|
|
# emit untar / wrapper / unit commands, but the rest of post-install must
|
|
|
|
|
# still be generated normally.
|
|
|
|
|
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 - -C /opt/furtka" not in joined
|
|
|
|
|
assert "furtka-reconcile.service" in joined # still in the conditional enable line
|
|
|
|
|
# The base system bootstrap (caddy etc) is unaffected.
|
|
|
|
|
assert "/etc/caddy/Caddyfile" in joined
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 18:29:42 +02:00
|
|
|
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": [],
|
|
|
|
|
}
|
|
|
|
|
]
|