-
Notifications
You must be signed in to change notification settings - Fork 1
Reference
The flags, the exit codes, how fettle becomes root, how the code is laid out, and how to work on it.
| Option | Effect |
|---|---|
-a, --all
|
run the default action set |
--dry-run |
print what would run; execute nothing (read-only queries still run) |
--no-sync |
dry-run preview: use cached repo data instead of a fresh sync |
--full-preview |
with --dry-run: elevate so the preview resolves new deps + removals (rhel)
|
--only ACTION / --skip ACTION
|
restrict / exclude actions (repeatable) |
--yes |
assume yes to all prompts (non-interactive) |
--no-aur-precheck |
skip the pre-upgrade AUR IoC gate (arch) |
--force-aur |
with --yes, install AUR pkgs despite a CRITICAL pre-check finding (arch)
|
-R, --auto-rebuild
|
offer to rebuild instead of only listing (with -r/-y) |
-v / -q / --no-color
|
verbose / quiet / disable color (also honors NO_COLOR) |
--distro NAME |
override distro detection |
--print-config / --version
|
print config or version and exit |
Short version: 0 means the run completed, not that the machine is clean. What
counts as "completed" depends on how you invoked it, deliberately.
0 |
1 |
2 |
|
|---|---|---|---|
a single action (fettle -V, -S, -P, …) |
nothing wrong, nothing missed | the action failed, or a check could not look, or it looked and found something | — |
fettle -M (compromise-check) |
it looked, and found nothing above Medium | a High or Critical finding, or it examined nothing at all | — |
--everything |
the run completed | an action could not do its job | — |
fettle report |
the dashboard was rebuilt | it could not be written | — |
fettle remote <host> |
the remote's own status | the remote's own status | — |
fettle remote <group> |
every host succeeded | any host failed or was unreachable | — |
| any invocation | — | — | bad arguments (argparse) |
compromise-check is the one action whose status turns on severity rather than on
findings existing. Every real machine has some — the reference desktop has four — and
exiting non-zero on all of them would make -M red forever and teach people to ignore
it, the same trap -H avoids. High and Critical are the two bands that also print the
preservation banner, so the status and the banner agree about what "stop and look at
this" means. Partial blindness (a missing bpftool, an unreadable /sys/fs/bpf) is
reported loudly and does not set the status; only a run that examined nothing fails
on it. See Compromise indicators.
| a single host inside a group | — | — | 255 = unreachable, distinct from a failed run |
Gate automation on a single action, not on --everything. fettle -V is a tripwire:
it goes red when a packaged file's contents have changed, when the integrity database
could not be opened, or when the tool is missing — any of which means you should look.
That is precisely what a cron job or CI step wants.
--everything answers a different question on purpose. Fourteen checks on a real machine
will essentially always find something — advisory-check alone reports 142 packages with
fixes available on the machine this was developed on — so a status that failed on findings
would be red every single time and would stop being read. It also does not fail on
blindness, which is the deliberate part: on that same machine chipsec cannot run at all,
so two checks report "could not run" on every run, and failing on a condition nobody can
fix is how an exit code becomes noise.
So read the summary, not just the status. Findings are in it, and everything that
could not be examined is listed under Not checked at the end of every run, with the
command to fix it where a missing tool is the cause:
▸ Not checked
these were not examined, so nothing above speaks for them
? storage device firmware — smartctl is not installed
install: sudo apt install smartmontools
That block is how fettle keeps its central promise — a check that cannot look must never render identically to a clean result — without spending the exit code on it.
fettle elevates lazily and by itself — you never need to type sudo fettle.
-
Maintenance actions re-exec under
sudoonly when a selected action will actually change the system. Read-only work such aspkg-audit(-P),aur-audit(-A) andconfig-drift(-d) runs unprivileged and never prompts.--dry-runnever elevates. -
hardening-audit(-H) elevates since v1.17.0. Its AppArmor axis reads state that is root-only: unprivileged,aa-statusreports "You do not have enough privilege to read the profile set" and exits 0, so the answer would be the reassuring half on its own. Pass--userto keep it unprivileged, the same waysys-audit --userworks. Measured on Debian 13: identical findings either way, 28 s as root against 27 s with--user. -
pkg-integrity(-V) is the exception: read-only, but it does elevate, because unprivileged it cannot read a large share of the files it must hash. "Read-only" and "needs no root" are different questions. -
sys-auditelevates itself too (most checks need root); pass--userto stay unprivileged.--listandremotedon't elevate.
Being in the read-only set does not mean an action executes unprivileged. It means
that action does not elevate on its own. Run it inside -a or --everything and the
process has already re-exec'd under sudo for the mutating half — so every action after
that point is root, including the ones that never asked to be.
That distinction is not academic: it hid three bugs for months. pkg-audit reads GNOME
extensions, rootless podman images and --user flatpak apps, all of which live in your
account. Run alone it saw them; run inside -a it was root, and root's copies are empty or
unreachable. Since v1.13.0-1.15.0 those three are asked as the invoking user regardless of
how the run started (see Package supply-chain). Any new check
should ask whose data it is reading before assuming the answer is "the machine's".
Because elevation re-execs the full python3 -m fettle path (not the fettle
name), it works even though the launcher in ~/.local/bin isn't on root's PATH
— which is why sudo fettle … is unnecessary (and fails with command not found
unless you also install to a system path).
Your config path is carried across the re-exec, so your keep_orphans,
exclude_foreign, and [updaters] settings are honored on elevated runs too
(sudo resets HOME to /root, so without this the elevated process would
quietly fall back to built-in defaults).
-
One backend per distro family (
fettle/backends/*.py) implementing a sharedPackageBackendABC; a backend advertises the actions it supports, and the CLI hides the rest. Adding a distro is one subclass + one registry line — never a new script. - Curated command allowlist per backend: config tunes behavior (skip flatpak, pick nala), it never discovers new commands to run.
-
Normalized supply-chain model (
fettle/supplychain/): oneFindingformat and one seven-question set; each source provider answers what its ecosystem can and states its coverage. -
Mockable seams: all command execution goes through one
run()wrapper, and thesys-auditchecks read/sys·/proc·/devthrough an injectableroot— so the whole thing is unit-tested with no root and no real hardware. -
Everything routes through one output layer (
fettle/output.py) for a single color / verbosity / summary language.
python -m venv venv && source venv/bin/activate
pip install -e '.[dev]' # pytest + ruff (dev-only; runtime stays pure-stdlib)
pytest -q # full unit suite
ruff check fettle/ tests/ # lintTests mock external commands via unittest.mock.patch("subprocess.run", …) and
fake /sys·/proc trees with a tmp_path root, so they need neither root nor
special hardware. Runtime code never imports pytest — the shipped tool is
pure standard library.
Unit tests mock the package managers, which proves the parsing but not the assumptions behind it — several bugs here were formats and exit codes that documentation described incorrectly. Two harnesses cover that:
- Containers for most things: fast, disposable, and reproducible in a way a borrowed host is not ("no findings" on a real box might mean clean, unentitled, or empty).
-
tests/lab/for what containers cannot reach — systemd timers actually firing, snapd, fwupd, real reboots, the privilege model, andfettle remoteover ssh. It builds small cloud-image VMs on a KVM/libvirt host, snapshots each one, and reverts before every run so the pending-update set is identical every time. Seetests/lab/README.md; host specifics go in a gitignoredlab.conf.
Both are stdlib/shell only. dependencies = [] is load-bearing — the remote zipapp has to
run under any bare python3.