7 Commits
Author SHA1 Message Date
claude-botandClaude Opus 5 2d9c3ba213 refactor(edge): split the edge-host role into one role per concern
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
edge-host was a single role doing eight unrelated jobs: flash-wear
mitigation, log shipping, VLAN networking, an MQTT bridge, a CAN interface,
the heating CAN services, a Z-Wave gateway and a power-meter reader.

That is not just untidy. It is what blocked a second thin client from
joining as an NTP peer: the host-generic work (journald to RAM, Alloy, the
VLAN sub-interface) could not be applied without also handing the box a
Z-Wave radio and a CAN bus it does not have. The role bound its services to
specific USB device paths, so a second host would have tried to start
containers for hardware that is not there.

Now:

  edge-flash-wear   journald -> RAM, noatime/lazytime, TRIM, eMMC health
  edge-log-shipper  Alloy: journal -> Loki
  edge-vlan         tagged VLAN sub-interface
  mqtt-bridge       loopback mosquitto -> cluster broker over TLS
  socketcan         can0 only
  heating-can       open3e (Vitocal/VX3) + e3oncan (E380 meter)
  zwave-gateway     zwave-js-ui, own compose project
  power-meter-sml   sml2mqtt, own compose project

The E380 reader lives with open3e rather than in its own role: it is part
of the same heating installation -- it feeds the heat pump's own energy
management -- not a separate system that happens to share a bus. can0 does
stay separate, because an interface is not the same concern as the services
consuming it.

Each role installs its own packages, so it stands alone. An edge host with
no peripherals takes the first five and none of the rest.

docker-daemon.json moves to docker-install with its own docker_log_driver
variable and Restart docker handler, since configuring the docker daemon was
never an edge-host concern. Handlers split ten ways; defaults split per
role; all sixteen templates and files moved with git mv.

Two deliberate behaviour changes on the node, both accepted:

1. Compose split. zwave-js and sml2mqtt shared a compose file, so touching
   either recreated both. They are now separate projects in their own
   directories. container_name is fixed, so the old project must release the
   names first -- a guarded one-time task downs it and removes the file.
   First apply costs a brief down/up on both. Data paths are unchanged; the
   zwave store is not migrated.

2. Header repointing. Templates and scripts carried "Managed by
   roles/edge-host" headers, which render onto the host -- they would have
   sent an operator to a role that no longer exists. Updating them changes
   file content, so it restarts the services that consume those files.

Verified by --check --diff against the live host. 24 changed items, and
every one is accounted for:

    5  pre-existing, unchanged from the baseline before this work:
       common timesyncd; open3e venv; E3onCAN venv; and the open3e /
       e3oncan restart handlers. The venv tasks install from a git URL,
       which pip cannot verify, so they report changed on every run --
       a separate defect, not introduced here.
    5  the compose split: zwave env + compose and sml compose to their
       new paths, plus the two legacy-project retirement tasks.
   14  the header repointing: journald conf, eMMC health script, alloy
       config, mosquitto bridge config, 80-can0.network, open3e overlay /
       env / unit, e3oncan unit, zwave backup script + env, sml2mqtt
       config -- and the resulting restarts of journald, alloy, mosquitto,
       open3e and e3oncan.

Nothing changed outside those three groups.

One consequence check mode cannot show: 80-can0.network.j2 changed, so the
"Reload networkd" handler fires on a real run and reconfigures can0,
briefly dropping the bus under open3e and e3oncan. It is absent from the
check-mode output only because shell handlers are skipped there. The VLAN
templates did NOT change, so the riskier reconfigure of the parent uplink
on this headless host is not triggered.

Also fixed in passing: the keepalived-ntp guard told operators to recover
with `--limit <host> --tags vlan40`, and referred to the edge-host role.
Both corrected. The role README becomes ansible/docs/edge-host.md -- it
documents the host, not any single role now.

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:33:57 +02:00
claude-botandClaude Opus 5 07faf0ae69 fix(edge-host-iso): carry the bootstrap fixes into the ISO side too
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Review on #474 caught that the previous commit fixed the procedure in
ansible/roles/edge-host/README.md and left the same two defects in the two
places an operator actually reads them first.

