From 453c927acea248b3a261936c3befa1abb659956d Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sat, 3 Oct 2026 16:14:43 -0700 Subject: [PATCH] feat(qnap-lcd): Proxmox-correct front-panel LCD menu for esh-nas-pve; albok redeploy note --- playbooks/qnap-lcd.yaml | 42 ++++++++ services/qnap-lcd/lcd-menu.py | 162 +++++++++++++++++++++++++++++ services/qnap-lcd/lcd-menu.service | 19 ++++ stacks/albok-service/README.md | 5 + 4 files changed, 228 insertions(+) create mode 100644 playbooks/qnap-lcd.yaml create mode 100644 services/qnap-lcd/lcd-menu.py create mode 100644 services/qnap-lcd/lcd-menu.service diff --git a/playbooks/qnap-lcd.yaml b/playbooks/qnap-lcd.yaml new file mode 100644 index 0000000..09588c7 --- /dev/null +++ b/playbooks/qnap-lcd.yaml @@ -0,0 +1,42 @@ +# Install the Proxmox-correct QNAP front-panel LCD menu on esh-nas-pve (Prime 2026-10-03: "it's running but not +# correctly, can you fix?"). Idempotent. Originals kept beside: lcd-menu.py.upstream, /root/lcd-menu.service.orig. +# scripts/elway infra-ops@10.0.50.55 --playbook playbooks/qnap-lcd.yaml +steps: + - name: keep the upstream menu script and unit + sudo: true + shell: | + set -e + cp -p /usr/local/bin/qnap_lcd/lcd-menu.py /usr/local/bin/qnap_lcd/lcd-menu.py.upstream + cp -p /etc/systemd/system/lcd-menu.service /root/lcd-menu.service.orig + creates: /usr/local/bin/qnap_lcd/lcd-menu.py.upstream + - name: menu script + sudo: true + upload: + src: services/qnap-lcd/lcd-menu.py + dest: /usr/local/bin/qnap_lcd/lcd-menu.py + mode: "0755" + - name: dry-run every screen before touching the panel + sudo: true + shell: python3 /usr/local/bin/qnap_lcd/lcd-menu.py --dry-run + changed_when: "false" + - name: service unit + sudo: true + upload: + src: services/qnap-lcd/lcd-menu.service + dest: /etc/systemd/system/lcd-menu.service + mode: "0644" + - name: re-enable (WantedBy moved to multi-user.target) and restart + sudo: true + shell: | + set -e + systemctl disable lcd-menu.service 2>/dev/null || true + systemctl daemon-reload + systemctl enable lcd-menu.service + systemctl restart lcd-menu.service +verify: + - name: running, enabled, and no screen errors since the restart + sudo: true + shell: | + sleep 8 + systemctl is-active --quiet lcd-menu.service && systemctl is-enabled --quiet lcd-menu.service && ! journalctl -u lcd-menu.service --since "-1min" --no-pager -q | grep -E "screen error|failed|Traceback" + changed_when: "false" diff --git a/services/qnap-lcd/lcd-menu.py b/services/qnap-lcd/lcd-menu.py new file mode 100644 index 0000000..3dbeb51 --- /dev/null +++ b/services/qnap-lcd/lcd-menu.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# QNAP front-panel LCD menu for esh-nas-pve (QNAP hardware running Proxmox VE, not QTS). +# +# Replaces the menu script of DigitalFunk/QnapLCD-Proxmox (checked out at /usr/local/bin/qnap_lcd, commit f2c9c46), +# whose "Proxmox" fork still opened on the TrueNAS screen: menu[0] ran `cli -c 'system version'`, a TrueNAS-only +# command, so the script died on its first screen and the panel stayed on "Initializing..." with dead buttons. +# The same script also hid the management address: it skipped every bridge, and on Proxmox the IP lives on vmbr0. +# Uses the repo's own `qnaplcd` driver unchanged (A125 display, 2x16, /dev/ttyS1 @ 1200 baud). +# Source of truth: eshpfi services/qnap-lcd/ (deployed by playbooks/qnap-lcd.yaml). Test without the panel: +# python3 lcd-menu.py --dry-run (prints every screen instead of writing to the LCD) +import json +import os +import platform +import subprocess +import sys +import threading +import time + +sys.stdout.reconfigure(line_buffering=True) + +DISPLAY_TIMEOUT = 30 # seconds the backlight stays on after a button press +REFRESH = 30 # seconds between redraws of the current screen +PORT, PORT_SPEED = '/dev/ttyS1', 1200 +WIDTH = 16 + + +def shell(cmd): + return subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.DEVNULL).strip() + + +def fit(text): + return str(text)[:WIDTH] + + +# ---------- screens: each returns two lines ---------- + +def host_ip(): + addrs = ipv4_addresses() + for name, ip in addrs: + if name == 'vmbr0': + return ip + return addrs[0][1] if addrs else 'no IPv4' + + +def screen_host(): + return [platform.node(), host_ip()] + + +def screen_pve(): + v = shell('pveversion') # pve-manager/9.2.21/4f6e0ac8 (running kernel: 7.0.14-20-pve) + ver = v.split('/')[1] if '/' in v else v + return [f'PVE {ver}', platform.release()] # 'Proxmox VE 9.2.21' is 17 chars, one too many for the panel + + +def screen_uptime(): + secs = int(float(open('/proc/uptime').read().split()[0])) + d, rem = divmod(secs, 86400) + h, rem = divmod(rem, 3600) + m = rem // 60 + up = f'{d}d {h:02d}:{m:02d}' if d else f'{h:02d}:{m:02d}' + load = os.getloadavg() + return [f'Up {up}', f'Ld {load[0]:.2f} {load[1]:.2f}'] + + +def ipv4_addresses(): + out = [] + for iface in json.loads(shell('ip -json address show')): + name = iface.get('ifname', '') + if name == 'lo' or name.startswith(('veth', 'tap', 'fwbr', 'fwpr', 'fwln')): + continue + for a in iface.get('addr_info', []): + if a.get('family') == 'inet': + out.append((name, a['local'])) + break + return out + + +def pools(): + rows = shell('zpool list -H -o name,size,alloc,cap,health').splitlines() + return [r.split('\t') for r in rows if r.strip()] + + +def build_menu(): + menu = [screen_host, screen_pve, screen_uptime] + for name, ip in ipv4_addresses(): + menu.append(lambda n=name, i=ip: [n, i]) + for name, size, alloc, cap, health in pools(): + menu.append(lambda n=name, s=size, a=alloc, c=cap, h=health: [f'{n} {h}', f'{a}/{s} {c}']) + return menu + + +# ---------- driver glue ---------- + +lcd = None +lcd_timer = None +menu = [screen_host] +menu_item = 0 +lock = threading.Lock() + + +def render(item): + try: + lines = menu[item]() + except Exception as e: # one broken screen must never kill the menu + lines = ['screen error', type(e).__name__] + print(f'screen {item} failed: {e!r}') + lines = [fit(l) for l in lines][:2] + if lcd is None: + print(f'[{item}] {lines[0]!r} | {lines[1] if len(lines) > 1 else ""!r}') + return + lcd.clear() + lcd.write(0, lines) + + +def lcd_on(): + global lcd_timer + lcd.backlight(True) + if lcd_timer: + lcd_timer.cancel() + lcd_timer = threading.Timer(DISPLAY_TIMEOUT, lambda: lcd.backlight(False)) + lcd_timer.daemon = True + lcd_timer.start() + + +def response_handler(command, data): + global menu_item + if command != 'Switch_Status': + return + with lock: + lcd_on() + if data == 0x01: # up + menu_item = (menu_item - 1) % len(menu) + elif data == 0x02: # down + menu_item = (menu_item + 1) % len(menu) + else: + return + render(menu_item) + + +def main(): + global lcd, menu, menu_item + if '--dry-run' in sys.argv: + menu = build_menu() + for i in range(len(menu)): + render(i) + return + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import qnaplcd + lcd = qnaplcd.QnapLCD(PORT, PORT_SPEED, response_handler) + lcd.reset() + lcd.clear() + lcd_on() + while True: + with lock: + menu = build_menu() + menu_item %= len(menu) + render(menu_item) + time.sleep(REFRESH) + + +if __name__ == '__main__': + main() diff --git a/services/qnap-lcd/lcd-menu.service b/services/qnap-lcd/lcd-menu.service new file mode 100644 index 0000000..e46fa0c --- /dev/null +++ b/services/qnap-lcd/lcd-menu.service @@ -0,0 +1,19 @@ +# QNAP front-panel LCD menu on esh-nas-pve. Source: eshpfi services/qnap-lcd/ (playbooks/qnap-lcd.yaml). +# Replaces the repo's unit, which ran a shell wrapper with no restart: when the menu died (it always did on +# Proxmox, see lcd-menu.py), nothing brought it back and the panel froze on "Initializing...". +[Unit] +Description=QNAP front-panel LCD menu +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/usr/local/bin/qnap_lcd +ExecStartPre=/usr/bin/python3 /usr/local/bin/qnap_lcd/preinit.py +ExecStart=/usr/bin/python3 /usr/local/bin/qnap_lcd/lcd-menu.py +ExecStopPost=/usr/bin/python3 /usr/local/bin/qnap_lcd/shutdown.py +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/stacks/albok-service/README.md b/stacks/albok-service/README.md index f34e259..ce09725 100644 --- a/stacks/albok-service/README.md +++ b/stacks/albok-service/README.md @@ -57,6 +57,11 @@ so `/srv/albok` is covered whole-VM. No file-level restic job for it. ## Deploy / redeploy +⚠ **Do not restart the service in the middle of a Nemi walk.** The hourly walk on nh3-dev (`services/albok-nemi/`) fails with +`Unreachable` and mails OnFailure; that happened 2026-10-03 16:11 at the 0.1.4 redeploy. It is harmless, since the next walk redoes the +work and the service commits per batch, but avoidable. Check first, on nh3-dev as lkraven: +`flock -n ~/.local/state/albok-nemi/run.lock true && echo idle || echo WALK RUNNING` + ```bash # 1. host prep (idempotent) scripts/elway infra-ops@10.100.50.40 --playbook playbooks/albok-service-host.yaml