Skip to main content

sd_switch/
lib.rs

1use crate::systemd::JobSet;
2use crate::systemd::UnitManager;
3mod error;
4mod i18n_lib;
5pub mod systemd;
6mod unit_file;
7
8use i18n_lib::MSG;
9use std::collections::BTreeMap;
10use std::{
11    collections::HashSet,
12    path::{Path, PathBuf},
13    rc::Rc,
14    time::Duration,
15};
16use systemd::UnitStatus;
17use unit_file::UnitFile;
18
19use anyhow::{Context, Result};
20
21fn pretty_unit_names<I>(unit_names: I) -> String
22where
23    I: IntoIterator,
24    I::Item: AsRef<str>,
25{
26    let mut str_vec = unit_names
27        .into_iter()
28        .map(|s| String::from(s.as_ref()))
29        .collect::<Vec<_>>();
30    str_vec.sort();
31    str_vec.join(", ")
32}
33
34fn is_unit_available(unit_path: &Path) -> bool {
35    unit_path.exists()
36        && !unit_path
37            .canonicalize()
38            .map(|p| p == Path::new("/dev/null"))
39            .unwrap_or(true)
40}
41
42/// Given an active unit name this returns the actual unit file name. In the
43/// case of a parameterized unit, e.g., `foo@bar.service` this function returns
44/// `foo@.service`. For nonparameterized unit names it returns none.
45fn parameterized_base_name(unit_name: &str) -> Option<String> {
46    let res = unit_name.splitn(2, '@').collect::<Vec<_>>();
47    match res[..] {
48        [base_name, arg_and_suffix] => {
49            let res = arg_and_suffix.rsplitn(2, '.').collect::<Vec<_>>();
50            match res[..] {
51                [suffix, arg] if !arg.is_empty() => Some(format!("{base_name}@.{suffix}")),
52                _ => None,
53            }
54        }
55        _ => None,
56    }
57}
58
59/// Returns the file path of the given unit name within the given directory.
60///
61/// If no matching file is found then `None` is returned.
62///
63/// If the given unit name is a parameterized named then an exactly matching
64/// file is returned, if it exists, otherwise the template file path is
65/// returned.
66fn find_unit_file_path(unit_directory: &Path, unit_name: &str) -> Option<PathBuf> {
67    Some(unit_directory.join(unit_name))
68        .filter(|e| is_unit_available(e))
69        .or_else(|| {
70            parameterized_base_name(unit_name)
71                .map(|n| unit_directory.join(n))
72                .filter(|e| is_unit_available(e))
73        })
74}
75
76/// A plan of unit actions needed to accomplish the switch.
77struct SwitchPlan {
78    unit_plan: BTreeMap<Rc<str>, UnitPlan>,
79}
80
81impl SwitchPlan {
82    fn build_unit_plan(
83        &'_ mut self,
84        name: Rc<str>,
85        populate_decisions: bool,
86    ) -> UnitPlanBuilder<'_> {
87        UnitPlanBuilder {
88            switch_plan: self,
89            populate_decisions,
90            name,
91            decisions: Vec::new(),
92        }
93    }
94
95    fn stop_units(&self) -> BTreeMap<&str, &UnitPlan> {
96        self.units_with_action(|a| *a == UnitAction::Stop || *a == UnitAction::StopStart)
97    }
98
99    fn start_units(&self) -> BTreeMap<&str, &UnitPlan> {
100        self.units_with_action(|a| *a == UnitAction::Start || *a == UnitAction::StopStart)
101    }
102
103    fn reload_units(&self) -> BTreeMap<&str, &UnitPlan> {
104        self.units_with_action(|a| *a == UnitAction::Reload)
105    }
106
107    fn restart_units(&self) -> BTreeMap<&str, &UnitPlan> {
108        self.units_with_action(|a| *a == UnitAction::Restart)
109    }
110
111    fn keep_old_units(&self) -> BTreeMap<&str, &UnitPlan> {
112        self.units_with_action(|a| *a == UnitAction::KeepOld)
113    }
114
115    fn unchanged_units(&self) -> BTreeMap<&str, &UnitPlan> {
116        self.units_with_action(|a| *a == UnitAction::NoAction)
117    }
118
119    fn units_with_action(
120        &self,
121        predicate: impl Fn(&UnitAction) -> bool,
122    ) -> BTreeMap<&str, &UnitPlan> {
123        self.unit_plan
124            .iter()
125            .filter(|(_, v)| predicate(&v.action))
126            .map(|(k, v)| (k.as_ref(), v))
127            .collect()
128    }
129}
130
131/// The plan for a given unit. The plan consists of the action to perform plus a
132/// chain of decisions why that action was chosen. Since populating the the
133/// decision chain is pretty expensive, it is only actually populated when
134/// specifically needed such as when verbose output is desired.
135struct UnitPlan {
136    action: UnitAction,
137    /// The decision trace showing the decision taken to arrive at the given
138    /// action.
139    decisions: Vec<UnitDecision>,
140}
141
142struct UnitPlanBuilder<'a> {
143    switch_plan: &'a mut SwitchPlan,
144    populate_decisions: bool,
145    name: Rc<str>,
146    decisions: Vec<UnitDecision>,
147}
148
149impl<'a> UnitPlanBuilder<'a> {
150    fn push_decision(mut self, decision: UnitDecision) -> Self {
151        if self.populate_decisions {
152            self.decisions.push(decision);
153        }
154        self
155    }
156
157    fn action(self, action: UnitAction) {
158        self.switch_plan.unit_plan.insert(
159            self.name,
160            UnitPlan {
161                action,
162                decisions: self.decisions,
163            },
164        );
165    }
166}
167
168/// The available actions for a given unit.
169#[derive(Debug, PartialEq, Eq)]
170enum UnitAction {
171    NoAction,
172    StopStart,
173    Start,
174    Stop,
175    Restart,
176    Reload,
177    KeepOld,
178}
179
180/// A description of why a given action was chosen for a unit.
181#[derive(Debug, Copy, Clone, PartialEq, Eq)]
182enum UnitDecision {
183    /// Whether the new unit exists.
184    NewExists,
185    /// Whether the old unit exists.
186    OldExists,
187    /// There is no old unit but there is an active unit with the same name does.
188    OldNotExists,
189    /// New and old units are equal using restart semantics.
190    RestartEq,
191    /// New and old units are equal using reload semantics.
192    ReloadEq,
193    /// The new unit is having a special target unit.
194    UnitType(unit_file::UnitType),
195    /// The new unit has a special switch method.
196    SwitchMethod(unit_file::UnitSwitchMethod),
197    /// The active unit refuse manual stop.
198    ActiveRefuseManualStop,
199    /// Wanted by active target.
200    WantedByActiveTarget,
201}
202
203struct UnitWithTarget {
204    unit_path: PathBuf,
205    unit_name: Rc<str>,
206    target_name: Rc<str>,
207}
208
209fn build_switch_plan(
210    old_dir: Option<&Path>,
211    new_dir: &Path,
212    populate_decisions: bool,
213    service_manager: &impl systemd::ServiceManager,
214) -> Result<SwitchPlan> {
215    let mut switch_plan = SwitchPlan {
216        unit_plan: BTreeMap::new(),
217    };
218
219    let mut active_unit_names = HashSet::new();
220
221    let active_units = service_manager
222        .list_units_by_states(&["active", "activating"])
223        .with_context(|| MSG.err_listing_active_units())?;
224
225    // Handle units that are currently active, typically this implies restarting
226    // the unit in some way.
227    for active_unit in active_units {
228        let new_unit_path_opt = find_unit_file_path(new_dir, active_unit.name());
229        let old_unit_path_opt = old_dir
230            .as_ref()
231            .and_then(|d| find_unit_file_path(d, active_unit.name()));
232
233        let active_unit_name: Rc<str> = active_unit.name().into();
234        active_unit_names.insert(active_unit_name.clone());
235
236        let mut upb = switch_plan.build_unit_plan(active_unit_name, populate_decisions);
237
238        if let Some(new_unit_path) = new_unit_path_opt {
239            let new_unit_file = UnitFile::load(&new_unit_path)
240                .with_context(|| MSG.err_read_unit_file(&new_unit_path))?;
241
242            upb = upb.push_decision(UnitDecision::NewExists);
243
244            if let Some(old_unit_path) = old_unit_path_opt {
245                let old_unit_file = UnitFile::load(&old_unit_path)
246                    .with_context(|| MSG.err_read_unit_file(&old_unit_path))?;
247
248                upb = upb.push_decision(UnitDecision::OldExists);
249
250                if old_unit_file.restart_eq(&new_unit_file) {
251                    upb.push_decision(UnitDecision::RestartEq)
252                        .action(UnitAction::NoAction);
253                } else if old_unit_file.reload_eq(&new_unit_file) {
254                    upb.push_decision(UnitDecision::ReloadEq)
255                        .action(UnitAction::Reload);
256                } else if new_unit_file.unit_type() == unit_file::UnitType::Target {
257                    upb = upb.push_decision(UnitDecision::UnitType(new_unit_file.unit_type()));
258
259                    if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
260                        upb.push_decision(UnitDecision::SwitchMethod(
261                            new_unit_file.switch_method(),
262                        ))
263                        .action(UnitAction::KeepOld);
264                    } else {
265                        upb.action(UnitAction::Start);
266                    }
267                } else {
268                    upb = upb
269                        .push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()));
270
271                    match new_unit_file.switch_method() {
272                        unit_file::UnitSwitchMethod::Reload => {
273                            upb.action(UnitAction::Reload);
274                        }
275                        unit_file::UnitSwitchMethod::Restart => {
276                            upb.action(UnitAction::Restart);
277                        }
278                        unit_file::UnitSwitchMethod::StopStart => {
279                            if service_manager
280                                .unit_manager(&active_unit)?
281                                .refuse_manual_stop()?
282                            {
283                                upb.push_decision(UnitDecision::ActiveRefuseManualStop)
284                                    .action(UnitAction::NoAction);
285                            } else {
286                                upb.action(UnitAction::StopStart);
287                            }
288                        }
289                        unit_file::UnitSwitchMethod::StopOnly => {
290                            if service_manager
291                                .unit_manager(&active_unit)?
292                                .refuse_manual_stop()?
293                            {
294                                upb.push_decision(UnitDecision::ActiveRefuseManualStop)
295                                    .action(UnitAction::NoAction);
296                            } else {
297                                upb.action(UnitAction::Stop);
298                            }
299                        }
300                        unit_file::UnitSwitchMethod::KeepOld => {
301                            upb.action(UnitAction::KeepOld);
302                        }
303                    }
304                }
305            } else {
306                upb = upb.push_decision(UnitDecision::OldNotExists);
307
308                if service_manager
309                    .unit_manager(&active_unit)?
310                    .refuse_manual_stop()?
311                {
312                    upb.push_decision(UnitDecision::ActiveRefuseManualStop)
313                        .action(UnitAction::KeepOld);
314                } else if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
315                    upb.push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()))
316                        .action(UnitAction::KeepOld);
317                } else {
318                    upb.action(UnitAction::StopStart);
319                }
320            }
321        } else if old_unit_path_opt.is_some() {
322            upb = upb.push_decision(UnitDecision::OldExists);
323
324            if service_manager
325                .unit_manager(&active_unit)?
326                .refuse_manual_stop()?
327            {
328                upb.push_decision(UnitDecision::ActiveRefuseManualStop)
329                    .action(UnitAction::KeepOld);
330            } else {
331                upb.action(UnitAction::Stop);
332            }
333        }
334    }
335
336    // Handle units that are not currently active but are wanted by an active
337    // target. Typically this is simply a matter of starting the new unit.
338    for wanted_unit in find_wanted_units(new_dir)? {
339        // Skip if the unit is not wanted by an active target.
340        if !active_unit_names.contains(&wanted_unit.target_name) {
341            continue;
342        }
343
344        // Skip if the unit is actually active.
345        if active_unit_names.contains(&wanted_unit.unit_name) {
346            continue;
347        }
348
349        let new_unit_file = UnitFile::load(&wanted_unit.unit_path)
350            .with_context(|| MSG.err_read_unit_file(&wanted_unit.unit_path))?;
351
352        let mut upb = switch_plan.build_unit_plan(wanted_unit.unit_name, populate_decisions);
353
354        upb = upb
355            .push_decision(UnitDecision::NewExists)
356            .push_decision(UnitDecision::WantedByActiveTarget);
357
358        if new_unit_file.switch_method() == unit_file::UnitSwitchMethod::StopOnly {
359            upb.push_decision(UnitDecision::SwitchMethod(new_unit_file.switch_method()))
360                .action(UnitAction::NoAction);
361        } else {
362            upb.action(UnitAction::Start);
363        }
364    }
365
366    Ok(switch_plan)
367}
368
369fn find_wanted_units(new_dir: &Path) -> Result<Vec<UnitWithTarget>> {
370    let mut result = Vec::new();
371
372    for dir_entry in std::fs::read_dir(new_dir)? {
373        let dir_entry = dir_entry.with_context(|| MSG.err_read_dir_entry(new_dir))?;
374
375        // Get the file name as a string. Ignore the string if we cannot parse it.
376        let entry_file_name = dir_entry
377            .file_name()
378            .into_string()
379            .expect("unit with valid Unicode file name");
380
381        if dir_entry.metadata()?.is_dir() && entry_file_name.ends_with(".target.wants") {
382            let dir_name = entry_file_name;
383            let target_name: Rc<str> = dir_name
384                .strip_suffix(".wants")
385                .expect("directory name should end in .wants")
386                .into();
387            for wants_entry in std::fs::read_dir(dir_entry.path())? {
388                let wants_entry = wants_entry
389                    .with_context(|| MSG.err_read_dir_entry(dir_entry.path().as_path()))?;
390
391                let unit_name = wants_entry
392                    .file_name()
393                    .into_string()
394                    .expect("unit with valid Unicode file name")
395                    .into();
396                result.push(UnitWithTarget {
397                    unit_path: wants_entry.path(),
398                    unit_name,
399                    target_name: target_name.clone(),
400                });
401            }
402        }
403    }
404
405    Ok(result)
406}
407
408fn exec_pre_reload<F>(
409    plan: &SwitchPlan,
410    service_manager: &impl systemd::ServiceManager,
411    job_handler: F,
412    dry_run: bool,
413    timeout: Duration,
414) -> Result<()>
415where
416    F: Fn(&str, &str) + Send + 'static,
417{
418    let stop_units = plan.stop_units();
419
420    if stop_units.is_empty() {
421        return Ok(());
422    }
423
424    println!(
425        "{}",
426        MSG.stopping_units(&pretty_unit_names(stop_units.keys()))
427    );
428
429    if !dry_run {
430        let mut job_set = service_manager.new_job_set()?;
431
432        for uf in stop_units.keys() {
433            job_set
434                .stop_unit(uf)
435                .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Stop))?;
436        }
437
438        job_set.wait_for_all(job_handler, timeout)?;
439    }
440
441    Ok(())
442}
443
444fn exec_reload(
445    service_manager: &impl systemd::ServiceManager,
446    dry_run: bool,
447    verbose: bool,
448) -> Result<()> {
449    if !dry_run {
450        if verbose {
451            println!("{}", MSG.resetting_failed_units());
452        }
453        service_manager
454            .reset_failed()
455            .with_context(|| MSG.err_resetting_failed_units())?;
456
457        if verbose {
458            println!("{}", MSG.reloading_systemd());
459        }
460        service_manager
461            .daemon_reload()
462            .with_context(|| MSG.err_reloading_systemd())?;
463    }
464
465    Ok(())
466}
467
468fn exec_post_reload<F>(
469    plan: &SwitchPlan,
470    service_manager: &impl systemd::ServiceManager,
471    job_handler: F,
472    dry_run: bool,
473    verbose: bool,
474    timeout: Duration,
475) -> Result<()>
476where
477    F: Fn(&str, &str) + Send + 'static,
478{
479    let mut job_set = service_manager.new_job_set()?;
480
481    {
482        let units = plan.reload_units();
483        if !units.is_empty() {
484            println!("{}", MSG.reloading_units(&pretty_unit_names(units.keys())));
485
486            if !dry_run {
487                for uf in units.keys() {
488                    job_set
489                        .reload_unit(uf)
490                        .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Reload))?;
491                }
492            }
493        }
494    }
495
496    {
497        let units = plan.restart_units();
498        if !units.is_empty() {
499            println!("{}", MSG.restarting_units(&pretty_unit_names(units.keys())));
500
501            if !dry_run {
502                for uf in units.keys() {
503                    job_set
504                        .restart_unit(uf)
505                        .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Restart))?;
506                }
507            }
508        }
509    }
510
511    {
512        let units = plan.keep_old_units();
513        if !units.is_empty() {
514            println!(
515                "{}",
516                MSG.keeping_old_units(&pretty_unit_names(units.keys()))
517            );
518        }
519    }
520
521    if verbose {
522        let units = plan.unchanged_units();
523        if !units.is_empty() {
524            println!("{}", MSG.unchanged_units(&pretty_unit_names(units.keys())));
525        }
526    }
527
528    {
529        let units = plan.start_units();
530        if !units.is_empty() {
531            println!("{}", MSG.starting_units(&pretty_unit_names(units.keys())));
532
533            if !dry_run {
534                for uf in units.keys() {
535                    job_set
536                        .start_unit(uf)
537                        .with_context(|| MSG.err_unit_action_failed(uf, UnitAction::Start))?;
538                }
539            }
540        }
541    }
542
543    job_set.wait_for_all(job_handler, timeout)?;
544
545    Ok(())
546}
547
548fn print_plan(plan: &SwitchPlan) {
549    if plan.unit_plan.is_empty() {
550        println!("The calculated switch plan is empty.");
551        return;
552    }
553
554    println!("Unit switch plan:");
555
556    for (unit_file, unit_plan) in &plan.unit_plan {
557        let action = match unit_plan.action {
558            UnitAction::NoAction => "No action",
559            UnitAction::StopStart => "Stop/Start",
560            UnitAction::Start => "Start",
561            UnitAction::Stop => "Stop",
562            UnitAction::Restart => "Restart",
563            UnitAction::Reload => "Reload",
564            UnitAction::KeepOld => "Keep active",
565        };
566        let decisions = pretty_unit_names(unit_plan.decisions.iter().map(|v| format!("{:?}", v)));
567        println!("  {action} {unit_file} ({decisions})");
568    }
569}
570
571/// Performs a systemd unit "switch".
572pub fn switch(
573    service_manager: &impl systemd::ServiceManager,
574    old_dir: Option<&Path>,
575    new_dir: &Path,
576    dry_run: bool,
577    verbose: bool,
578    timeout: Duration,
579) -> Result<()> {
580    let system_status = service_manager.system_status()?;
581
582    // If the systemd manager status is degraded then inform the user about the
583    // failed units. We will still attempt to perform the switch, though.
584    if matches!(system_status, systemd::SystemStatus::Degraded) {
585        let units_by_states = service_manager.list_units_by_states(&["failed"])?;
586        let failed: Vec<&str> = units_by_states.iter().map(|status| status.name()).collect();
587        let failed = failed.join(", ");
588        eprintln!(
589            "The service manager is degraded.\n\
590             Failed services: {failed}\n\
591             Attempting to continue anyway..."
592        );
593    }
594
595    let do_switch = {
596        use systemd::SystemStatus::{
597            Degraded, Initializing, Maintenance, Running, Starting, Stopping,
598        };
599        match system_status {
600            Initializing | Starting | Running | Degraded => true,
601            Maintenance | Stopping => false,
602        }
603    };
604
605    if !do_switch {
606        if verbose {
607            println!("Skipping switch since systemd has {system_status} status");
608        }
609        return Ok(());
610    }
611
612    let plan = build_switch_plan(old_dir, new_dir, verbose, service_manager)
613        .context("Failed to build switch plan")?;
614
615    if verbose {
616        print_plan(&plan);
617    }
618
619    let job_handler = move |name: &str, state: &str| {
620        if verbose || state != "done" {
621            println!("{name} {state}");
622        }
623    };
624
625    exec_pre_reload(&plan, service_manager, job_handler, dry_run, timeout)
626        .context("Failed to perform pre-reload tasks")?;
627
628    exec_reload(service_manager, dry_run, verbose)?;
629
630    exec_post_reload(
631        &plan,
632        service_manager,
633        job_handler,
634        dry_run,
635        verbose,
636        timeout,
637    )
638    .context("Failed to perform post-reload tasks")?;
639
640    Ok(())
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    #[test]
648    fn can_get_base_name_for_parameterized_unit() {
649        assert_eq!(
650            parameterized_base_name("foo@bar.service"),
651            Some(String::from("foo@.service"))
652        );
653        assert_eq!(
654            parameterized_base_name("foo@bar.baz.service"),
655            Some(String::from("foo@.service"))
656        );
657    }
658
659    #[test]
660    fn no_base_name_for_nonparameterized_units() {
661        assert_eq!(parameterized_base_name("foo@.service"), None);
662        assert_eq!(parameterized_base_name("foo.service"), None);
663        assert_eq!(parameterized_base_name("foo@barservice"), None);
664    }
665}