You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(facts): add requires_command guard sentinel and preconditions() hook
Background
----------
Two related reliability gaps existed in the fact-collection layer:
1. The old shell guard for requires_command used:
! command -v <bin> >/dev/null || (<fact-command>)
When the binary is absent this produces *no stdout*, making it
impossible to distinguish "binary not installed" from "binary
installed but returned no data". Both cases silently return
default(), masking real errors.
2. There was no way for a fact to express runtime prerequisites beyond
"this binary must exist" — e.g. ZFS datasets cannot be listed when
the zfs kernel module is not loaded, even if the zfs binary is
present.
Changes
-------
Replace the old `! … ||` guard with an explicit if/then/else that emits
a unique marker when the binary is absent:
if command -v <bin> >/dev/null 2>&1; then
(<fact-command>);
else
echo '##PYINFRA_NOCMD##';
fi
The marker `_MISSING_COMMAND_MARKER = "##PYINFRA_NOCMD##"` is detected
in `_get_fact` and converted into a `MissingCommandError` exception so
callers can tell the two cases apart.
FactError
└── FactNotCollected # base: fact skipped, not a data error
├── MissingCommandError # requires_command binary absent
└── FactPreconditionError # preconditions() not satisfied
All three are exported from `pyinfra.api`.
Both exceptions obey the deploy phase in `get_fact()`:
- **Prepare phase**: silently return `default()`. The binary / module
may simply not be installed yet; a later operation will install it.
- **Execute phase**: re-raise. The binary / module should already be
present; its absence indicates incorrect ordering in the deploy.
New optional override on `FactBase`:
def preconditions(self, state, host) -> bool | tuple[Literal[False], str] | None:
Return `True` / `None` to proceed, `False` to skip with a generic
message, or `(False, "human reason")` to skip with context. The
framework raises `FactPreconditionError` automatically — fact authors
never need to import exception classes.
* `ZfsPools` added with `requires_command = "zpool"`.
* `ZfsDatasets.preconditions()` uses the existing `server.KernelModules`
fact to verify the zfs kernel module is loaded before attempting
`zfs get -H all` — a real-world scenario where `requires_command`
alone is not sufficient.
Tests
-----
* tests/test_api/test_api_facts_missing_command.py — phase-aware
behaviour for MissingCommandError and the ##PYINFRA_NOCMD## sentinel.
* tests/test_api/test_api_facts_preconditions.py — phase-aware
behaviour for FactPreconditionError via preconditions().
0 commit comments