That happened because my grep for the stale patterns ran with the working
directory set to ansible/, so `images/edge-host-iso/README.md` was a path
that did not exist. grep returned nothing and I read that as clean. A false
negative from a bad path, not a clean sweep.

Fixed now, verified from the repository root:

  airootfs/usr/local/bin/edge-install
    Printed the superseded two-command sequence at the console immediately
    after imaging -- the very first instructions anyone sees, telling them
    to run common.yml with -l edge_host. Now prints the single unrestricted
    run, plus the two steps that precede it (confirm the host resolves by
    name, add it to [edge_host]).

  README.md
    Described the image as being for `edge-1.srv.renner.uno`, a name that
    does not resolve, and repeated the same two-command bootstrap. Both
    replaced. This file is the link target of step 2 in the ansible-side
    README, so following the procedure end to end landed straight back on
    the defect #474 claims to close.

Also corrected while there: the ISO README called the target "the bare-metal
N100". edge-1 is a Wyse 3040 (Atom x5-Z8350), and the installer text further
down already says it "covers the Wyse 3040". Rewritten to describe the image
as not tied to one box or model, since the installer autodetects the target
disk -- which is what makes it usable for the second thin client.

Also from review: a comment on why `become: true` is set when the connection
is already root. Not taken -- ignore_unreachable, which common.yml sets. The
review's own reasoning applies: Ansible already skips an unreachable host and
continues with the others, so it adds nothing here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 08:21:27 +02:00
k.rennerandClaude Fable 5 244929fff2 edge-host-iso: pin mmc modules into the installed initramfs
mkinitcpio's autodetect missed the eMMC stack in the chroot; the
installed system hung unbootable with root on mmcblk0p2. Pin
sdhci_acpi/sdhci_pci/mmc_block in MODULES and regenerate during install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:48:19 +02:00
k.rennerandClaude Fable 5 ed80d83a73 edge-host-iso: fix eMMC detection on the Wyse 3040
The live env detects the eMMC card (sdhci_acpi) but does not autoload
mmc_block, so /dev/mmcblk0 never appears and edge-install finds no
target. Autoload via modules-load.d, belt-and-braces modprobe in
edge-install, check mmcblk0-2, and fix the lsblk fallback (empty TRAN
column shifted the awk fields).

Diagnosed live over SSH on the actual 3040 (10.2.50.109).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:36:08 +02:00
k.rennerandClaude Fable 5 81dc3d0a85 edge-host-iso: headless install support + internal-disk autodetect
Target changed from N100 to a Wyse 3040 (2G RAM / 8G eMMC): default
disk is now autodetected (nvme, else mmcblk0, else first non-USB disk)
and the live environment carries the homelab SSH keys, so the install
runs over SSH without a monitor (firmware boot-order/Secure-Boot still
needs one console visit if not already set).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:42:40 +02:00
k.rennerandClaude Fable 5 dbe8efdc62 images: build edge-host ISO in an archlinux container
mkarchiso against the host repos fails on Manjaro (no archinstall/
reflector, linux is a virtual package) — releng needs real Arch repos.
Split the profile assembly into build-inner.sh and run it in
archlinux:latest (--privileged for pacstrap's bind mounts); verified
build on the Manjaro workstation. gitignore the ISO output dir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:34:13 +02:00
k.rennerandClaude Fable 5 ee98c88ef4 images: add edge-host installer ISO
Releng-based archiso overlay: `edge-install` in the live env wipes the
N100 NVMe and installs the ansible-ready base system (systemd-boot,
networkd DHCP, sshd key-only, docker). Service config stays in the
edge-host ansible role; the ISO only produces the base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:14:36 +02:00