use serde::Serialize;
pub mod help;
#[derive(Serialize, Clone)]
pub struct Input {
pub name: String,
#[serde(rename = "type")]
pub ty: String,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Serialize, Clone)]
pub struct Descriptor {
pub id: String,
pub summary: String,
pub long: String,
pub privileged: bool,
pub output_kind: String,
pub inputs: Vec<Input>,
pub flags: Vec<String>,
pub examples: Vec<String>,
}
fn input(name: &str, required: bool) -> Input {
Input {
name: name.into(),
ty: "string".into(),
required,
default: None,
}
}
fn mutation(
id: &str,
summary: &str,
long: &str,
output_kind: &str,
extra_flags: &[&str],
) -> Descriptor {
let mut flags = vec![
"--host".to_string(),
"--json".to_string(),
"--dry-run".to_string(),
"--force".to_string(),
];
flags.extend(extra_flags.iter().map(|f| f.to_string()));
Descriptor {
id: id.into(),
summary: summary.into(),
long: long.into(),
privileged: true,
output_kind: output_kind.into(),
inputs: vec![input("unit", true)],
flags,
examples: vec![format!("fez {} --json", id.replace('.', " "))],
}
}
pub fn registry() -> Vec<Descriptor> {
vec![
Descriptor {
id: "services.list".into(),
summary: "List systemd units".into(),
long: "List systemd units on the target host. Use --state to filter by \
active state (e.g. active, failed, inactive). Read-only; never mutates."
.into(),
privileged: false,
output_kind: "ServiceList".into(),
inputs: vec![input("state", false)],
flags: vec!["--host".into(), "--json".into(), "--state".into()],
examples: vec![
"fez services list --state failed --json".into(),
"fez --host web01 services list".into(),
],
},
Descriptor {
id: "services.status".into(),
summary: "Show one unit's status".into(),
long: "Show the current status of a single systemd unit (active state, \
sub-state, enablement). Read-only."
.into(),
privileged: false,
output_kind: "ServiceStatus".into(),
inputs: vec![input("unit", true)],
flags: vec!["--host".into(), "--json".into()],
examples: vec!["fez services status sshd.service --json".into()],
},
Descriptor {
id: "services.logs".into(),
summary: "Read a unit's journal".into(),
long: "Read journal entries for a unit. Filter with --since and --priority \
(journalctl syntax), cap with --lines, or stream with --follow. Read-only."
.into(),
privileged: false,
output_kind: "LogEntries".into(),
inputs: vec![input("unit", true)],
flags: vec![
"--host".into(),
"--json".into(),
"--since".into(),
"--priority".into(),
"--lines".into(),
"--follow".into(),
],
examples: vec![
"fez services logs sshd.service --lines 100 --json".into(),
"fez services logs nginx.service --since '1 hour ago' --priority err".into(),
],
},
mutation(
"services.start",
"Start a unit",
"Start a systemd unit immediately. Privileged. Protected units are \
refused unless --force is supplied. Exits 8 on a protected-unit refusal.",
"ServiceMutation",
&[],
),
mutation(
"services.stop",
"Stop a unit",
"Stop a running systemd unit. Privileged. Protected units are refused \
unless --force is supplied (exit 8).",
"ServiceMutation",
&[],
),
mutation(
"services.restart",
"Restart a unit",
"Restart a systemd unit. Privileged. Protected units are refused unless \
--force is supplied (exit 8).",
"ServiceMutation",
&[],
),
mutation(
"services.reload",
"Reload a unit's configuration",
"Ask a unit to reload its configuration without a full restart. \
Privileged. Protected units are refused unless --force is supplied (exit 8).",
"ServiceMutation",
&[],
),
Descriptor {
id: "services.enable".into(),
summary: "Enable a unit".into(),
long: "Enable a unit so it starts at boot. Add --now to also start it \
immediately. Privileged. Protected units are refused unless --force is supplied (exit 8)."
.into(),
privileged: true,
output_kind: "ServiceEnablement".into(),
inputs: vec![input("unit", true)],
flags: vec![
"--host".into(),
"--json".into(),
"--dry-run".into(),
"--force".into(),
"--now".into(),
],
examples: vec![
"fez services enable chronyd.service --json".into(),
"fez services enable chronyd.service --now".into(),
],
},
Descriptor {
id: "services.disable".into(),
summary: "Disable a unit".into(),
long: "Disable a unit so it no longer starts at boot. Add --now to also \
stop it immediately. Privileged. Protected units are refused unless --force is supplied (exit 8)."
.into(),
privileged: true,
output_kind: "ServiceEnablement".into(),
inputs: vec![input("unit", true)],
flags: vec![
"--host".into(),
"--json".into(),
"--dry-run".into(),
"--force".into(),
"--now".into(),
],
examples: vec![
"fez services disable chronyd.service --json".into(),
"fez services disable chronyd.service --now".into(),
],
},
]
}
pub fn find(id: &str) -> Option<Descriptor> {
registry().into_iter().find(|d| d.id == id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_descriptor_has_long_and_examples() {
for d in registry() {
assert!(!d.long.trim().is_empty(), "{} missing long", d.id);
assert!(!d.examples.is_empty(), "{} has no examples", d.id);
for ex in &d.examples {
assert!(ex.starts_with("fez "), "{}: bad example {:?}", d.id, ex);
}
}
}
#[test]
fn protected_capabilities_document_force() {
for d in registry() {
if d.privileged {
assert!(
d.long.contains("--force") || d.examples.iter().any(|e| e.contains("--force")),
"{}: privileged capability should mention --force",
d.id
);
}
}
}
#[test]
fn enable_disable_have_now_example() {
for id in ["services.enable", "services.disable"] {
let d = find(id).unwrap();
assert!(
d.examples.iter().any(|e| e.contains("--now")),
"{id}: needs --now example"
);
}
}
}