fix(nixos): smartd self-gates on the hardware — the doctor was right, smartd wasn't
Some checks failed
Check / eval (push) Has been cancelled

BACKLOG #118. Bernardo booted the live ISO and the Waybar health icon was red,
reporting smartd. Everything downstream turned out to be working correctly,
which is the part worth recording: smartd's config is DEVICESCAN, and where no
drive answers SMART it exits 17 ("Unable to monitor any SMART enabled
devices"), systemd marks the unit failed, nomarchy-doctor faithfully reports a
failed system unit, and Waybar paints @bad. The doctor was telling the truth.
smartd was the bug.

Scope was never live-only, which is why this sat in NOW rather than as a live
nit: services.smartd.enable mkDefaults true on every machine and QEMU virtio
exposes no SMART, so every VM install has been booting to a health warning
about a daemon with nothing to do — and every V2 run had been showing it as
noise.

Fixed with the distro's own self-gate convention: an ExecCondition running
`smartctl --scan`, which prints nothing exactly when smartd would find nothing.
A failed condition leaves the unit inactive rather than failed. Deliberately
NOT SuccessExitStatus = 17: that would also swallow exit 17 from a machine that
does have drives, which is the entire reason the daemon ships.

V2. checks.smartd-gate boots the REAL distro module rather than a restatement
of it (its nixpkgs.config needs mkForce to yield to the test's pkgs) and
asserts both halves, because they pull in opposite directions: a gate that
never skips leaves the red icon, and a gate that always skips silently disables
drive-health monitoring on real hardware — the failure nobody notices until a
disk dies quietly. So the no-SMART node must go ActiveState=inactive, unfailed,
and absent from `systemctl --failed` (what the doctor actually reads); and the
gate's logic is driven against a scan that DOES find a device, since QEMU
cannot answer SMART honestly and pretending otherwise would test nothing. The
check was proved to fail by unwiring the condition. flake check, doctor,
hardware-toggles, live-baseline-apps, option-docs and state-bridges pass.

V3 pending: that smartd still RUNS where drives have SMART (dev box, real
NVMe). Queued with an explicit fail-condition — if it skips there, revert the
gate rather than tune it.

Also swept: #120's size table said the duplicate chromium was gone; #121 was
reverted, so it is back and the table says so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 17:38:58 +01:00
parent 0727ba6d44
commit 19ba4e2b12
6 changed files with 162 additions and 51 deletions

View File

@@ -560,10 +560,6 @@
touch $out
'';
# nomarchy-doctor's contract: an induced failed unit flips the
# sheet to ✖/exit-1 and names the unit; with the failure
# cleared it reports healthy/exit-0. Minimal node (just the
# package) — the disk/flake/snapper checks self-skip in a VM.
# Live-ISO baseline apps (ROADMAP § live-ISO baseline apps, #103).
# The live session shipped no
# browser and no office for as long as it existed — the thing a user
@@ -654,6 +650,74 @@
touch $out
'';
# smartd self-gate (BACKLOG #118). Where no drive answers SMART,
# smartd exits 17 and systemd marks the unit FAILED — so the doctor
# honestly reported a failed unit and Waybar went red on every QEMU
# guest and most live USBs. Both halves matter and they pull in
# opposite directions, which is why both are asserted here: a
# gate that never skips leaves the red icon, and a gate that always
# skips silently disables drive-health monitoring on real hardware —
# the failure nobody would notice until a disk died quietly.
smartd-gate = pkgs.testers.runNixOSTest {
name = "nomarchy-smartd-gate";
nodes = {
# QEMU virtio: no SMART. The machine Bernardo saw. Imports the
# REAL distro module, so this guards the shipped wiring rather
# than a restatement of it — runNixOSTest supplies the node's
# pkgs, so the module's own `nixpkgs.config` has to yield.
nosmart = { lib, pkgs, ... }: {
imports = [ ./modules/nixos/default.nix ];
nixpkgs.config = lib.mkForce { allowUnfree = true; };
nomarchy.system.greeter.enable = false;
# For the test's own probing only — the gate calls smartctl by
# store path, so this changes nothing about what is measured.
environment.systemPackages = [ pkgs.smartmontools ];
};
};
testScript = ''
nosmart.wait_for_unit("multi-user.target")
# The gate itself agrees there is nothing to monitor.
cond = nosmart.succeed(
"systemctl show -p ExecCondition --value smartd.service"
)
assert "smartd-any-smart-device" in cond, f"gate not wired: {cond}"
nosmart.succeed("smartctl --scan | tee /dev/stderr | (! grep -q .)")
# Inactive, NOT failed the whole point. Before #118 this was
# 'failed' with Result=exit-code / status=17.
state = nosmart.succeed(
"systemctl show -p ActiveState --value smartd.service"
).strip()
assert state == "inactive", f"expected inactive, got {state}"
nosmart.fail("systemctl is-failed --quiet smartd.service")
# And therefore the thing the user actually sees: the doctor's
# failed-unit check is what reddened the Waybar icon.
failed = nosmart.succeed("systemctl --failed --no-legend --plain")
assert "smartd" not in failed, f"smartd still failed: {failed}"
# The other half: a machine WITH a SMART device must still run
# smartd. QEMU cannot answer SMART, so drive the gate's logic
# against a scan that finds one, rather than pretend otherwise.
nosmart.succeed(
"mkdir -p /tmp/fake && "
"printf '#!/bin/sh\\necho \"/dev/sda -d scsi # /dev/sda, SCSI device\"\\n' "
"> /tmp/fake/smartctl && chmod +x /tmp/fake/smartctl"
)
gate = cond.split("argv[]=")[1].split(" ")[0].strip("; ]")
nosmart.succeed(
f"sed 's|/nix/store/[^ ]*/bin/smartctl|/tmp/fake/smartctl|' {gate} > /tmp/gate2"
)
nosmart.succeed("chmod +x /tmp/gate2")
nosmart.succeed("/tmp/gate2") # exit 0 => smartd would start
'';
};
# nomarchy-doctor's contract: an induced failed unit flips the
# sheet to ✖/exit-1 and names the unit; with the failure
# cleared it reports healthy/exit-0. Minimal node (just the
# package) — the disk/flake/snapper checks self-skip in a VM.
doctor = pkgs.testers.runNixOSTest {
name = "nomarchy-doctor";
nodes.machine = { ... }: {