Repository navigation
usb: a console helper for headless Linux hosts (script, helper disk, console test) - #76
Conversation
USB serial gave the managed computer a CDC ACM port, but setting up a login on it was left to the administrator: a udev rule and a getty by hand, with the docs admitting the Linux path was never tried. nanokvm-serial-console.sh (POSIX sh) runs on the managed host. plan and status only read: the distribution, the init system, the cdc_acm driver for the running kernel, the getty, and the NanoKVM on the USB bus by 3346:1009 with its serial number. apply, with a confirmation, writes a udev rule on systemd hosts that starts serial-getty@<port> for the NanoKVM's port only, bound to its serial number, with ModemManager told to ignore it; on sysvinit and busybox init it appends one inittab line. Every change is recorded with backups, a second apply changes nothing, and undo reverts. It refuses in containers, on immutable or read-only systems, without cdc_acm or a getty, and on init systems it does not handle, and prints the manual steps. The bootloader is out of scope. The NanoKVM serves the script at /nanokvm-serial-console.sh (public, the host has no credentials) and, with USB serial and the virtual disk enabled, S03usbdev attaches it as a read-only second LUN of the virtual drive: a 1440 KiB FAT12 image built reproducibly in Python, with the script as setup.sh and a readme. The USB settings page shows the fetch command with the serial number, the helper disk state, and a console test that sends Enter on ttyGS0 and reports the answer (login prompt, password, shell, text, silence, busy, unavailable). Tests: the script against fake roots under dash, busybox sh and bash (detection, every refusal, plan and status write nothing and run nothing, apply and undo, idempotence); the disk image (layout, mtools reads it, reproducible); the probe against a pty with a fake getty; the handlers. The docs now say the terminal's 9600 baud is nominal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The FAT boot sector carried the boot signature with no code after the BPB. A BIOS with a USB drive first in its boot order would run those zeros. Write the stub mkfs.fat uses: it prints that the disk is not bootable, waits for a key and goes on with the next device. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A host can have the login already, set up after the docs: a udev rule of its own that starts serial-getty@ for this NanoKVM, or the getty running on its port. The Proxmox host of 2026-10-11 is one. plan and apply now find such a rule (any udev rules file naming the vendor, product and this serial number with serial-getty@) and the running unit (the invocation link systemd keeps), report them, and change nothing: this script adds no second rule. A rule for another NanoKVM does not count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Seen on the Proxmox matrix: Ubuntu cloud images ship without linux-modules-extra, and Debian genericcloud images run a cloud kernel without USB drivers. The refusal now names the package to install. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Debian 11 (systemd 247) a repeated add event for a device that is already present does not start the unit named in SYSTEMD_WANTS, so an apply right after an undo left the login off until the next plug. When the unit is not active after the trigger, apply starts it; the rule still covers the next plug and boot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bash script in the style of community-scripts/ProxmoxVE tools/pve: header, root and PVE version check (8 and 9; 7 only warns), whiptail menu for plan, apply, undo and the serial number, or the helper's own arguments without dialogs. The POSIX helper is embedded in a quoted heredoc, so the published script downloads nothing; scripts/ build-community-script.py assembles it from the source, and CI checks the committed copy matches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…le test
On the Proxmox matrix the getty on Arch and Ubuntu 26.04 resets the terminal
and asks where the cursor is (CSI 6 n) before it prints the login prompt, and
waits for the answer. The console test only listened, so on those hosts it
reported "text" with the leftovers of ESC 7 / ESC 8 ("7 8") instead of the
prompt. The probe now replies to each query the way the browser terminal
does, and the classifier drops the two-byte ESC sequences too, so the setup
alone reads as silence and the prompt that follows as login.
The Ubuntu hint for a missing cdc_acm adds the case seen on 26.04 at first
boot: the running kernel's linux-modules-extra is already gone from the
archive, so linux-generic and a reboot are the way.
Tests: the captured setup sequence, a fake host that waits for the reply
before the prompt, and the extra hint line. The community-scripts copy is
rebuilt from the source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pped helper The community-scripts copy was the whole POSIX helper in a bash heredoc, with its Alpine, sysvinit, runit and OSTree branches that no Proxmox host has. It is now a short bash script of its own for Proxmox VE and Proxmox Backup Server (Debian, systemd), in that project's form: the product gate, the Debian release check, the container refusal, the whiptail menu and the argument mode. It writes the same rule file with the same content and records to the same backup directory as the helper, so either entry point reads the other's setup as "already set up" and undo works across them. The template, the assembling script and its CI step go; the global shellcheck step covers the new file. Checked on the Proxmox host with the NanoKVM passed through: Debian 12 and Debian 11 VMs with a stub pveversion (status, plan, apply with the login prompt read on the NanoKVM, apply again, undo with the port silent, apply after undo, undo again), once with a stub proxmox-backup-manager for the PBS branch; read-only status and plan on the real PVE 9.2 host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The maintainers' review of the DevScripts submission: the script carried its own colours, messages, checks and whiptail menu instead of the project's. It now sources Core (core.func, tools.func, error_handler.func, api.func) the way the current host tools do, and uses load_functions, catch_errors, the telemetry hook, header_info with APP_TYPE=tools (the banner comes from their generator), root_check, on_pve_host and _pve_version, and prompt_select, prompt_confirm and prompt_input for the menu. pve_check is not called: it needs /etc/pve mounted, and a console for a host in trouble must not depend on that; its version policy (PVE 8.x and 9.x) is kept, so PVE 7 is out, as it is for the rest of that project. Under catch_errors every command must succeed or be tested, so the reads are written that way. The Source line points at NanoKVM OS: the stock firmware has no USB serial function. Arguments: plan|status|apply|undo and --yes; the serial number of a NanoKVM that is not plugged in comes from NANOKVM_SERIAL. Checked on the Proxmox host with the NanoKVM passed through, Core from its main branch: Debian 13, 12 and 11 VMs with a stub pveversion, and the PBS branch with a stub proxmox-backup-manager on Debian 12: status and plan, apply without --yes and without a terminal changes nothing, apply --yes with the login prompt read on the NanoKVM, apply again, undo with the port silent, apply after undo, undo again, --help, a bad argument. Read-only status and plan on the real PVE 9.2 host with a local Core copy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review of Two fixes are needed before merging:
Integration note: current main also introduces a content conflict in Validation: all 18 host-helper tests and all 5 FAT-image tests pass. The existing targeted Go serial tests pass; the added overlapping-probe regression fails as described. Tests ran on local PTYs/fake roots; the physical NanoKVM was not touched. |
…-helper # Conflicts: # .github/workflows/ci.yml
Review: the busy check was an observation only, and deviceInUse skips this process, so two console tests at once both opened the port, read each other's answer and restored each other's line settings. The probe now takes a per-device lease before anything else and gives it back after the device is closed and the settings are restored; a second probe meanwhile reports busy. The lease registry (serial_lease.go) is the one the terminal session in #78 will share, so either holder keeps the other out in both start orders. Test: a 600 ms probe on a silent host, a 200 ms probe started while the first waits for its answer is busy, the lease is gone when the first ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: build.sh tools writes tools/serial-console/nanokvm-serial-console.sh and .img, and verify rejects every unlisted output. The script copy is the source file byte for byte; the FAT image comes from scripts/build-serial-console-disk.py, which is reproducible (two builds give the same bytes, as its test checks). Both hashes are from those. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Both fixed, and main merged in (
Tests after the merge: the 18 helper tests, the 5 FAT-image tests, |
… lease Review of the serial session, three findings. One writer. The device side and the input side both wrote bytes and notices to the WebSocket, and gorilla allows one data writer at a time: closing the device while a large browser write was blocked panicked with "concurrent write to websocket connection", in a goroutine the HTTP recovery does not cover. Every data write and its deadline now go through one mutex (wsWriter); the pings are control frames and stay outside. Cancellable writes. writeAllSerial waited for room in the tty for ever, and it ran in the goroutine that reads the WebSocket, so a browser that closed while the line did not take data was never seen and the device stayed claimed. The browser's messages now go through a bounded queue to a writer goroutine that waits in 200 ms slices and stops with the session; the WebSocket reader never waits on the tty. The ping goroutine is the watch on the connection's life: a ping to a browser that has gone fails, and that ends the session. No write timeout: a slow line still gets its bytes. The descriptor is closed after every goroutine has returned, and the lease released after that. The lease. The probe's busy check saw a terminal, but a terminal started while the probe ran opened the device and later got the probe's restored canonical settings over its raw line. #76 now holds a per-device lease for the probe's whole run (serial_lease.go); the session claims the same one, so either keeps the other out in both start orders. Tests: a disconnect during traffic in both directions (20 runs under -race), the browser closing during a blocked write releases the device within the ping interval, a terminal refused while the probe holds the device and admitted afterwards; the existing exclusivity test with the generic notice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Re-review complete on |
USB serial gave the managed computer a CDC ACM port, but the login on it was left to the administrator: a udev rule and a getty by hand, and
docs/usb-composition.mdsaid the Linux side was never tried. This adds a helper that sets the login up on the host, two ways to get it there (the NanoKVM's web server, and a read-only helper disk next to the virtual drive), a console test from the USB settings page, and the same helper wrapped for the Proxmox VE Helper-Scripts.The script:
nanokvm-serial-console.shPOSIX sh (dash, busybox ash, bash), runs on the managed Linux host, about 560 lines, nothing beyond coreutils.
plan(default) andstatusonly read. They report the distribution (os-release), the init system (systemd, sysvinit or busybox init with/etc/inittab, runit, OpenRC), whether this is a container, an immutable or read-only system (OSTree, NixOS), thecdc_acmdriver for the running kernel (loaded, built in, or a module file), the getty, and the NanoKVM on the USB bus (by3346:1009, with its serial number andttyACMname). Then they print whatapplywould change.applyasks for confirmation (--yesskips it; a non-terminal without--yeschanges nothing) and:/etc/udev/rules.d/90-nanokvm-serial-console.rules, two rules bound to the vendor, product and serial number: on the USB deviceENV{ID_MM_DEVICE_IGNORE}="1"(ModemManager stays away, old and new ModemManager), and on the ttyACTION=="add", SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", the same mark plusID_MM_PORT_IGNORE,TAG+="systemd",ENV{SYSTEMD_WANTS}+="serial-getty@%k.service",SYMLINK+="nanokvm-console". The login starts for whicheverttyACMnthe NanoKVM becomes, only while it is present. Thenudevadm control --reloadand, when the port is present,udevadm trigger --subsystem-match=tty --sysname-match=<port> --action=add(flags that old udevadm has too), and it checksserial-getty@<port>is active; when the trigger alone did not start it (systemd 247, Debian 11, after anundo), it starts the unit and checks again.restoreconon the rule when SELinux enforces.ttyACMn::respawn:getty …to/etc/inittab, on sysvinitnkac:2345:respawn:…(backup kept), and sends init SIGHUP./var/backups/nanokvm-serial-console/<timestamp>/. A secondapplychanges nothing and leaves no record.undoreverts the last change, reloads udev and stops the getty./.dockerenv,container=in PID 1's environment, cgroup), on OSTree/NixOS/read-only root, withoutcdc_acmfor the running kernel (with the known reasons: Ubuntu cloud images withoutlinux-modules-extra, Debian's cloud kernel), withoutserial-getty@.serviceoragetty/getty, and on runit or OpenRC as PID 1.serial-getty@for its serial number, or a runningserial-getty@<port>that the rule did not start),applysays so and adds no second rule.--serial SERIALbinds the rule when the NanoKVM is not plugged in yet (the USB page's command includes it); a connected NanoKVM wins over the option.console=is out of scope: early boot cannot go through CDC ACM anyway.Getting it onto a headless host
GET /nanokvm-serial-console.shon the NanoKVM, public (the host has no credentials, and the script is firmware source), withX-Checksum-Sha256. The USB page showscurl -fsS -k https://<nanokvm>/nanokvm-serial-console.sh -o nanokvm-serial-console.sh && sh nanokvm-serial-console.sh --serial <serial>(-konly when the page itself is HTTPS), the same for the NanoKVM's USB network address when the USB network is up (a host with no other network), and awgetform for busybox hosts.S03usbdevadds a second LUN tomass_storage.disk0, read-only, removable,inquiry_string"NanoKVM Setup Disk", backed by/kvmapp/system/share/serial-console/nanokvm-serial-console.img: a 1440 KiB FAT12 image (volume labelNANOKVM) with the script assetup.shand areadme.txt.mount -o ro /dev/disk/by-label/NANOKVM /mnt && sh /mnt/setup.sh. The LUN is removed again when serial is off, so the drives match the composition. A LUN can only be added or removed while the function is not linked into the configuration (the kernel answers "Resource busy"), so the script settles it before theln -s.scripts/build-serial-console-disk.py, in Python, byte-for-byte reproducible (fixed timestamps and volume id), no mtools needed on the build host;platform/build.sh toolsbuilds it andpackages.listships both files under/kvmapp/system/share/serial-console/. Its boot sector carries the stubmkfs.fatwrites ("This is not a bootable disk", wait for a key, next device), so a BIOS with a USB drive first in its boot order does not run zeros./dev/disk/by-label/NANOKVMnames both. A CD-ROM LUN would need an ISO 9660 image (FAT cannot be read through a 2048-byte-sector device); that can follow if/dev/sr0naming is wanted./etc/init.d/S03usbdevon a device is a symlink to the base package's copy (packages.list:46,/usr/libexec/nanokvm/legacy/S03usbdev), so the helper disk needsnanokvm-baserebuilt, not only the app package.firmware/services/community-scripts/nanokvm-serial-console.shis a separate bash script for Proxmox VE and Proxmox Backup Server hosts only (Debian, systemd), built on that project's Core as their host tools are: it sourcescore.func,tools.func,error_handler.funcandapi.funcfromCOMMUNITY_SCRIPTS_CORE_URL(so it needs that reachable, or a local copy), and usesload_functions,catch_errors,header_info(APP_TYPE=tools),root_check,on_pve_host, theprompt_*helpers for the menu, andplan|status|apply|undo [--yes]withNANOKVM_SERIALfor use without prompts. PVE 8.x/9.x and PBS on bookworm/trixie;pve_checkis not called because it requires/etc/pveto be mounted. It writes the same rule file with the same content and records to the same backup directory as the POSIX helper, so a host set up by one is "already set up" for the other andundoworks across them. Submitted as New Script: NanoKVM USB serial console (tools/pve) community-scripts/DevScripts#2414.Server and web
GET /api/vm/serial-console(admin): the gadget's USB serial number, the script's path, size and SHA-256, and whether the helper disk is shipped and attached (LUN 1 backed by it). When a package upgrade replaces the image while the LUN holds the old one, the kernel shows the path as deleted: reported as attached andstale, and the page says the computer sees the new disk once the USB devices reconnect (the next composition change or boot).POST /api/vm/serial-console/test(admin): sends one carriage return on/dev/ttyGS0, raw 8N1, and reads the answer for up to five seconds, as the browser terminal's first Enter does. Reportslogin,password,shell(the prompt ends the text),text,silent,busy(picocom's lock file, or any other process holding the port, found through/proc/*/fd),unavailable(no USB serial),error. ANSI sequences are stripped. Some gettys (seen on Arch and Ubuntu 26.04) reset the terminal and ask for the cursor position before the prompt, and wait for the answer: the probe replies as the browser terminal does, so the prompt can follow.enandru.docs/usb-composition.mddescribes the helper, and now says the terminal's 9600 baud is nominal (it said 115200 while the server always opensttyGSat 9600).README.mdpoints to the helper.Checked
firmware/services/test_serial_console.py(in CI): fake roots for Debian 12 and 8, Devuan (sysvinit), Rocky with SELinux, Alpine (busybox init with inittab), runit, unknown init, noos-release; every refusal (three container signals, OSTree, NixOS, read-only root, nocdc_acmwith the Ubuntu and Debian cloud hints, no unit, no agetty); the foreign rule and the foreign getty;planandstatuschange no file and run no command, for every case, under dash, busybox sh and bash;applywrites the expected rules or inittab line and runs exactlyudevadm control --reload, the targeted trigger and theis-activecheck, and starts the unit when the trigger did not; a secondapplychanges nothing;undorestores files and stops the getty;--serialand the connected device; a rule for another serial is replaced with a backup;--helpand a bad argument. shellcheck clean.scripts/test-serial-console-disk.py(in CI): layout, both FATs, the root directory entries with lower-case names, reproducibility, and mtools (mdir,mtype) reading the files back.classifySerialResponse(prompts, ANSI, the captured terminal setup); the probe against a pty with a fake getty (login prompt, a getty that waits for the cursor position reply, ANSI shell prompt, silent host within the timeout, missing device, a lock file, another process holding the port); the state and the handlers.go vet,go test -tags teststub ./....tsc --noEmit, eslint, prettier,test:i18n.scripts/test-platform-build.py,test-platform-config.py.pveversion, and once a stubproxmox-backup-managerfor the PBS branch; Core from its main branch): status and plan without the device and withNANOKVM_SERIAL, apply without--yesand without a terminal changes nothing,apply --yes→nkvm-test-debianNN login:read on the NanoKVM, apply again (nothing to do), undo (inactive, port silent), apply after undo, undo again, undo with nothing left,--help, a bad argument. PBS is exercised through the stub only. Read-onlystatusandplanon the real PVE 9.2 host, with a local Core copy, report the hand-made setup and change nothing.GET /api/vm/serial-consolereports the seriald19506b31359c372; applying the console preset createslun.1withfile= the image,ro=1,removable=1, inquiry "NanoKVM Setup Disk"; turning serial off removes the LUN andattachedgoes false, turning it on brings it back; the console test reportssilent, right for a Windows host without a login on the COM port, andbusywhile another process holds/dev/ttyGS0.Linux hosts end to end
A second NanoKVM (serial-only composition, NanoKVM OS 1.0.0-beta.11) on a Proxmox VE 9.2 host (pve-manager 9.2.21, kernel 7.0.14-19-pve, Debian 13.7), passed through (
qm set --usb0 host=3346:1009, hot-plugged into running q35 VMs) to one VM per distribution, one at a time. For each VM the driver ranplanwithout the device and with it,apply, read the getty state, sent Enter from the NanoKVM side and captured what came back, ranapplyagain (nothing changed),undo(getty gone, port silent),applyagain,undoagain, then detached the device and checked the host's own getty was back on/dev/ttyACM0.undothe trigger alone did not restart the unit there, the script's explicitsystemctl startdidttyACM0::respawn:/sbin/getty -L 115200 ttyACM0 vt220)CSI 6 n) and waits for the reply before the prompt; the passive capture ended before that. The console test now answers the query (commit c859e2b); the browser terminal always did.cdc_acm: Debian genericcloud (cloud kernel), Ubuntu cloud withoutlinux-modules-extra(on 26.04 the running kernel's package had already left the archive: the hint now sayslinux-genericand reboot), Alpine cloudlinux-virt, openSUSE Minimal-VMkernel-default-base. The table rows are after the ordinary kernel was installed (Debian generic image;linux-modules-extra/linux-generic;linux-lts;kernel-default), which is what an installed host runs.statusandplanreport "already set up by /etc/udev/rules.d/70-nanokvm-serial-console.rules" and would change nothing. Noapplywas run there.Not covered
ttyGS0fails on current builds withpicocom: executable file not found:scripts/build-picocom.shexists but nothing builds or packages it, although the docs say it is bundled. Found while checking thebusystate; terminal: bridge serial ports in the server, without picocom #78 replaces picocom with a native session.docs/USB-SERIAL.md) is not installed by the script.platform/expected.sha256lists the twotools/serial-console/outputs (the script byte for byte, the reproducible FAT image); the web assets still need the platform build, as for any web change.🤖 Generated with Claude Code