feat(qnap-lcd): Proxmox-correct front-panel LCD menu for esh-nas-pve; albok redeploy note
This commit is contained in:
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user