1use serde::Serialize;
4
5pub mod help;
6
7#[derive(Serialize, Clone)]
9pub struct Input {
10 pub name: String,
12 #[serde(rename = "type")]
14 pub ty: String,
15 pub required: bool,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub default: Option<String>,
20}
21
22#[derive(Serialize, Clone)]
24pub struct Descriptor {
25 pub id: String,
27 pub summary: String,
29 pub long: String,
31 pub privileged: bool,
33 pub output_kind: String,
35 pub inputs: Vec<Input>,
37 pub flags: Vec<String>,
39 pub examples: Vec<String>,
41}
42
43fn input(name: &str, required: bool) -> Input {
44 Input {
45 name: name.into(),
46 ty: "string".into(),
47 required,
48 default: None,
49 }
50}
51
52fn mutation(
53 id: &str,
54 summary: &str,
55 long: &str,
56 output_kind: &str,
57 extra_flags: &[&str],
58) -> Descriptor {
59 let mut flags = vec![
60 "--host".to_string(),
61 "--json".to_string(),
62 "--dry-run".to_string(),
63 "--force".to_string(),
64 ];
65 flags.extend(extra_flags.iter().map(|f| f.to_string()));
66 Descriptor {
67 id: id.into(),
68 summary: summary.into(),
69 long: long.into(),
70 privileged: true,
71 output_kind: output_kind.into(),
72 inputs: vec![input("unit", true)],
73 flags,
74 examples: vec![format!("fez {} --json", id.replace('.', " "))],
75 }
76}
77
78fn enablement(id: &str, summary: &str, long: &str) -> Descriptor {
79 let verb = id.rsplit('.').next().expect("capability id has a verb");
80 Descriptor {
81 id: id.into(),
82 summary: summary.into(),
83 long: long.into(),
84 privileged: true,
85 output_kind: "ServiceEnablement".into(),
86 inputs: vec![input("unit", true)],
87 flags: vec![
88 "--host".into(),
89 "--json".into(),
90 "--dry-run".into(),
91 "--force".into(),
92 "--now".into(),
93 ],
94 examples: vec![
95 format!("fez services {verb} chronyd.service --json"),
96 format!("fez services {verb} chronyd.service --now"),
97 ],
98 }
99}
100
101pub fn registry() -> Vec<Descriptor> {
103 vec![
104 Descriptor {
105 id: "services.list".into(),
106 summary: "List systemd units".into(),
107 long: "List systemd units on the target host. Use --state to filter by \
108active state (e.g. active, failed, inactive). Read-only; never mutates."
109 .into(),
110 privileged: false,
111 output_kind: "ServiceList".into(),
112 inputs: vec![input("state", false)],
113 flags: vec!["--host".into(), "--json".into(), "--state".into()],
114 examples: vec![
115 "fez services list --state failed --json".into(),
116 "fez --host web01 services list".into(),
117 ],
118 },
119 Descriptor {
120 id: "services.status".into(),
121 summary: "Show one unit's status".into(),
122 long: "Show the current status of a single systemd unit (active state, \
123sub-state, enablement). Read-only."
124 .into(),
125 privileged: false,
126 output_kind: "ServiceStatus".into(),
127 inputs: vec![input("unit", true)],
128 flags: vec!["--host".into(), "--json".into()],
129 examples: vec!["fez services status sshd.service --json".into()],
130 },
131 Descriptor {
132 id: "services.logs".into(),
133 summary: "Read a unit's journal".into(),
134 long: "Read journal entries for a unit. Filter with --since and --priority \
135(journalctl syntax), cap with --lines, or stream with --follow. Read-only."
136 .into(),
137 privileged: false,
138 output_kind: "LogEntries".into(),
139 inputs: vec![input("unit", true)],
140 flags: vec![
141 "--host".into(),
142 "--json".into(),
143 "--since".into(),
144 "--priority".into(),
145 "--lines".into(),
146 "--follow".into(),
147 ],
148 examples: vec![
149 "fez services logs sshd.service --lines 100 --json".into(),
150 "fez services logs nginx.service --since '1 hour ago' --priority err".into(),
151 ],
152 },
153 mutation(
154 "services.start",
155 "Start a unit",
156 "Start a systemd unit immediately. Privileged. Protected units are \
157refused unless --force is supplied. Exits 8 on a protected-unit refusal.",
158 "ServiceMutation",
159 &[],
160 ),
161 mutation(
162 "services.stop",
163 "Stop a unit",
164 "Stop a running systemd unit. Privileged. Protected units are refused \
165unless --force is supplied (exit 8).",
166 "ServiceMutation",
167 &[],
168 ),
169 mutation(
170 "services.restart",
171 "Restart a unit",
172 "Restart a systemd unit. Privileged. Protected units are refused unless \
173--force is supplied (exit 8).",
174 "ServiceMutation",
175 &[],
176 ),
177 mutation(
178 "services.reload",
179 "Reload a unit's configuration",
180 "Ask a unit to reload its configuration without a full restart. \
181Privileged. Protected units are refused unless --force is supplied (exit 8).",
182 "ServiceMutation",
183 &[],
184 ),
185 enablement(
186 "services.enable",
187 "Enable a unit",
188 "Enable a unit so it starts at boot. Add --now to also start it \
189immediately. Privileged. Protected units are refused unless --force is supplied (exit 8).",
190 ),
191 enablement(
192 "services.disable",
193 "Disable a unit",
194 "Disable a unit so it no longer starts at boot. Add --now to also \
195stop it immediately. Privileged. Protected units are refused unless --force is supplied (exit 8).",
196 ),
197 ]
198}
199
200pub fn find(id: &str) -> Option<Descriptor> {
202 registry().into_iter().find(|d| d.id == id)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn every_descriptor_has_long_and_examples() {
211 for d in registry() {
212 assert!(!d.long.trim().is_empty(), "{} missing long", d.id);
213 assert!(!d.examples.is_empty(), "{} has no examples", d.id);
214 for ex in &d.examples {
215 assert!(ex.starts_with("fez "), "{}: bad example {:?}", d.id, ex);
216 }
217 }
218 }
219
220 #[test]
221 fn protected_capabilities_document_force() {
222 for d in registry() {
223 if d.privileged {
224 assert!(
225 d.long.contains("--force") || d.examples.iter().any(|e| e.contains("--force")),
226 "{}: privileged capability should mention --force",
227 d.id
228 );
229 }
230 }
231 }
232
233 #[test]
234 fn enable_disable_have_now_example() {
235 for id in ["services.enable", "services.disable"] {
236 let d = find(id).unwrap();
237 assert!(
238 d.examples.iter().any(|e| e.contains("--now")),
239 "{id}: needs --now example"
240 );
241 }
242 }
243}