diff options
author | Rina Fujino <18257209+rina23q@users.noreply.github.com> | 2022-01-10 17:57:41 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-10 17:57:41 +0100 |
commit | 3ab4cf779ebacafd5990285d07ccfe03bebaea03 (patch) | |
tree | ae8f06bc774a39f2575abf933cb92a04fc710a79 /crates/core/tedge/src/system_services/managers/general_manager.rs | |
parent | 9c4dee566f25a7a3a2b2548a03d0c4a4bf65b66e (diff) |
#639 Remove the dependency on systemd (#729)
* The purpose of this change is to get rid of the hard-coded dependencies on systemd from tedge connect/disconnect, to allow users to use other system managers, e.g. OpenRC, initd, etc..
* If /etc/tedge/system.toml exists, tedge connect/disconnect uses the service manager defined in the file.
* If the file is not given by user, tedge connect/disconnect uses /bin/systemctl as the service manager. (the same behaviour as we have it so far)
* Delete old service implementation files for BSD, OpenRC, systemd, and NULL.
* Add system.toml example files for BSD and OpenRC.
* Add a reference guide to explain the format of system.toml configuration file.
Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
Diffstat (limited to 'crates/core/tedge/src/system_services/managers/general_manager.rs')
-rw-r--r-- | crates/core/tedge/src/system_services/managers/general_manager.rs | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/crates/core/tedge/src/system_services/managers/general_manager.rs b/crates/core/tedge/src/system_services/managers/general_manager.rs new file mode 100644 index 00000000..fad556e3 --- /dev/null +++ b/crates/core/tedge/src/system_services/managers/general_manager.rs @@ -0,0 +1,401 @@ +use crate::system_services::{ + CommandBuilder, InitConfig, SystemConfig, SystemService, SystemServiceError, + SystemServiceManager, SERVICE_CONFIG_FILE, +}; +use std::fmt; +use std::path::PathBuf; +use std::process::ExitStatus; +use tedge_users::{UserManager, ROOT_USER}; + +#[derive(Debug)] +pub struct GeneralServiceManager { + user_manager: UserManager, + init_config: InitConfig, + config_path: String, +} + +impl GeneralServiceManager { + pub fn try_new( + user_manager: UserManager, + config_root: PathBuf, + ) -> Result<Self, SystemServiceError> { + let init_config = SystemConfig::try_new(config_root.clone())?.init; + let config_path = config_root + .join(SERVICE_CONFIG_FILE) + .to_str() + .unwrap_or(SERVICE_CONFIG_FILE) + .to_string(); + Ok(Self { + user_manager, + init_config, + config_path, + }) + } +} + +impl SystemServiceManager for GeneralServiceManager { + fn name(&self) -> &str { + &self.init_config.name + } + + fn check_operational(&self) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::CheckManager.try_exec_command(self)?; + + match exec_command.to_command().status() { + Ok(status) if status.success() => Ok(()), + _ => Err(SystemServiceError::ServiceManagerUnavailable { + cmd: exec_command.to_string(), + name: self.name().to_string(), + }), + } + } + + fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Stop(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Restart(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Enable(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Disable(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn is_service_running(&self, service: SystemService) -> Result<bool, SystemServiceError> { + let exec_command = ServiceCommand::IsActive(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str()) + .map(|status| status.success()) + } +} + +#[derive(Debug, PartialEq)] +struct ExecCommand { + exec: String, + args: Vec<String>, +} + +impl ExecCommand { + fn try_new( + config: Vec<String>, + cmd: ServiceCommand, + config_path: String, + ) -> Result<Self, SystemServiceError> { + match config.split_first() { + Some((exec, args)) => Ok(Self { + exec: exec.to_string(), + args: args.to_vec(), + }), + None => Err(SystemServiceError::SystemConfigInvalidSyntax { + reason: "Requires 1 or more arguments.".to_string(), + cmd: cmd.to_string(), + path: config_path, + }), + } + } + + fn try_new_with_placeholder( + config: Vec<String>, + service_cmd: ServiceCommand, + config_path: String, + service: SystemService, + ) -> Result<Self, SystemServiceError> { + let replaced = + replace_with_service_name(&config, service_cmd, config_path.as_str(), service)?; + Self::try_new(replaced, service_cmd, config_path) + } + + fn to_command(&self) -> std::process::Command { + CommandBuilder::new(&self.exec) + .args(&self.args) + .silent() + .build() + } +} + +impl fmt::Display for ExecCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.args.is_empty() { + write!(f, "{}", self.exec) + } else { + let mut s = self.exec.to_owned(); + for arg in &self.args { + s = format!("{} {}", s, arg); + } + write!(f, "{}", s) + } + } +} + +fn replace_with_service_name( + input_args: &[String], + service_cmd: ServiceCommand, + config_path: &str, + service: SystemService, +) -> Result<Vec<String>, SystemServiceError> { + if !input_args.iter().any(|s| s == "{}") { + return Err(SystemServiceError::SystemConfigInvalidSyntax { + reason: "A placeholder '{}' is missing.".to_string(), + cmd: service_cmd.to_string(), + path: config_path.to_string(), + }); + } + + let mut args = input_args.to_owned(); + for item in args.iter_mut() { + if item == "{}" { + *item = SystemService::as_service_name(service).to_string(); + } + } + + Ok(args) +} + +#[derive(Debug, Copy, Clone)] +enum ServiceCommand { + CheckManager, + Stop(SystemService), + Restart(SystemService), + Enable(SystemService), + Disable(SystemService), + IsActive(SystemService), +} + +impl ServiceCommand { + fn try_exec_command( + &self, + service_manager: &GeneralServiceManager, + ) -> Result<ExecCommand, SystemServiceError> { + let config_path = service_manager.config_path.clone(); + match self { + Self::CheckManager => ExecCommand::try_new( + service_manager.init_config.is_available.clone(), + ServiceCommand::CheckManager, + config_path, + ), + Self::Stop(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.stop.clone(), + ServiceCommand::Stop(*service), + config_path, + *service, + ), + Self::Restart(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.restart.clone(), + ServiceCommand::Restart(*service), + config_path, + *service, + ), + Self::Enable(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.enable.clone(), + ServiceCommand::Enable(*service), + config_path, + *service, + ), + Self::Disable(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.disable.clone(), + ServiceCommand::Disable(*service), + config_path, + *service, + ), + Self::IsActive(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.is_active.clone(), + ServiceCommand::IsActive(*service), + config_path, + *service, + ), + } + } +} + +impl fmt::Display for ServiceCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::CheckManager => write!(f, "is_available"), + Self::Stop(_service) => write!(f, "stop"), + Self::Restart(_service) => write!(f, "restart"), + Self::Enable(_service) => write!(f, "enable"), + Self::Disable(_service) => write!(f, "disable"), + Self::IsActive(_service) => write!(f, "is_active"), + } + } +} + +impl GeneralServiceManager { + fn run_service_command_as_root( + &self, + exec_command: ExecCommand, + config_path: &str, + ) -> Result<ServiceCommandExitStatus, SystemServiceError> { + let _root_guard = self.user_manager.become_user(ROOT_USER); + + exec_command + .to_command() + .status() + .map_err(|_| SystemServiceError::ServiceCommandNotFound { + service_command: exec_command.to_string(), + path: config_path.to_string(), + }) + .map(|status| ServiceCommandExitStatus { + status, + service_command: exec_command.to_string(), + }) + } +} + +#[derive(Debug)] +struct ServiceCommandExitStatus { + status: ExitStatus, + service_command: String, +} + +impl ServiceCommandExitStatus { + fn must_succeed(self) -> Result<(), SystemServiceError> { + if self.status.success() { + Ok(()) + } else { + match self.status.code() { + Some(code) => Err(SystemServiceError::ServiceCommandFailedWithCode { + service_command: self.service_command, + code, + }), + None => Err(SystemServiceError::ServiceCommandFailedBySignal { + service_command: self.service_command, + }), + } + } + } + + fn success(self) -> bool { + self.status.success() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::*; + use test_case::test_case; + + #[test_case( + vec!["bin".to_string(), "{}".to_string(), "arg2".to_string()], + vec!["bin".to_string(), "mosquitto".to_string(), "arg2".to_string()] + ;"one placeholder")] + #[test_case( + vec!["bin".to_string(), "{}".to_string(), "{}".to_string()], + vec!["bin".to_string(), "mosquitto".to_string(), "mosquitto".to_string()] + ;"several placeholders")] + fn replace_placeholder_with_service(input: Vec<String>, expected_output: Vec<String>) { + let replaced_config = replace_with_service_name( + &input, + ServiceCommand::Stop(SystemService::Mosquitto), + "/dummy/path.toml", + SystemService::Mosquitto, + ) + .unwrap(); + assert_eq!(replaced_config, expected_output) + } + + #[test] + fn fail_to_replace_placeholder_with_service() { + let input = vec!["bin".to_string(), "arg1".to_string(), "arg2".to_string()]; + let system_config_error = replace_with_service_name( + &input, + ServiceCommand::Stop(SystemService::Mosquitto), + "dummy/path.toml", + SystemService::Mosquitto, + ) + .unwrap_err(); + assert_matches!( + system_config_error, + SystemServiceError::SystemConfigInvalidSyntax { .. } + ) + } + + #[test_case( + vec!["bin".to_string(), "arg1".to_string(), "arg2".to_string()], + ExecCommand { + exec: "bin".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()] + } + ;"with arguments")] + #[test_case( + vec!["bin".to_string()], + ExecCommand { + exec: "bin".to_string(), + args: vec![] + } + ;"only executable")] + fn build_exec_command(config: Vec<String>, expected: ExecCommand) { + let exec_command = ExecCommand::try_new( + config, + ServiceCommand::Stop(SystemService::Mosquitto), + "test/dummy.toml".to_string(), + ) + .unwrap(); + assert_eq!(exec_command, expected); + } + + #[test] + fn fail_to_build_exec_command() { + let config = vec![]; + let system_config_error = ExecCommand::try_new( + config, + ServiceCommand::Stop(SystemService::Mosquitto), + "test/dummy.toml".to_string(), + ) + .unwrap_err(); + assert_matches!( + system_config_error, + SystemServiceError::SystemConfigInvalidSyntax { .. } + ); + } + + #[test_case( + ExecCommand { + exec: "bin".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()] + }, + r#""bin" "arg1" "arg2""# + ;"with arguments")] + #[test_case( + ExecCommand { + exec: "bin".to_string(), + args: vec![] + }, + r#""bin""# + ;"only executable")] + fn construct_command(exec_command: ExecCommand, expected: &str) { + let command = exec_command.to_command(); + assert_eq!(format!("{:?}", command), expected); + } + + #[test_case( + ExecCommand { + exec: "bin".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()] + }, + "bin arg1 arg2" + ;"with arguments")] + #[test_case( + ExecCommand { + exec: "bin".to_string(), + args: vec![] + }, + "bin" + ;"only executable")] + fn print_exec_command(exec_command: ExecCommand, expected: &str) { + assert_eq!(exec_command.to_string(), expected) + } +} |