Modernize lager uninstall to match what the modern install creates#116
Conversation
The --all cleanup had not kept pace with several releases of install changes: the udev glob (lager-*.rules) never matched the shipped 99-instrument.rules, and the usbtmc modprobe blacklist, the lager-box-config sudoers file, the firewall helper script, the lager sysctl config, and the "lager" group were never removed at all. The removal list is now a single module-level spec shared by the confirmation listing, --dry-run, the removal session, and the unit tests, so it cannot silently drift from what install creates again. Privileged removals now actually happen (and report honestly) on boxes without passwordless sudo: each sudo step used to run over BatchMode SSH with `|| true`, so every one failed silently and printed "done" -- a plain uninstall left /etc/lager behind while claiming success (reproduced live on hardware). All privileged steps now run in one interactive ssh -t session (at most one sudo password prompt) with per-step OK/FAILED results read back from the box, sudoers grants removed last, and failures summarized instead of hidden. --all now also revokes access: this machine's key is stripped from the box's authorized_keys, matched exactly by the local ~/.ssh/lager_box.pub key blob (falling back to the default key comment). Previously the "deploy keys" cleanup deleted box-side private keys that modern installs never create while the actual access grant survived. --keep-config is honored together with --all (previously --all deleted /etc/lager regardless), and --dry-run inspects the real artifact list without requiring sudo. Left in place by design (and said so in the output): docker itself (packages, buildx, the daemon.json dns entry) and pip/apt packages installed for lager workflows.
There was a problem hiding this comment.
Code Review
This pull request significantly improves the lager uninstall command to ensure it completely and reliably removes all artifacts created by modern installs. Key changes include consolidating privileged removal steps into a single interactive SSH session (reducing sudo password prompts and preventing silent failures on boxes without passwordless sudo), honoring --keep-config alongside --all, and removing the local machine's key from the remote authorized_keys. Unit tests were also added to pin this uninstallation specification. Feedback on the changes suggests using a user-specific path instead of a hardcoded /tmp file for temporary results to avoid security risks, using a context manager with explicit encoding when opening the local public key file, and joining remote commands with semicolons instead of newlines to prevent terminal echo issues during interactive SSH sessions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| "sudo rm -rf /etc/lager", | ||
| ) | ||
|
|
||
| _PRIV_RESULTS_PATH = "/tmp/lager-uninstall-results.txt" |
There was a problem hiding this comment.
Using a hardcoded path in /tmp can lead to conflicts or security issues (such as symlink attacks or permission errors) in multi-user environments if another user has already created the file with restrictive permissions. Since this path is only executed on the remote box via shell commands, using a user-specific path like ~/.lager-uninstall-results.txt is much safer and avoids shared directory conflicts.
| _PRIV_RESULTS_PATH = "/tmp/lager-uninstall-results.txt" | |
| _PRIV_RESULTS_PATH = "~/.lager-uninstall-results.txt" |
| fields = open(pub_path).read().strip().split() | ||
| if len(fields) >= 2: | ||
| return fields[1] |
There was a problem hiding this comment.
Opening a file without a with statement leaves the file descriptor open until garbage collection runs, which can lead to resource leaks. Additionally, it is safer to specify an explicit encoding (like utf-8) to prevent potential decoding errors on systems with different default locales.
| fields = open(pub_path).read().strip().split() | |
| if len(fields) >= 2: | |
| return fields[1] | |
| with open(pub_path, "r", encoding="utf-8") as f: | |
| fields = f.read().strip().split() | |
| if len(fields) >= 2: | |
| return fields[1] |
| ssh_cmd = ["ssh", "-t"] | ||
| if ssh_pool: | ||
| ssh_cmd.extend(ssh_pool.get_ssh_options(ip)) | ||
| ssh_cmd.extend([ssh_host, "\n".join(wrapped)]) |
There was a problem hiding this comment.
When executing commands interactively over SSH with a pseudo-TTY (ssh -t), passing a multi-line string joined by newlines can sometimes cause issues with terminal echo, line-by-line execution, or race conditions with interactive prompts (like sudo password prompts). Joining the commands with ; instead of \n ensures they are sent as a single robust command line.
| ssh_cmd.extend([ssh_host, "\n".join(wrapped)]) | |
| ssh_cmd.extend([ssh_host, "; ".join(wrapped)]) |
…g for pubkey read, semicolon-joined priv session payload
|
Adopted all three findings in the latest commit:
Since 1 and 3 alter the exact payload that was hardware-validated, the new payload shape was re-verified in a stubbed-ssh sandbox against the real step spec: all 8 steps execute, tilde expansion works, and the results file lands in |
Summary
lager uninstallhad not kept pace with several releases of install changes. An audit against what today'slager install/lager box config applyactually create found six problems, all fixed here:--alludev glob never matched the shipped rules. It removedlager-*.rules/*lager*.rules, but the instrument rules file is99-instrument.rules— so the primary artifact survived a "complete removal".lager-box-configsudoers file, the firewall helper script (/usr/local/lib/lager/secure_box_firewall.sh), the lager sysctl config, and thelagergroup.|| true, so each one failed silently and printed "done" — reproduced live: a plain uninstall reported "Removing /etc/lager directory... done" while the directory survived.authorized_keys— survived.--allignored--keep-configand deleted/etc/lagerregardless.--dry-runinspected the stale artifact list and usedsudo duunder BatchMode, reporting/etc/lageras "(not found)" on boxes where it was present.Design
UNINSTALL_ALL_PRIV_STEPS) shared by the confirmation listing,--dry-run, the removal session, and the unit tests — pinned bytest/unit/test_uninstall_spec.pyso the list cannot silently drift from what install creates again.lager updatealready uses): all sudo steps run in a singlessh -tsession with per-step OK/FAIL results written to a results file and read back — at most one sudo password prompt, honest per-step reporting, failures summarized at the end. Sudoers grants are removed last so earlier steps can ride them or the cached sudo timestamp.~/.ssh/lager_box.pubkey blob (comments vary across old installs), falling back to the default key comment; runs as the last remote operation since it cuts BatchMode access. The output warns that the next SSH will need a password.Validation (hardware, 2026-07-13)
--allwipe): exactly one sudo password prompt in the privileged session; every step reported individually; post-verify confirmed all artifacts gone (/etc/lager, udev rules, blacklist, sudoers files, firewall script,lagergroup, third_party). The subsequent reinstall's own SSH check reported "Passwordless SSH not configured" — independent proof the authorized_keys revocation worked — and the box restored fully on v0.31.7.--all --keep-config): zero password prompts;/etc/lagerstep correctly skipped andsaved_nets.jsonsurvived untouched while all--allartifacts were removed; reinstall restored the box end-to-end (lager helloOK, instruments enumerate).lshiding files when any sibling path was missing) — fixed and covered by the; truehandling.Notes for reviewers
docker image prune -afbehavior is unchanged (it has always removed ALL unused images); the confirmation listing now says so explicitly.pigpiocontainer) are retained for old boxes.