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]
|
||||
|
||||
### 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
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -1,22 +1,36 @@
|
|||
#!/bin/bash
|
||||
# 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.
|
||||
set -e
|
||||
# user which URL to open. Mirrors the live-ISO furtka-update-issue pattern:
|
||||
# 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)
|
||||
ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
||||
issue=/etc/issue
|
||||
|
||||
{
|
||||
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 " Furtka is ready."
|
||||
echo
|
||||
echo " Open in a browser on another device on your network:"
|
||||
echo
|
||||
echo " http://${hostname}.local (easy — try this first)"
|
||||
if [ -n "$ip" ]; then
|
||||
echo " http://${ip} (fallback if the first doesn't work)"
|
||||
if ((${#ips[@]})); then
|
||||
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
|
||||
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]
|
||||
Description=Furtka console welcome banner
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
# No network-online dependency on purpose: paint the banner (with a
|
||||
# "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]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/furtka/current/assets/bin/furtka-welcome
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
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/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/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/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/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` (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.
|
||||
|
||||
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
|
||||
|
||||
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/"
|
||||
|
||||
# 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)"
|
||||
# releng ships menu entries labelled "Arch Linux install medium" across three
|
||||
# bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
Furtka Live Installer starting…
|
||||
|
||||
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]
|
||||
Description=Write Furtka /etc/issue with current IP for the console welcome
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
# No network-online dependency on purpose: the first run should paint the
|
||||
# banner (with a "no IP yet" fallback) as early as possible; the timer
|
||||
# re-runs it until an address shows up.
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/furtka-update-issue
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
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
|
||||
# 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
|
||||
# doesn't work. Reload at the end nudges agetty to redraw.
|
||||
set -e
|
||||
# doesn't work.
|
||||
#
|
||||
# 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 " Open Furtka in a browser on another device on your network:"
|
||||
echo
|
||||
echo " http://proksi.local:5000 (easy — try this first)"
|
||||
if [ -n "$ip" ]; then
|
||||
echo " http://${ip}:5000 (fallback if the first doesn't work)"
|
||||
if ((${#ips[@]})); then
|
||||
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
|
||||
echo
|
||||
echo " Then follow the wizard to install Furtka on this machine."
|
||||
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():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
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
|
||||
of the install still works — the resource manager just won't be installed,
|
||||
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():
|
||||
print(
|
||||
|
|
@ -478,7 +481,7 @@ def build_archinstall_config(s):
|
|||
"docker",
|
||||
# Base OS post-install services. Only packaged units go here —
|
||||
# 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.
|
||||
"caddy",
|
||||
"avahi-daemon",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue