=APPLY(plan.json)
1
lfr.kosir.info · agentless · MIT

linux-apply

One hand for the plans that do not need a whole liturgy.

A shared applier for the toolkit. Give it a plan from linux-patch, linux-users or linux-firewall and it normalises the plan into actions, re-validates each one against the live host, and runs only what is still needed.

“A plan is a snapshot. A server is not.” The discipline every tool in the family shares, in one line

Each tool already has its own apply, with its own careful, tool-specific behaviour — linux-patch's serialized reboots, linux-users' protection rules, linux-firewall's SSH guard. This is not a replacement for those. It is the lighter, uniform path for straightforward plans, and the one place where actions from different tools can be composed into a single run.

action_plan.json · adapted 4 days ago · click to re-validate against live state

[patch.update] update 5 package(s) (4 security)
[users.lock] lock m.horvat
[patch.update] update 3 package(s) on app02
[firewall.rule] add service https, then reload
↑ four actions, all of them believed necessary on Tuesday
2

The Shape of an Action

Deliberately simple, so that nothing clever can hide in it

An action plan is a list of hosts, and per host a list of actions. Each action carries an id, a description, an optional validate snippet, and a run snippet. That is the whole schema.

// one action, as an adapter emits it { "schema": "linux-apply.action-plan", "hosts": [ {"host": "web01.hostname.loc", "actions": [ {"id": "web01.hostname.loc:dnf-update", "description": "update 5 package(s) (4 security)", "validate": "dnf -q check-update kernel openssl ...; [ $? -eq 100 ]", "run": "dnf -y update kernel openssl ...", "kind": "patch.update"} ]} ] }

The validate line is the whole point. Exit 0 means the change is still needed. Anything else means the world has moved on since the plan was written, and the action is skipped rather than forced.

Action plans are produced by adapters, never written by hand. An adapter takes a tool's own plan and emits the normalised form, and it owns the safe construction of the shell snippets — every value taken from the source plan is shell-quoted on the way in. Hand-writing a run snippet would put that responsibility on thee, which is exactly the arrangement this avoids.

3

Three Wired Adapters

Each with a guard chosen for the way that particular thing goes wrong

AdapterReadsGuard, and what it drops
patch
--from patch
linux-patch.update-plan
One dnf-update action per host
Guarded by a live dnf check-update of the same packages, so a host somebody patched by hand since the plan was made is skipped. Honours --security.
users
--from users
linux-users.account-plan
One lock action per stale candidate
Guarded by “the account still exists and is not currently logged in”, so anyone who came back since discover is skipped. root and UID 0 are dropped defensively. Expiry only with --expire.
firewall
--from firewall
linux-firewall.rule-plan
Per-rule actions, then one reload per host
Each rule guarded by a live firewall-cmd --query — add only if absent, remove only if present. Removal of the ssh service or the control SSH port is dropped defensively. nftables and unmanaged hosts are skipped.

Three, not ten. linux-audit and linux-drift are read-only and have no plan to apply; linux-harden and linux-diskspace have their own executors with behaviour too specific to normalise — watchdogs, backups, protected-path deny lists. Those stay where they are.

4

How run Behaves

One confirmation, one script, one guard per action

For each host it builds a single script that, per action, runs the validate guard and then the run command only if the guard passes. The host is confirmed once — with its actions listed — before anything executes. Nothing runs without the guard agreeing the change is still needed.

web01.hostname.loc: [patch.update] update 5 package(s) (4 security) [users.lock] lock m.horvat [firewall.rule] add service https, then reload Apply 3 action(s) on web01.hostname.loc? [y/N/q]

Afterwards it writes apply_results.json, recording each action's outcome as one of three words:

OutcomeWhat it means
applied The guard agreed, the command ran, and it exited cleanly.
skipped The guard said the change was no longer needed. Not a failure — usually the most reassuring line in the file.
failed The guard agreed and the command did not succeed. Named, with the host, so thou canst go and look.
No spreadsheet, and that is deliberate

This is the one tool in the family that writes no xlsx. It has no third-party dependencies at all — standard library only, not even openpyxl. The reporting belongs to the discover tools; this one only needs to say what it did, to a file another program can read.

5

The Rite of Execution

Adapt, or adapt and run in one breath

# receive it — there is nothing to install git clone https://github.com/vikozs/linux-apply.git cd linux-apply
# adapt — turn a tool plan into an action plan, touching no host linux-apply adapt --from patch \ --plan patch_plan.json -o action_plan.json
# run — confirms per host, guards every action linux-apply run --plan action_plan.json \ -H hosts.txt -u local.user \ --ask-ssh-pass --sudo-pass-same-as-ssh
# or adapt and run in one step — lock stale accounts, and expire them too linux-apply run --from users --tool-plan users_plan.json --expire \ -H hosts.txt -u local.user \ --ask-ssh-pass --sudo-pass-same-as-ssh

Python 3.9+ and nothing else, plus sshpass only if thou usest password SSH login. Auth flags match the rest of the family. Passwords travel by stdin or the SSHPASS environment variable, never argv.

6

What This Actually Is

Breaking the fourth wall, briefly

A real tool, written by a real sysadmin who noticed he was writing the same validate-then-act loop for the fourth time and stopped. Agentless, MIT licensed, standard library only.

It is not the toolkit's only executor and was never meant to be. Each tool keeps its own apply where the behaviour is genuinely specific — a reboot that must be serialized, a watchdog that must survive a lost session, a deny list that must not be configurable. This one exists for the plans where none of that is needed, and for the runs where thou wantest patch, users and firewall actions to go out together.

The adapters shell-quote every value they take from a source plan before it reaches a command, because those plans were built from what hosts reported, and a host can be lying. That part is not a joke.

Built by vK, who has been paged at 03:00 and did not enjoy it. It serves the wider toolkit — three of its books, so far.

Kneel also at the parish and read at the scriptorium. The Cluster abides. The Loop reconciles. This one checks first.

7

The Communion of Tools

Nothing here works alone, and neither shouldst thou

The fleet is one testament in several books. Each does one job, writes a report thou canst hand to anyone, and refuses to be clever with thy servers.