fix(console): always show the IP fallback on the welcome banner
The /etc/issue banner on both the live ISO and the installed system was a oneshot after network-online.target that simply dropped the http://<ip> line when DHCP hadn't delivered yet — so a slow NIC (first hardware bench, 2026-08-24) left the screen offering only proksi.local. On the ISO, releng's root autologin on tty1 also made agetty --reload a no-op. - furtka-issue.timer (ISO) / furtka-welcome.timer (target) re-run the banner script every 5 s; services drop RemainAfterExit + network wait - fallback line is always present: every global IPv4, or a 'no IP yet — check cable / DHCP' hint; only rewrites + reloads on change - build.sh removes releng's getty@tty1 autologin drop-in; banner tells users a shell is root + Enter away Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
This commit is contained in:
parent
e1968facef
commit
0e05bef667
13 changed files with 133 additions and 27 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -7,6 +7,25 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Console banner always shows the IP fallback.** On both the live ISO and
|
||||||
|
the installed system the `/etc/issue` welcome was written once, after
|
||||||
|
`network-online.target`, and simply omitted the `http://<ip>` line when no
|
||||||
|
address existed yet — so on a slow-DHCP NIC (first hardware bench,
|
||||||
|
2026-08-24) the screen only ever offered `proksi.local`. The banner script
|
||||||
|
now runs from a timer (`furtka-issue.timer` on the ISO,
|
||||||
|
`furtka-welcome.timer` on the installed box, every 5 s) and always prints
|
||||||
|
a fallback line: every global IPv4 address once there is one, or a
|
||||||
|
"no IP address yet — check the cable / DHCP" hint until then. It only
|
||||||
|
rewrites `/etc/issue` and nudges agetty when the text changed, so the
|
||||||
|
login prompt doesn't flicker. The live ISO no longer auto-logs `root` in
|
||||||
|
on tty1 (releng default) — that handed the console to a shell before the
|
||||||
|
banner could update; tty1 now stays at the login prompt with a live
|
||||||
|
banner, and the banner tells you a shell is `root` + Enter away. Boxes
|
||||||
|
that self-update get the new timer linked + enabled automatically; the
|
||||||
|
refresh loop takes effect after their next reboot.
|
||||||
|
|
||||||
## [26.19-alpha] - 2026-08-23
|
## [26.19-alpha] - 2026-08-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Regenerates /etc/issue on the installed system so the console tells the
|
# Regenerates /etc/issue on the installed system so the console tells the
|
||||||
# user which URL to open. Mirrors the live-ISO furtka-update-issue pattern.
|
# user which URL to open. Mirrors the live-ISO furtka-update-issue pattern:
|
||||||
set -e
|
# runs at boot and every few seconds from furtka-welcome.timer, so the IP
|
||||||
|
# fallback line is always on screen — with the address once DHCP handed one
|
||||||
|
# out, or with a "no IP yet" hint before that / after the cable is pulled.
|
||||||
|
# Only rewrites + nudges agetty when the text changed (no prompt flicker).
|
||||||
|
set -u
|
||||||
|
|
||||||
hostname=$(cat /etc/hostname)
|
issue=/etc/issue
|
||||||
ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
|
||||||
|
|
||||||
{
|
hostname=$(cat /etc/hostname 2>/dev/null || hostname)
|
||||||
|
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
|
||||||
|
|
||||||
|
new=$(
|
||||||
echo
|
echo
|
||||||
echo " Furtka is ready."
|
echo " Furtka is ready."
|
||||||
echo
|
echo
|
||||||
echo " Open in a browser on another device on your network:"
|
echo " Open in a browser on another device on your network:"
|
||||||
echo
|
echo
|
||||||
echo " http://${hostname}.local (easy — try this first)"
|
echo " http://${hostname}.local (easy — try this first)"
|
||||||
if [ -n "$ip" ]; then
|
if ((${#ips[@]})); then
|
||||||
echo " http://${ip} (fallback if the first doesn't work)"
|
for ip in "${ips[@]}"; do
|
||||||
|
echo " http://${ip} (fallback if the first doesn't work)"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " (fallback: no IP address yet — waiting for the network."
|
||||||
|
echo " Check the cable / DHCP. This line updates by itself.)"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
} > /etc/issue
|
)
|
||||||
|
|
||||||
agetty --reload 2>/dev/null || true
|
if [ "$new" != "$(cat "$issue" 2>/dev/null)" ]; then
|
||||||
|
printf '%s\n' "$new" > "$issue"
|
||||||
|
agetty --reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Furtka console welcome banner
|
Description=Furtka console welcome banner
|
||||||
After=network-online.target
|
# No network-online dependency on purpose: paint the banner (with a
|
||||||
Wants=network-online.target
|
# "no IP yet" fallback) as early as possible; furtka-welcome.timer re-runs
|
||||||
|
# it every few seconds so the address appears as soon as DHCP delivers.
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/opt/furtka/current/assets/bin/furtka-welcome
|
ExecStart=/opt/furtka/current/assets/bin/furtka-welcome
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
10
assets/systemd/furtka-welcome.timer
Normal file
10
assets/systemd/furtka-welcome.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Keep the Furtka console welcome (IP fallback) up to date
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=3s
|
||||||
|
OnUnitActiveSec=5s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -27,13 +27,15 @@ The build starts from Arch's stock `releng` profile (the same one used to build
|
||||||
| `overlay/airootfs/opt/furtka/` | Directory where `webinstaller/` is copied at build time |
|
| `overlay/airootfs/opt/furtka/` | Directory where `webinstaller/` is copied at build time |
|
||||||
| `overlay/airootfs/etc/hostname` | Live-ISO hostname (`proksi`) so mDNS advertises the installer as `proksi.local` |
|
| `overlay/airootfs/etc/hostname` | Live-ISO hostname (`proksi`) so mDNS advertises the installer as `proksi.local` |
|
||||||
| `overlay/airootfs/etc/issue` | Welcome banner on the TTY pointing users at `http://proksi.local:5000` |
|
| `overlay/airootfs/etc/issue` | Welcome banner on the TTY pointing users at `http://proksi.local:5000` |
|
||||||
| `overlay/airootfs/usr/local/bin/furtka-update-issue` | Rewrites `/etc/issue` at runtime so the banner also shows the DHCP-assigned IP as a fallback URL |
|
| `overlay/airootfs/usr/local/bin/furtka-update-issue` | Rewrites `/etc/issue` at runtime so the banner always carries a fallback line: the DHCP-assigned IP(s), or a "no IP yet — check cable / DHCP" hint until one arrives. Only touches the file (and `agetty --reload`s) when the text changed |
|
||||||
| `overlay/airootfs/etc/systemd/system/` | `furtka-webinstaller.service` (Flask on :5000) + `furtka-issue.service` (runs the banner-updater on network-online), each symlinked into `multi-user.target.wants/` to auto-start on boot |
|
| `overlay/airootfs/etc/systemd/system/` | `furtka-webinstaller.service` (Flask on :5000) + `furtka-issue.service` (banner-updater, first run at boot) in `multi-user.target.wants/`, and `furtka-issue.timer` (re-runs the updater every 5 s) in `timers.target.wants/` |
|
||||||
|
|
||||||
The systemd service runs `flask --app app run --host 0.0.0.0 --port 5000` under `/opt/furtka`. The `0.0.0.0` binding is important — the Flask default is localhost-only, which wouldn't be reachable from another machine on the LAN.
|
The systemd service runs `flask --app app run --host 0.0.0.0 --port 5000` under `/opt/furtka`. The `0.0.0.0` binding is important — the Flask default is localhost-only, which wouldn't be reachable from another machine on the LAN.
|
||||||
|
|
||||||
mDNS is wired: `avahi-daemon` + `nss-mdns` come from `packages.extra`, the live ISO's hostname is `proksi`, and as soon as `systemd-networkd-wait-online` fires the installer is reachable at `http://proksi.local:5000`. The raw IP still shows on the console for fallback — some Windows clients need the Bonjour service for `.local` to resolve at all.
|
mDNS is wired: `avahi-daemon` + `nss-mdns` come from `packages.extra`, the live ISO's hostname is `proksi`, and as soon as `systemd-networkd-wait-online` fires the installer is reachable at `http://proksi.local:5000`. The raw IP still shows on the console for fallback — some Windows clients need the Bonjour service for `.local` to resolve at all.
|
||||||
|
|
||||||
|
`build.sh` also deletes releng's `getty@tty1.service.d/autologin.conf`. With root auto-logged-in, tty1 belongs to a shell and `agetty --reload` can't redraw the banner, so the IP line was a snapshot of whatever DHCP had done by the time getty started. Without autologin tty1 sits at the login prompt showing the live banner; for a shell type `root` + Enter (releng's root has no password) — the banner says so.
|
||||||
|
|
||||||
## Test flow
|
## Test flow
|
||||||
|
|
||||||
1. Build: `./iso/build.sh`
|
1. Build: `./iso/build.sh`
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,15 @@ cat "$SCRIPT_DIR/overlay/profiledef.sh" >> "$PROFILE_WORK/profiledef.sh"
|
||||||
|
|
||||||
cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
|
cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/"
|
||||||
|
|
||||||
|
# releng auto-logs root in on tty1. That turns our /etc/issue banner into a
|
||||||
|
# one-shot snapshot: once agetty has handed tty1 to a shell, `agetty --reload`
|
||||||
|
# from furtka-update-issue has nothing left to redraw, so the IP fallback only
|
||||||
|
# shows if DHCP happened to beat getty. Drop the drop-in: tty1 stays at the
|
||||||
|
# login prompt with a live-refreshed banner (furtka-issue.timer), and a shell
|
||||||
|
# is one `root` + Enter away (releng's root has no password) — the banner says
|
||||||
|
# so.
|
||||||
|
rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d"
|
||||||
|
|
||||||
echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)"
|
echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)"
|
||||||
# releng ships menu entries labelled "Arch Linux install medium" across three
|
# releng ships menu entries labelled "Arch Linux install medium" across three
|
||||||
# bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to
|
# bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@
|
||||||
Furtka Live Installer starting…
|
Furtka Live Installer starting…
|
||||||
|
|
||||||
Once ready, open http://proksi.local:5000 on another device
|
Once ready, open http://proksi.local:5000 on another device
|
||||||
on your network. The exact URL will appear below.
|
on your network. The exact URL (and an IP fallback) will
|
||||||
|
appear here in a few seconds.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Write Furtka /etc/issue with current IP for the console welcome
|
Description=Write Furtka /etc/issue with current IP for the console welcome
|
||||||
After=network-online.target
|
# No network-online dependency on purpose: the first run should paint the
|
||||||
Wants=network-online.target
|
# banner (with a "no IP yet" fallback) as early as possible; the timer
|
||||||
|
# re-runs it until an address shows up.
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/local/bin/furtka-update-issue
|
ExecStart=/usr/local/bin/furtka-update-issue
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
10
iso/overlay/airootfs/etc/systemd/system/furtka-issue.timer
Normal file
10
iso/overlay/airootfs/etc/systemd/system/furtka-issue.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Keep the Furtka console welcome (IP fallback) up to date
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=3s
|
||||||
|
OnUnitActiveSec=5s
|
||||||
|
AccuracySec=1s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../furtka-issue.timer
|
||||||
|
|
@ -2,22 +2,40 @@
|
||||||
# Regenerates /etc/issue so the live-ISO console tells the user which URL
|
# Regenerates /etc/issue so the live-ISO console tells the user which URL
|
||||||
# to open in their browser. Shows proksi.local (via avahi/mDNS) as the
|
# to open in their browser. Shows proksi.local (via avahi/mDNS) as the
|
||||||
# preferred URL and the raw IP as a fallback for networks where mDNS
|
# preferred URL and the raw IP as a fallback for networks where mDNS
|
||||||
# doesn't work. Reload at the end nudges agetty to redraw.
|
# doesn't work.
|
||||||
set -e
|
#
|
||||||
|
# Runs at boot and then every few seconds from furtka-issue.timer, so the
|
||||||
|
# fallback line is *always* on screen: with the address once DHCP has
|
||||||
|
# handed one out, or with a "no IP yet" hint before that / when the cable
|
||||||
|
# is unplugged. Only rewrites + nudges agetty when the text actually
|
||||||
|
# changed, so the login prompt doesn't flicker on every tick.
|
||||||
|
set -u
|
||||||
|
|
||||||
ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
issue=/etc/issue
|
||||||
|
|
||||||
{
|
mapfile -t ips < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
|
||||||
|
|
||||||
|
new=$(
|
||||||
echo
|
echo
|
||||||
echo " Open Furtka in a browser on another device on your network:"
|
echo " Open Furtka in a browser on another device on your network:"
|
||||||
echo
|
echo
|
||||||
echo " http://proksi.local:5000 (easy — try this first)"
|
echo " http://proksi.local:5000 (easy — try this first)"
|
||||||
if [ -n "$ip" ]; then
|
if ((${#ips[@]})); then
|
||||||
echo " http://${ip}:5000 (fallback if the first doesn't work)"
|
for ip in "${ips[@]}"; do
|
||||||
|
echo " http://${ip}:5000 (fallback if the first doesn't work)"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " (fallback: no IP address yet — waiting for the network."
|
||||||
|
echo " Check the cable / DHCP. This line updates by itself.)"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
echo " Then follow the wizard to install Furtka on this machine."
|
echo " Then follow the wizard to install Furtka on this machine."
|
||||||
echo
|
echo
|
||||||
} > /etc/issue
|
echo " Need a shell? Log in as root (no password)."
|
||||||
|
echo
|
||||||
|
)
|
||||||
|
|
||||||
agetty --reload 2>/dev/null || true
|
if [ "$new" != "$(cat "$issue" 2>/dev/null)" ]; then
|
||||||
|
printf '%s\n' "$new" > "$issue"
|
||||||
|
agetty --reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,25 @@ def test_systemd_units_reference_current_paths():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_welcome_banner_is_timer_driven():
|
||||||
|
"""The console banner must keep refreshing so the IP fallback is always
|
||||||
|
on screen. A oneshot with RemainAfterExit=yes never re-runs from a timer
|
||||||
|
(the unit stays 'active'), and a network-online dependency would delay the
|
||||||
|
first paint — both regressions the hardware bench hit in 26.19."""
|
||||||
|
timer = (ASSETS / "systemd" / "furtka-welcome.timer").read_text()
|
||||||
|
assert "OnUnitActiveSec=" in timer
|
||||||
|
assert "furtka-welcome.timer" in app._FURTKA_UNITS
|
||||||
|
service = (ASSETS / "systemd" / "furtka-welcome.service").read_text()
|
||||||
|
assert "RemainAfterExit" not in service
|
||||||
|
assert "network-online.target" not in service
|
||||||
|
|
||||||
|
|
||||||
|
def test_welcome_banner_always_prints_fallback_line():
|
||||||
|
script = (ASSETS / "bin" / "furtka-welcome").read_text()
|
||||||
|
assert "no IP address yet" in script
|
||||||
|
assert "fallback" in script
|
||||||
|
|
||||||
|
|
||||||
def test_read_asset_raises_for_missing_file():
|
def test_read_asset_raises_for_missing_file():
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
app._read_asset("does/not/exist.html")
|
app._read_asset("does/not/exist.html")
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,9 @@ def _resource_manager_commands():
|
||||||
file isn't present (dev box without an ISO build), returns [] so the rest
|
file isn't present (dev box without an ISO build), returns [] so the rest
|
||||||
of the install still works — the resource manager just won't be installed,
|
of the install still works — the resource manager just won't be installed,
|
||||||
and nothing else on the system references furtka-* units.
|
and nothing else on the system references furtka-* units.
|
||||||
|
# Re-runs the welcome banner every few seconds so the console always
|
||||||
|
# shows the current IP fallback (or a "no IP yet" hint).
|
||||||
|
"furtka-welcome.timer",
|
||||||
"""
|
"""
|
||||||
if not RESOURCE_MANAGER_PAYLOAD.exists():
|
if not RESOURCE_MANAGER_PAYLOAD.exists():
|
||||||
print(
|
print(
|
||||||
|
|
@ -478,7 +481,7 @@ def build_archinstall_config(s):
|
||||||
"docker",
|
"docker",
|
||||||
# Base OS post-install services. Only packaged units go here —
|
# Base OS post-install services. Only packaged units go here —
|
||||||
# archinstall runs `systemctl enable` on this list *before*
|
# archinstall runs `systemctl enable` on this list *before*
|
||||||
# custom_commands, so our own furtka-welcome + furtka-status.timer
|
# custom_commands, so our own furtka-welcome.timer + furtka-status.timer
|
||||||
# units (written in custom_commands) are enabled there instead.
|
# units (written in custom_commands) are enabled there instead.
|
||||||
"caddy",
|
"caddy",
|
||||||
"avahi-daemon",
|
"avahi-daemon",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue