use std::collections::BTreeMap;
use std::path::PathBuf;
use sysknife_brain::planning_tools::propose_plan::KNOWN_ACTIONS;
use sysknife_core::action_family::{DEBIAN_ONLY_ACTIONS, FEDORA_ONLY_ACTIONS};
use sysknife_daemon::actions::{
apparmor, apt, apt_preferences, auditd, certbot, cloudinit, containers, deployment, distrobox,
fail2ban, filesystem, flatpak, grub, identity, journald, layering, livepatch, logging, lvm,
mounts, multipass, netplan, network, package_repos, pam, ppa, processes, reboot,
release_upgrade, resolvectl, services, snap, ssh, sudoers, sysctl, system_info, toolbox,
ubuntu_pro, ufw, users, ActionMechanism, ActionSpec,
};
fn sections() -> Vec<(&'static str, Vec<ActionSpec>)> {
vec![
("Deployment (atomic host)", deployment::specs()),
("Package layering (rpm-ostree)", layering::specs()),
("Filesystem", filesystem::specs()),
("Flatpak", flatpak::specs()),
("Toolbox", toolbox::specs()),
("Services", services::specs()),
("Processes", processes::specs()),
("Journald", journald::specs()),
("Storage — LVM", lvm::specs()),
("Kernel parameters — sysctl", sysctl::specs()),
("Mounts & swap", mounts::specs()),
("Log management", logging::specs()),
("PAM password policy", pam::specs()),
("auditd", auditd::specs()),
("certbot / ACME", certbot::specs()),
("Scoped sudoers.d", sudoers::specs()),
("Network", network::specs()),
("resolvectl", resolvectl::specs()),
("Identity / time / locale", identity::specs()),
("Users & groups", users::specs()),
("SSH keys", ssh::specs()),
("Package repositories", package_repos::specs()),
("System info", system_info::specs()),
("Containers", containers::specs()),
("Reboot", reboot::specs()),
("AppArmor", apparmor::specs()),
("cloud-init", cloudinit::specs()),
("fail2ban", fail2ban::specs()),
("apt", apt::specs()),
("apt preferences / pinning", apt_preferences::specs()),
("PPA", ppa::specs()),
("snap", snap::specs()),
("ufw", ufw::specs()),
("netplan", netplan::specs()),
("distrobox", distrobox::specs()),
("GRUB", grub::specs()),
("Ubuntu release upgrade", release_upgrade::specs()),
("Ubuntu Pro", ubuntu_pro::specs()),
("Livepatch", livepatch::specs()),
("Multipass", multipass::specs()),
]
}
fn table_text(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('|', "\\|")
.replace('*', "\\*")
.replace('[', "\\[")
.replace(']', "\\]")
.replace('<', "<")
.replace('>', ">")
.replace('\n', " ")
}
fn code_cell(s: &str) -> String {
format!(
"`{}`",
s.replace('`', "'")
.replace('\\', "\\\\")
.replace('|', "\\|")
)
}
fn command(m: &ActionMechanism) -> String {
match m {
ActionMechanism::Command { program, args } => {
let mut parts = vec![(*program).to_string()];
parts.extend(args.iter().cloned());
code_cell(&parts.join(" "))
}
ActionMechanism::FileScan { path } => format!("scan {}", code_cell(path)),
ActionMechanism::FileWrite { path, .. } => format!("write {}", code_cell(path)),
ActionMechanism::FilePatch { path, .. } => format!("patch {}", code_cell(path)),
ActionMechanism::FileDelete { path } => format!("delete {}", code_cell(path)),
}
}
fn distro(name: &str) -> &'static str {
if FEDORA_ONLY_ACTIONS.contains(&name) {
"Fedora"
} else if DEBIAN_ONLY_ACTIONS.contains(&name) {
"Ubuntu"
} else {
"All"
}
}
fn build_reference() -> String {
let descriptions: BTreeMap<&str, &str> = KNOWN_ACTIONS.iter().copied().collect();
let mut out = String::new();
out.push_str("# Action reference\n\n");
out.push_str(
"**This file is generated. Do not edit by hand.**\n\
Regenerate with `UPDATE_ACTION_REFERENCE=1 cargo test -p sysknife-daemon \
--test action_reference_doc`; a plain `cargo test` fails if it drifts from \
the catalogue.\n\n\
Every row is derived from the live code: the command from each action's \
`ActionSpec` mechanism, the risk from its `risk_level`, the distro from \
`sysknife-core::action_family`, and the description from the brain's \
`KNOWN_ACTIONS` list. **Distro** is `All` (cross-distro), `Ubuntu` \
(Debian-family only), or `Fedora` (atomic-host only). **Rb** = requires \
reboot; **Ro** = automatic rollback available.\n\n",
);
let mut total = 0usize;
for (title, specs) in sections() {
out.push_str(&format!("## {title}\n\n"));
out.push_str("| Action | Command | Risk | Distro | Rb | Ro | Description |\n");
out.push_str("|---|---|---|---|---|---|---|\n");
for spec in &specs {
total += 1;
let desc = descriptions.get(spec.action_name).copied().unwrap_or("");
out.push_str(&format!(
"| `{}` | {} | {:?} | {} | {} | {} | {} |\n",
spec.action_name,
command(&spec.mechanism),
spec.risk_level,
distro(spec.action_name),
if spec.reboot_required { "✓" } else { "–" },
if spec.rollback_available {
"✓"
} else {
"–"
},
table_text(desc),
));
}
out.push('\n');
}
out.push_str(&format!(
"---\n\n_{total} actions have an `ActionSpec` and are tabled above. The \
full catalogue (`KNOWN_ACTION_NAMES`) also includes `ListJobHistory`, \
which the dispatcher handles before the executor, for **{}** total._\n",
total + 1
));
out
}
fn doc_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../docs/action-reference.md")
}
#[test]
fn action_reference_doc_is_current() {
let generated = build_reference();
let path = doc_path();
if std::env::var("UPDATE_ACTION_REFERENCE").is_ok() {
std::fs::write(&path, &generated).expect("write action-reference.md");
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_default();
assert_eq!(
committed, generated,
"docs/action-reference.md is out of date with the action catalogue. \
Regenerate: UPDATE_ACTION_REFERENCE=1 cargo test -p sysknife-daemon \
--test action_reference_doc"
);
}