Skip to content

Reference

Paul Asadoorian edited this page Aug 10, 2026 · 4 revisions

Reference

The flags, the exit codes, how fettle becomes root, how the code is laid out, and how to work on it.

Common options

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

Exit codes — what fettle's status actually means

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
--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)
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.

How elevation works

fettle elevates lazily and by itself — you never need to type sudo fettle.

  • Maintenance actions re-exec under sudo only when a selected action will actually change the system. Read-only work — pkg-audit (-P), aur-audit (-A), hardening-audit (-H), config-drift (-d) — runs unprivileged and never prompts. --dry-run never elevates.
  • 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-audit elevates itself too (most checks need root); pass --user to stay unprivileged. --list and remote don't elevate.

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).

Architecture

  • One backend per distro family (fettle/backends/*.py) implementing a shared PackageBackend ABC; 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/): one Finding format 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 the sys-audit checks read /sys·/proc·/dev through an injectable root — 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.

Development

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/    # lint

Tests 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.

Testing against real distros

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, and fettle remote over 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. See tests/lab/README.md; host specifics go in a gitignored lab.conf.

Both are stdlib/shell only. dependencies = [] is load-bearing — the remote zipapp has to run under any bare python3.

Clone this wiki locally