feat(qnap-lcd): Proxmox-correct front-panel LCD menu for esh-nas-pve; albok redeploy note

This commit is contained in:
vh
2026-10-03 16:14:43 -07:00
parent f696aba10a
commit 453c927ace
4 changed files with 228 additions and 0 deletions
+42
View File
@@ -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"
+162
View File
@@ -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()
+19
View File
@@ -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
+5
View File
@@ -57,6 +57,11 @@ so `/srv/albok` is covered whole-VM. No file-level restic job for it.
## Deploy / redeploy ## 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 ```bash
# 1. host prep (idempotent) # 1. host prep (idempotent)
scripts/elway infra-ops@10.100.50.40 --playbook playbooks/albok-service-host.yaml scripts/elway infra-ops@10.100.50.40 --playbook playbooks/albok-service-host.yaml