Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sources/api/thar-be-settings/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,7 @@ pub enum Error {
uri: String,
source: schnauzer::v1::Error,
},

#[snafu(display("One or more service restarts failed"))]
ServiceRestartsFailed,
}
168 changes: 108 additions & 60 deletions sources/api/thar-be-settings/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,98 @@ impl Services {
}
Self(output)
}

/// Restart all systemd units across all services with a single systemctl call.
fn restart_systemd(&self) -> Result<()> {
// Collect systemd unit names from all services' restart commands.
// e.g. "/bin/systemctl try-restart foo.service" -> "foo.service"
let units: Vec<&str> = self
.0
.values()
.flat_map(|s| s.model.restart_commands.iter())
.filter(|cmd| cmd.contains("systemctl"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below, why not starts_with if we're assuming the first word is the command?

.filter_map(|cmd| cmd.split_whitespace().last())
.collect();

if units.is_empty() {
return Ok(());
}

debug!("Restarting systemd units: {:?}", &units);
let mut cmd = Command::new("/bin/systemctl");
cmd.arg("try-reload-or-restart");
cmd.args(&units);
Comment on lines +62 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't automatically treat all systemctl ... commands as wanting systemctl try-reload-or-restart ... instead.

We should assume the restart command must be issued exactly as specified. The only optimization permitted is to group the restart commands by subcommand - all systemctl restart ... together, all systemctl try-restart ... together, and so on.

It might also be prudent to lean into the non-determinism for restarts - for example, preparing the entire batch of restart commands, then randomizing the order. There may be some brittleness from potential ordering surprises that needs to be shaken out.


let result = cmd.output().context(error::CommandExecutionFailureSnafu {
command: "systemctl try-reload-or-restart",
})?;

ensure!(
result.status.success(),
error::FailedRestartCommandSnafu {
command: format!("systemctl try-reload-or-restart {}", units.join(" ")),
stderr: String::from_utf8_lossy(&result.stderr),
}
);

Ok(())
}
}

impl Service {
/// Execute non-systemd restart commands for this service.
fn restart_non_systemd(&self) -> Result<()> {
for restart_command in &self.model.restart_commands {
// Skip systemd commands - handled by Services::restart_systemd()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of a shame we have to sort this twice

if restart_command.contains("systemctl") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want contains or starts_with?

continue;
}

// Split on space, assume the first item is the command
// and the rest are args.
debug!("Restart command: {:?}", &restart_command);
let mut command_strings = restart_command.split(' ');
let command = command_strings
.next()
.context(error::InvalidRestartCommandSnafu {
command: restart_command.as_str(),
})?;
trace!("Command: {}", &command);
trace!("Args: {:?}", &command_strings);

// Go execute the restart command
let mut process_command = Command::new(command);
process_command.args(command_strings);
if let Some(ref changed_settings) = self.changed_settings {
if !changed_settings.is_empty() {
process_command.env("CHANGED_SETTINGS", join(changed_settings, " "));
}
}
let result = process_command
.output()
.context(error::CommandExecutionFailureSnafu {
command: restart_command.as_str(),
})?;

// If the restart command exited nonzero, call it a failure
ensure!(
result.status.success(),
error::FailedRestartCommandSnafu {
command: restart_command.as_str(),
stderr: String::from_utf8_lossy(&result.stderr),
}
);
trace!(
"Command stdout: {}",
String::from_utf8_lossy(&result.stdout)
);
trace!(
"Command stderr: {}",
String::from_utf8_lossy(&result.stderr)
);
}
Ok(())
}
}

/// Returns a `Services` reflecting the set of services affected by the given changed settings in
Expand Down Expand Up @@ -133,70 +225,26 @@ where
Ok(service_map)
}

/// Call the `restart()` method on each Service in a Services object
/// Restart services, batching systemd services together and then iterating non-systemd services.
/// Does not bail early on failure; attempts all restarts and reports any failures at the end.
pub fn restart_services(services: Services) -> Result<()> {
for (name, service) in services.0 {
debug!("Checking for restart-commands for {name}");
service.restart()?;
}
Ok(())
}

/// This trait is primarily meant to extend the Service model. It uses the metadata
/// inside the Service struct to restart the service.
trait ServiceRestart {
/// Restart the service
fn restart(&self) -> Result<()>;
}
let mut restart_failed = false;

impl ServiceRestart for Service {
fn restart(&self) -> Result<()> {
let restart_commands = &self.model.restart_commands;
info!("restart commands {restart_commands:?}");
for restart_command in restart_commands {
// Split on space, assume the first item is the command
// and the rest are args.
debug!("Restart command: {:?}", &restart_command);
let mut command_strings = restart_command.split(' ');
let command = command_strings
.next()
.context(error::InvalidRestartCommandSnafu {
command: restart_command.as_str(),
})?;
trace!("Command: {}", &command);
trace!("Args: {:?}", &command_strings);

// Go execute the restart command
let mut process_command = Command::new(command);
process_command.args(command_strings);
if let Some(ref changed_settings) = self.changed_settings {
if !changed_settings.is_empty() {
process_command.env("CHANGED_SETTINGS", join(changed_settings, " "));
}
}
let result = process_command
.output()
.context(error::CommandExecutionFailureSnafu {
command: restart_command.as_str(),
})?;
if let Err(e) = services.restart_systemd() {
error!("systemctl restart failed: {}", e);
restart_failed = true;
}

// If the restart command exited nonzero, call it a failure
ensure!(
result.status.success(),
error::FailedRestartCommandSnafu {
command: restart_command.as_str(),
stderr: String::from_utf8_lossy(&result.stderr),
}
);
trace!(
"Command stdout: {}",
String::from_utf8_lossy(&result.stdout)
);
trace!(
"Command stderr: {}",
String::from_utf8_lossy(&result.stderr)
);
for (name, service) in &services.0 {
if let Err(e) = service.restart_non_systemd() {
error!("Failed to restart {}: {}", name, e);
Comment on lines +238 to +240

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems cleaner to partition Services into two sets, one for systemd and one for non-systemd. These could be represented by different types and each could have a restart method.

restart_failed = true;
}
}

if restart_failed {
error::ServiceRestartsFailedSnafu.fail()
} else {
Ok(())
}
}