Skip to main content

sysknife_daemon/actions/
deployment.rs

1//! rpm-ostree and ostree deployment actions (Fedora Atomic).
2//!
3//! `RebaseSystem` and `SetKernelArguments` take an operand and run
4//! `sudo /usr/lib/sysknife/action-steps rpm-ostree <rebase|kargs> ...`; the
5//! helper validates the target or every kernel argument again and builds the
6//! rpm-ostree argv itself. `/usr/bin/rpm-ostree kargs *` admitted
7//! `--append=init=/bin/sh` and `rebase *` any local image, past the daemon's
8//! screens. `UpdateSystem`, `RollbackDeployment` and `CleanupDeployments` take
9//! no operand and keep exact grants of their own.
10
11use super::{command_mechanism, ActionSpec};
12use sysknife_types::RiskLevel;
13
14/// The root-owned helper the operand-taking rpm-ostree actions go through; see
15/// the same constant in `layering.rs`.
16const ACTION_STEPS: &str = "/usr/lib/sysknife/action-steps";
17
18pub fn specs() -> Vec<ActionSpec> {
19    vec![
20        get_system_state(),
21        collect_diagnostics(),
22        get_deployment_history(),
23        list_deployments(),
24        update_system(),
25        pin_deployment(0),
26        unpin_deployment(0),
27        rebase_system("fedora/41/x86_64/silverblue"),
28        cleanup_deployments(),
29        reboot_system(),
30        rollback_deployment(),
31        get_kernel_arguments(),
32        set_kernel_arguments(&[], &[]),
33    ]
34}
35
36pub fn get_system_state() -> ActionSpec {
37    ActionSpec {
38        action_name: "GetSystemState",
39        mechanism: command_mechanism("rpm-ostree", ["status", "--json"]),
40        risk_level: RiskLevel::Low,
41        reboot_required: false,
42        rollback_available: false,
43    }
44}
45
46pub fn collect_diagnostics() -> ActionSpec {
47    ActionSpec {
48        action_name: "CollectDiagnostics",
49        mechanism: command_mechanism("journalctl", ["-b", "-n", "500", "--no-pager"]),
50        risk_level: RiskLevel::Low,
51        reboot_required: false,
52        rollback_available: false,
53    }
54}
55
56pub fn get_deployment_history() -> ActionSpec {
57    ActionSpec {
58        action_name: "GetDeploymentHistory",
59        mechanism: command_mechanism("rpm-ostree", ["status", "--json"]),
60        risk_level: RiskLevel::Low,
61        reboot_required: false,
62        rollback_available: false,
63    }
64}
65
66pub fn list_deployments() -> ActionSpec {
67    ActionSpec {
68        action_name: "ListDeployments",
69        mechanism: command_mechanism("rpm-ostree", ["status", "--json"]),
70        risk_level: RiskLevel::Low,
71        reboot_required: false,
72        rollback_available: false,
73    }
74}
75
76pub fn update_system() -> ActionSpec {
77    ActionSpec {
78        action_name: "UpdateSystem",
79        mechanism: command_mechanism("sudo", ["rpm-ostree", "upgrade"]),
80        risk_level: RiskLevel::High,
81        reboot_required: true,
82        rollback_available: true,
83    }
84}
85
86pub fn pin_deployment(index: u32) -> ActionSpec {
87    ActionSpec {
88        action_name: "PinDeployment",
89        mechanism: super::ActionMechanism::Command {
90            program: "sudo",
91            args: vec![
92                "ostree".to_string(),
93                "admin".to_string(),
94                "pin".to_string(),
95                index.to_string(),
96            ],
97        },
98        risk_level: RiskLevel::High,
99        reboot_required: false,
100        rollback_available: false,
101    }
102}
103
104pub fn unpin_deployment(index: u32) -> ActionSpec {
105    ActionSpec {
106        action_name: "UnpinDeployment",
107        mechanism: super::ActionMechanism::Command {
108            program: "sudo",
109            args: vec![
110                "ostree".to_string(),
111                "admin".to_string(),
112                "pin".to_string(),
113                "--unpin".to_string(),
114                index.to_string(),
115            ],
116        },
117        risk_level: RiskLevel::High,
118        reboot_required: false,
119        rollback_available: false,
120    }
121}
122
123pub fn rebase_system(target_ref: &str) -> ActionSpec {
124    ActionSpec {
125        action_name: "RebaseSystem",
126        // The helper runs `rpm-ostree rebase <target_ref>`.
127        mechanism: command_mechanism("sudo", [ACTION_STEPS, "rpm-ostree", "rebase", target_ref]),
128        risk_level: RiskLevel::High,
129        reboot_required: true,
130        rollback_available: true,
131    }
132}
133
134pub fn cleanup_deployments() -> ActionSpec {
135    ActionSpec {
136        action_name: "CleanupDeployments",
137        mechanism: command_mechanism("sudo", ["rpm-ostree", "cleanup", "--rollback", "--pending"]),
138        risk_level: RiskLevel::High,
139        reboot_required: false,
140        rollback_available: false,
141    }
142}
143
144pub fn reboot_system() -> ActionSpec {
145    ActionSpec {
146        action_name: "RebootSystem",
147        mechanism: command_mechanism("sudo", ["systemctl", "reboot"]),
148        risk_level: RiskLevel::High,
149        reboot_required: true,
150        rollback_available: false,
151    }
152}
153
154pub fn rollback_deployment() -> ActionSpec {
155    ActionSpec {
156        action_name: "RollbackDeployment",
157        mechanism: command_mechanism("sudo", ["rpm-ostree", "rollback"]),
158        risk_level: RiskLevel::High,
159        reboot_required: true,
160        rollback_available: false,
161    }
162}
163
164pub fn get_kernel_arguments() -> ActionSpec {
165    ActionSpec {
166        action_name: "GetKernelArguments",
167        mechanism: command_mechanism("rpm-ostree", ["kargs"]),
168        risk_level: RiskLevel::Low,
169        reboot_required: false,
170        rollback_available: false,
171    }
172}
173
174/// Add and remove kernel arguments. The daemon sends `append <arg>` and
175/// `delete <arg>` pairs, and the helper writes `--append=<arg>` and
176/// `--delete=<arg>` itself, so no option reaches rpm-ostree from this argv. With
177/// both lists empty the helper runs a bare `rpm-ostree kargs`, which prints them.
178pub fn set_kernel_arguments(args_to_add: &[&str], args_to_remove: &[&str]) -> ActionSpec {
179    let pairs = args_to_add
180        .iter()
181        .flat_map(|a| ["append", *a])
182        .chain(args_to_remove.iter().flat_map(|a| ["delete", *a]));
183
184    ActionSpec {
185        action_name: "SetKernelArguments",
186        mechanism: command_mechanism(
187            "sudo",
188            [ACTION_STEPS, "rpm-ostree", "kargs"]
189                .into_iter()
190                .chain(pairs),
191        ),
192        risk_level: RiskLevel::High,
193        reboot_required: true,
194        rollback_available: true,
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::actions::{exclusive_resource, ActionMechanism, ExclusiveResource};
202
203    fn argv(spec: &ActionSpec) -> (&'static str, Vec<&str>) {
204        match &spec.mechanism {
205            ActionMechanism::Command { program, args } => {
206                (*program, args.iter().map(String::as_str).collect())
207            }
208            other => panic!("{} is not a command: {other:?}", spec.action_name),
209        }
210    }
211
212    /// `/usr/bin/rpm-ostree rebase *` booted any image and `kargs *` took
213    /// `--append=init=/bin/sh`. Both go through the helper verb now, which
214    /// validates the target and every kernel argument again.
215    #[test]
216    fn rebase_and_kargs_go_through_the_helper_verb() {
217        let rebase = rebase_system("fedora:fedora/41/x86_64/silverblue");
218        let (program, args) = argv(&rebase);
219        assert_eq!(program, "sudo");
220        assert_eq!(
221            args,
222            [
223                ACTION_STEPS,
224                "rpm-ostree",
225                "rebase",
226                "fedora:fedora/41/x86_64/silverblue"
227            ]
228        );
229        let kargs = set_kernel_arguments(&["nomodeset", "quiet"], &["rhgb"]);
230        let (program, args) = argv(&kargs);
231        assert_eq!(program, "sudo");
232        assert_eq!(
233            args,
234            [
235                ACTION_STEPS,
236                "rpm-ostree",
237                "kargs",
238                "append",
239                "nomodeset",
240                "append",
241                "quiet",
242                "delete",
243                "rhgb"
244            ]
245        );
246        assert!(
247            args.iter().all(|a| !a.starts_with('-')),
248            "kargs sends an option; the helper writes --append=/--delete= itself: {args:?}"
249        );
250    }
251
252    /// The operand-free actions keep their exact argv and exact grants.
253    #[test]
254    fn fixed_rpm_ostree_actions_keep_their_argv() {
255        for (spec, expected) in [
256            (update_system(), vec!["rpm-ostree", "upgrade"]),
257            (rollback_deployment(), vec!["rpm-ostree", "rollback"]),
258            (
259                cleanup_deployments(),
260                vec!["rpm-ostree", "cleanup", "--rollback", "--pending"],
261            ),
262        ] {
263            let (program, args) = argv(&spec);
264            assert_eq!(program, "sudo", "{}", spec.action_name);
265            assert_eq!(args, expected, "{}", spec.action_name);
266        }
267    }
268
269    #[test]
270    fn deployment_actions_still_contend_for_the_rpm_ostree_lock() {
271        for spec in [
272            update_system(),
273            rebase_system("fedora"),
274            cleanup_deployments(),
275            rollback_deployment(),
276            set_kernel_arguments(&["quiet"], &[]),
277            set_kernel_arguments(&[], &[]),
278        ] {
279            assert_eq!(
280                exclusive_resource(&spec),
281                Some(ExclusiveResource::RpmOstree),
282                "{} lost the rpm-ostree lock",
283                spec.action_name
284            );
285        }
286    }
287}