From 3ab4cf779ebacafd5990285d07ccfe03bebaea03 Mon Sep 17 00:00:00 2001 From: Rina Fujino <18257209+rina23q@users.noreply.github.com> Date: Mon, 10 Jan 2022 17:57:41 +0100 Subject: #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> --- crates/core/tedge/src/cli/connect/cli.rs | 15 +- crates/core/tedge/src/cli/connect/command.rs | 4 +- crates/core/tedge/src/cli/disconnect/cli.rs | 15 +- .../tedge/src/cli/disconnect/disconnect_bridge.rs | 2 +- crates/core/tedge/src/command.rs | 3 - crates/core/tedge/src/error.rs | 5 + crates/core/tedge/src/main.rs | 15 - .../tedge/src/system_services/command_builder.rs | 8 +- crates/core/tedge/src/system_services/error.rs | 79 ++-- crates/core/tedge/src/system_services/manager.rs | 13 + .../core/tedge/src/system_services/managers/bsd.rs | 199 ---------- .../tedge/src/system_services/managers/config.rs | 138 +++++++ .../system_services/managers/general_manager.rs | 401 +++++++++++++++++++++ .../core/tedge/src/system_services/managers/mod.rs | 8 +- .../tedge/src/system_services/managers/null.rs | 35 -- .../tedge/src/system_services/managers/openrc.rs | 199 ---------- .../tedge/src/system_services/managers/systemd.rs | 217 ----------- 17 files changed, 617 insertions(+), 739 deletions(-) delete mode 100644 crates/core/tedge/src/system_services/managers/bsd.rs create mode 100644 crates/core/tedge/src/system_services/managers/config.rs create mode 100644 crates/core/tedge/src/system_services/managers/general_manager.rs delete mode 100644 crates/core/tedge/src/system_services/managers/null.rs delete mode 100644 crates/core/tedge/src/system_services/managers/openrc.rs delete mode 100644 crates/core/tedge/src/system_services/managers/systemd.rs (limited to 'crates/core/tedge') diff --git a/crates/core/tedge/src/cli/connect/cli.rs b/crates/core/tedge/src/cli/connect/cli.rs index 42e68885..1f3e2643 100644 --- a/crates/core/tedge/src/cli/connect/cli.rs +++ b/crates/core/tedge/src/cli/connect/cli.rs @@ -1,5 +1,6 @@ use crate::cli::connect::*; use crate::command::{BuildCommand, BuildContext, Command}; +use crate::system_services::service_manager; use structopt::StructOpt; #[derive(StructOpt, Debug, PartialEq)] @@ -27,20 +28,26 @@ impl BuildCommand for TEdgeConnectOpt { fn build_command(self, context: BuildContext) -> Result, crate::ConfigError> { Ok(match self { TEdgeConnectOpt::C8y { is_test_connection } => ConnectCommand { - config_location: context.config_location, + config_location: context.config_location.clone(), config_repository: context.config_repository, cloud: Cloud::C8y, common_mosquitto_config: CommonMosquittoConfig::default(), is_test_connection, - service_manager: context.service_manager.clone(), + service_manager: service_manager( + context.user_manager.clone(), + context.config_location.tedge_config_root_path, + )?, }, TEdgeConnectOpt::Az { is_test_connection } => ConnectCommand { - config_location: context.config_location, + config_location: context.config_location.clone(), config_repository: context.config_repository, cloud: Cloud::Azure, common_mosquitto_config: CommonMosquittoConfig::default(), is_test_connection, - service_manager: context.service_manager.clone(), + service_manager: service_manager( + context.user_manager.clone(), + context.config_location.tedge_config_root_path, + )?, }, } .into_boxed()) diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index fb2f9d17..262e6163 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -431,7 +431,9 @@ fn new_bridge( println!("Checking if {} is available.\n", service_manager.name()); let service_manager_result = service_manager.check_operational(); - if let Err(SystemServiceError::ServiceManagerUnavailable(name)) = &service_manager_result { + if let Err(SystemServiceError::ServiceManagerUnavailable { cmd: _, name }) = + &service_manager_result + { println!( "Warning: '{}' service manager is not available on the system.\n", name diff --git a/crates/core/tedge/src/cli/disconnect/cli.rs b/crates/core/tedge/src/cli/disconnect/cli.rs index 12181669..1b77aff6 100644 --- a/crates/core/tedge/src/cli/disconnect/cli.rs +++ b/crates/core/tedge/src/cli/disconnect/cli.rs @@ -1,5 +1,6 @@ use crate::cli::disconnect::disconnect_bridge::*; use crate::command::*; +use crate::system_services::service_manager; use structopt::StructOpt; const C8Y_CONFIG_FILENAME: &str = "c8y-bridge.conf"; @@ -17,20 +18,26 @@ impl BuildCommand for TEdgeDisconnectBridgeCli { fn build_command(self, context: BuildContext) -> Result, crate::ConfigError> { let cmd = match self { TEdgeDisconnectBridgeCli::C8y => DisconnectBridgeCommand { - config_location: context.config_location, + config_location: context.config_location.clone(), config_file: C8Y_CONFIG_FILENAME.into(), cloud: Cloud::C8y, use_mapper: true, use_agent: true, - service_manager: context.service_manager.clone(), + service_manager: service_manager( + context.user_manager.clone(), + context.config_location.tedge_config_root_path, + )?, }, TEdgeDisconnectBridgeCli::Az => DisconnectBridgeCommand { - config_location: context.config_location, + config_location: context.config_location.clone(), config_file: AZURE_CONFIG_FILENAME.into(), cloud: Cloud::Azure, use_mapper: true, use_agent: false, - service_manager: context.service_manager.clone(), + service_manager: service_manager( + context.user_manager.clone(), + context.config_location.tedge_config_root_path, + )?, }, }; Ok(cmd.into_boxed()) diff --git a/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs b/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs index d1854a8c..295dd641 100644 --- a/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs +++ b/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs @@ -63,7 +63,7 @@ impl DisconnectBridgeCommand { // If this fails, do not continue with applying changes and stopping/disabling tedge-mapper. self.remove_bridge_config_file()?; - if let Err(SystemServiceError::ServiceManagerUnavailable(name)) = + if let Err(SystemServiceError::ServiceManagerUnavailable { cmd: _, name }) = self.service_manager.check_operational() { println!( diff --git a/crates/core/tedge/src/command.rs b/crates/core/tedge/src/command.rs index 530d519b..6637d0da 100644 --- a/crates/core/tedge/src/command.rs +++ b/crates/core/tedge/src/command.rs @@ -1,5 +1,3 @@ -use crate::system_services::*; -use std::sync::Arc; use tedge_users::UserManager; /// A trait to be implemented by all tedge sub-commands. @@ -152,6 +150,5 @@ pub trait BuildCommand { pub struct BuildContext { pub config_repository: tedge_config::TEdgeConfigRepository, pub config_location: tedge_config::TEdgeConfigLocation, - pub service_manager: Arc, pub user_manager: UserManager, } diff --git a/crates/core/tedge/src/error.rs b/crates/core/tedge/src/error.rs index 3e4bb431..46aa68d3 100644 --- a/crates/core/tedge/src/error.rs +++ b/crates/core/tedge/src/error.rs @@ -1,3 +1,5 @@ +use crate::system_services; + #[derive(thiserror::Error, Debug)] pub enum TEdgeError { #[error("TOML parse error")] @@ -20,4 +22,7 @@ pub enum TEdgeError { #[error(transparent)] FromRumqttClient(#[from] rumqttc::ClientError), + + #[error(transparent)] + FromSystemServiceError(#[from] system_services::SystemServiceError), } diff --git a/crates/core/tedge/src/main.rs b/crates/core/tedge/src/main.rs index 8c2df446..e6313ab3 100644 --- a/crates/core/tedge/src/main.rs +++ b/crates/core/tedge/src/main.rs @@ -1,9 +1,7 @@ #![forbid(unsafe_code)] #![deny(clippy::mem_forget)] -use crate::system_services::*; use anyhow::Context; -use std::sync::Arc; use structopt::StructOpt; use tedge_users::UserManager; use tedge_utils::paths::{home_dir, PathsError}; @@ -36,7 +34,6 @@ fn main() -> anyhow::Result<()> { let build_context = BuildContext { config_repository, config_location: tedge_config_location, - service_manager: service_manager(user_manager.clone()), user_manager, }; @@ -48,15 +45,3 @@ fn main() -> anyhow::Result<()> { cmd.execute() .with_context(|| format!("failed to {}", cmd.description())) } - -fn service_manager(user_manager: UserManager) -> Arc { - if cfg!(feature = "openrc") { - Arc::new(OpenRcServiceManager::new(user_manager)) - } else if cfg!(target_os = "linux") { - Arc::new(SystemdServiceManager::new(user_manager)) - } else if cfg!(target_os = "freebsd") { - Arc::new(BsdServiceManager::new(user_manager)) - } else { - Arc::new(NullSystemServiceManager) - } -} diff --git a/crates/core/tedge/src/system_services/command_builder.rs b/crates/core/tedge/src/system_services/command_builder.rs index 9d9e8ffb..bb66f313 100644 --- a/crates/core/tedge/src/system_services/command_builder.rs +++ b/crates/core/tedge/src/system_services/command_builder.rs @@ -13,8 +13,12 @@ impl CommandBuilder { } } - pub fn arg(mut self, arg: impl AsRef) -> CommandBuilder { - self.command.arg(arg); + pub fn args(mut self, args: I) -> CommandBuilder + where + I: IntoIterator, + S: AsRef, + { + self.command.args(args); self } diff --git a/crates/core/tedge/src/system_services/error.rs b/crates/core/tedge/src/system_services/error.rs index 1b2e8c1a..0de341b3 100644 --- a/crates/core/tedge/src/system_services/error.rs +++ b/crates/core/tedge/src/system_services/error.rs @@ -1,63 +1,34 @@ #[derive(thiserror::Error, Debug)] pub enum SystemServiceError { - #[error(transparent)] - IoError(#[from] std::io::Error), - - #[error(transparent)] - SystemdError(#[from] SystemdError), - - #[error(transparent)] - OpenRcServiceError(#[from] OpenRcServiceError), - - #[error(transparent)] - BsdServiceError(#[from] BsdServiceError), - - #[error("Unexpected value for exit status.")] - UnexpectedExitStatus, - - #[error("Unsupported operation.")] - UnsupportedOperation, - - #[error("Service Manager: '{0}' is not available on the system or elevated permissions have not been granted.")] - ServiceManagerUnavailable(String), -} - -/// The error type used by the `SystemdServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum SystemdError { - #[error("Systemd returned unspecific error for service {service} while performing {cmd} it.\nHint: {hint}")] - UnspecificError { - service: &'static str, - cmd: &'static str, - hint: &'static str, - }, - - #[error("Service {service} not found. Install {service} to use this command.")] - ServiceNotFound { service: &'static str }, + #[error("Service command <{service_command:?}> failed with code: {code:?}.")] + ServiceCommandFailedWithCode { service_command: String, code: i32 }, - #[error("Service {service} not loaded.")] - ServiceNotLoaded { service: &'static str }, + #[error("Service command <{service_command:?}> terminated by a signal.")] + ServiceCommandFailedBySignal { service_command: String }, - #[error("Returned exit code: '{code:?}' for: systemd' is unhandled.")] - UnhandledReturnCode { code: i32 }, -} - -/// The error type used by the `OpenRcServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum OpenRcServiceError { - #[error("Service command <{service_command:?}> failed with code: {code:?}.")] - ServiceCommandFailed { + #[error( + "Service command <{service_command:?}> not found.\n\ + Check '{path}' file." + )] + ServiceCommandNotFound { service_command: String, - code: Option, + path: String, }, -} -/// The error type used by the `BsdServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum BsdServiceError { - #[error("Service command <{service_command:?}> failed with code: {code:?}.")] - ServiceCommandFailed { - service_command: String, - code: Option, + #[error("Failed to execute '{cmd}' to check the service manager availability.\n\ + Service manager '{name}' is not available on the system or elevated permissions have not been granted.")] + ServiceManagerUnavailable { cmd: String, name: String }, + + #[error("Toml syntax error in the system config file '{path}': {reason}")] + SystemConfigInvalidToml { path: String, reason: String }, + + #[error( + "Syntax error in the system config file for '{cmd}': {reason}\n\ + Check '{path}' file." + )] + SystemConfigInvalidSyntax { + reason: String, + cmd: String, + path: String, }, } diff --git a/crates/core/tedge/src/system_services/manager.rs b/crates/core/tedge/src/system_services/manager.rs index 899f6386..7c39ea8c 100644 --- a/crates/core/tedge/src/system_services/manager.rs +++ b/crates/core/tedge/src/system_services/manager.rs @@ -1,5 +1,8 @@ use crate::system_services::*; use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; +use tedge_users::UserManager; /// Abstraction over the system-provided facility that manages starting, stopping as well as other /// service-related management functions of system services. @@ -38,3 +41,13 @@ pub trait SystemServiceManager: Debug { } } } + +pub fn service_manager( + user_manager: UserManager, + config_root: PathBuf, +) -> Result, SystemServiceError> { + Ok(Arc::new(GeneralServiceManager::try_new( + user_manager, + config_root, + )?)) +} diff --git a/crates/core/tedge/src/system_services/managers/bsd.rs b/crates/core/tedge/src/system_services/managers/bsd.rs deleted file mode 100644 index bca56a1c..00000000 --- a/crates/core/tedge/src/system_services/managers/bsd.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::system_services::*; -use std::process::ExitStatus; -use tedge_users::{UserManager, ROOT_USER}; - -/// Service manager that uses `service(8)` as found on FreeBSD to control system services. -/// -#[derive(Debug)] -pub struct BsdServiceManager { - user_manager: UserManager, -} - -impl BsdServiceManager { - pub fn new(user_manager: UserManager) -> Self { - Self { user_manager } - } -} - -impl SystemServiceManager for BsdServiceManager { - fn name(&self) -> &str { - "service(8)" - } - - fn check_operational(&self) -> Result<(), SystemServiceError> { - let mut command = ServiceCommand::CheckManager.into_command(); - - match command.status() { - Ok(status) if status.success() => Ok(()), - _ => Err(SystemServiceError::ServiceManagerUnavailable( - self.name().to_string(), - )), - } - } - - fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Stop(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Restart(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Enable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Disable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn is_service_running(&self, service: SystemService) -> Result { - let service_command = ServiceCommand::IsActive(service); - - self.run_service_command_as_root(service_command) - .map(|status| status.success()) - } -} - -impl BsdServiceManager { - fn run_service_command_as_root( - &self, - service_command: ServiceCommand, - ) -> Result { - let _root_guard = self.user_manager.become_user(ROOT_USER); - - service_command - .into_command() - .status() - .map_err(Into::into) - .map(|status| ServiceCommandExitStatus { - status, - service_command, - }) - } -} - -struct ServiceCommandExitStatus { - status: ExitStatus, - service_command: ServiceCommand, -} - -impl ServiceCommandExitStatus { - fn must_succeed(self) -> Result<(), SystemServiceError> { - if self.status.success() { - Ok(()) - } else { - Err(BsdServiceError::ServiceCommandFailed { - service_command: self.service_command.to_string(), - code: self.status.code(), - } - .into()) - } - } - - fn success(self) -> bool { - self.status.success() - } -} - -const SERVICE_BIN: &str = "/usr/sbin/service"; - -#[derive(Debug, Copy, Clone)] -enum ServiceCommand { - CheckManager, - Stop(SystemService), - Restart(SystemService), - Enable(SystemService), - Disable(SystemService), - IsActive(SystemService), -} - -impl ServiceCommand { - fn to_string(self) -> String { - match self { - Self::CheckManager => format!("{} -l", SERVICE_BIN), - Self::Stop(service) => format!( - "{} {} stop", - SERVICE_BIN, - SystemService::as_service_name(service) - ), - Self::Restart(service) => { - format!( - "{} {} restart", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::Enable(service) => { - format!( - "{} {} enable", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::Disable(service) => { - format!( - "{} {} forcedisable", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::IsActive(service) => { - format!( - "{} {} status", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - } - } - - fn into_command(self) -> std::process::Command { - match self { - Self::CheckManager => CommandBuilder::new(SERVICE_BIN).arg("-l").silent().build(), - Self::Stop(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("stop") - .silent() - .build(), - Self::Restart(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("restart") - .silent() - .build(), - Self::Enable(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("enable") - .silent() - .build(), - - Self::Disable(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - // - // Use "forcedisable" as otherwise it could fail if you have a commented out - // `# mosquitto_enable="YES"` or - // `# mosquitto_enable="NO"` in your `/etc/rc.conf` file. - // - .arg("forcedisable") - .silent() - .build(), - Self::IsActive(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("status") - .silent() - .build(), - } - } -} diff --git a/crates/core/tedge/src/system_services/managers/config.rs b/crates/core/tedge/src/system_services/managers/config.rs new file mode 100644 index 00000000..af495f02 --- /dev/null +++ b/crates/core/tedge/src/system_services/managers/config.rs @@ -0,0 +1,138 @@ +use crate::system_services::*; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +pub const SERVICE_CONFIG_FILE: &str = "system.toml"; + +#[derive(Deserialize, Debug, PartialEq)] +pub struct SystemConfig { + pub(crate) init: InitConfig, +} + +#[derive(Deserialize, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct InitConfig { + pub name: String, + pub is_available: Vec, + pub restart: Vec, + pub stop: Vec, + pub enable: Vec, + pub disable: Vec, + pub is_active: Vec, +} + +impl Default for InitConfig { + fn default() -> Self { + Self { + name: "systemd".to_string(), + is_available: vec!["/bin/systemctl".into(), "--version".into()], + restart: vec!["/bin/systemctl".into(), "restart".into(), "{}".into()], + stop: vec!["/bin/systemctl".into(), "stop".into(), "{}".into()], + enable: vec!["/bin/systemctl".into(), "enable".into(), "{}".into()], + disable: vec!["/bin/systemctl".into(), "disable".into(), "{}".into()], + is_active: vec!["/bin/systemctl".into(), "is-active".into(), "{}".into()], + } + } +} + +impl Default for SystemConfig { + fn default() -> Self { + Self { + init: InitConfig::default(), + } + } +} + +impl SystemConfig { + pub fn try_new(config_root: PathBuf) -> Result { + let config_path = config_root.join(SERVICE_CONFIG_FILE); + let config_path_str = config_path.to_str().unwrap_or(SERVICE_CONFIG_FILE); + + match fs::read_to_string(config_path.clone()) { + Ok(contents) => { + let config: SystemConfig = toml::from_str(contents.as_str()).map_err(|e| { + SystemServiceError::SystemConfigInvalidToml { + path: config_path_str.to_string(), + reason: format!("{}", e), + } + })?; + Ok(config) + } + Err(_) => { + println!("The system config file '{}' doesn't exist. Use '/bin/systemctl' as a service manager.\n", config_path_str); + Ok(Self::default()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + #[test] + fn deserialize_system_config() { + let config: SystemConfig = toml::from_str( + r#" + [init] + name = "systemd" + is_available = ["/bin/systemctl", "--version"] + restart = ["/bin/systemctl", "restart", "{}"] + stop = ["/bin/systemctl", "stop", "{}"] + enable = ["/bin/systemctl", "enable", "{}"] + disable = ["/bin/systemctl", "disable", "{}"] + is_active = ["/bin/systemctl", "is-active", "{}"] + "#, + ) + .unwrap(); + + assert_eq!(config.init.name, "systemd"); + assert_eq!( + config.init.is_available, + vec!["/bin/systemctl", "--version"] + ); + assert_eq!(config.init.restart, vec!["/bin/systemctl", "restart", "{}"]); + assert_eq!(config.init.stop, vec!["/bin/systemctl", "stop", "{}"]); + assert_eq!(config.init.enable, vec!["/bin/systemctl", "enable", "{}"]); + assert_eq!(config.init.disable, vec!["/bin/systemctl", "disable", "{}"]); + assert_eq!( + config.init.is_active, + vec!["/bin/systemctl", "is-active", "{}"] + ); + } + + #[test] + fn read_system_config_file() -> anyhow::Result<()> { + let toml_conf = r#" + [init] + name = "systemd" + is_available = ["/bin/systemctl", "--version"] + restart = ["/bin/systemctl", "restart", "{}"] + stop = ["/bin/systemctl", "stop", "{}"] + enable = ["/bin/systemctl", "enable", "{}"] + disable = ["/bin/systemctl", "disable", "{}"] + is_active = ["/bin/systemctl", "is-active", "{}"] + "#; + let expected_config: SystemConfig = toml::from_str(toml_conf)?; + + let (_dir, config_root_path) = create_temp_system_config(toml_conf)?; + let config = SystemConfig::try_new(config_root_path).unwrap(); + + assert_eq!(config, expected_config); + + Ok(()) + } + + // Need to return TempDir, otherwise the dir will be deleted when this function ends. + fn create_temp_system_config(content: &str) -> std::io::Result<(TempDir, PathBuf)> { + let temp_dir = TempDir::new()?; + let config_root = temp_dir.path().to_path_buf(); + let config_file_path = config_root.join(SERVICE_CONFIG_FILE); + let mut file = std::fs::File::create(config_file_path.as_path())?; + file.write_all(content.as_bytes())?; + Ok((temp_dir, config_root)) + } +} 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 { + 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 { + 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, +} + +impl ExecCommand { + fn try_new( + config: Vec, + cmd: ServiceCommand, + config_path: String, + ) -> Result { + 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, + service_cmd: ServiceCommand, + config_path: String, + service: SystemService, + ) -> Result { + 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, 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 { + 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 { + 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, expected_output: Vec) { + 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, 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) + } +} diff --git a/crates/core/tedge/src/system_services/managers/mod.rs b/crates/core/tedge/src/system_services/managers/mod.rs index 675ca1b7..c5f21cb7 100644 --- a/crates/core/tedge/src/system_services/managers/mod.rs +++ b/crates/core/tedge/src/system_services/managers/mod.rs @@ -1,6 +1,4 @@ -mod bsd; -mod null; -mod openrc; -mod systemd; +mod config; +mod general_manager; -pub use self::{bsd::*, null::*, openrc::*, systemd::*}; +pub use self::{config::*, general_manager::*}; diff --git a/crates/core/tedge/src/system_services/managers/null.rs b/crates/core/tedge/src/system_services/managers/null.rs deleted file mode 100644 index c3416d8b..00000000 --- a/crates/core/tedge/src/system_services/managers/null.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::system_services::{SystemService, SystemServiceError, SystemServiceManager}; - -/// A system service manager that always fails. -#[derive(Debug)] -pub struct NullSystemServiceManager; - -impl SystemServiceManager for NullSystemServiceManager { - fn name(&self) -> &str { - "null (no service manager)" - } - - fn check_operational(&self) -> Result<(), SystemServiceError> { - Err(SystemServiceError::UnsupportedOperation) - } - - fn stop_service(&self, _service: SystemService) -> Result<(), SystemServiceError> { - Err(SystemServiceError::UnsupportedOperation) - } - - fn restart_service(&self, _service: SystemService) -> Result<(), SystemServiceError> { - Err(SystemServiceError::UnsupportedOperation) - } - - fn enable_service(&self, _service: SystemService) -> Result<(), SystemServiceError> { - Err(SystemServiceError::UnsupportedOperation) - } - - fn disable_service(&self, _service: SystemService) -> Result<(), SystemServiceError> { - Err(SystemServiceError::UnsupportedOperation) - } - - fn is_service_running(&self, _service: SystemService) -> Result { - Err(SystemServiceError::UnsupportedOperation) - } -} diff --git a/crates/core/tedge/src/system_services/managers/openrc.rs b/crates/core/tedge/src/system_services/managers/openrc.rs deleted file mode 100644 index 4a24da01..00000000 --- a/crates/core/tedge/src/system_services/managers/openrc.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::system_services::{ - CommandBuilder, OpenRcServiceError, SystemService, SystemServiceError, SystemServiceManager, -}; -use std::process::ExitStatus; -use tedge_users::{UserManager, ROOT_USER}; - -/// Service manager that uses [OpenRC][1] to control system services. -/// -/// [1]: https://github.com/OpenRc/openrc -/// -#[derive(Debug)] -pub struct OpenRcServiceManager { - user_manager: UserManager, -} - -impl OpenRcServiceManager { - pub fn new(user_manager: UserManager) -> Self { - Self { user_manager } - } -} - -impl SystemServiceManager for OpenRcServiceManager { - fn name(&self) -> &str { - "OpenRC" - } - - fn check_operational(&self) -> Result<(), SystemServiceError> { - let mut command = ServiceCommand::CheckManager.into_command(); - - match command.status() { - Ok(status) if status.success() => Ok(()), - _ => Err(SystemServiceError::ServiceManagerUnavailable( - self.name().to_string(), - )), - } - } - - fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Stop(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Restart(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Enable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Disable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn is_service_running(&self, service: SystemService) -> Result { - let service_command = ServiceCommand::IsActive(service); - - self.run_service_command_as_root(service_command) - .map(|status| status.success()) - } -} - -impl OpenRcServiceManager { - fn run_service_command_as_root( - &self, - service_command: ServiceCommand, - ) -> Result { - let _root_guard = self.user_manager.become_user(ROOT_USER); - - service_command - .into_command() - .status() - .map_err(Into::into) - .map(|status| ServiceCommandExitStatus { - status, - service_command, - }) - } -} - -struct ServiceCommandExitStatus { - status: ExitStatus, - service_command: ServiceCommand, -} - -impl ServiceCommandExitStatus { - fn must_succeed(self) -> Result<(), SystemServiceError> { - if self.status.success() { - Ok(()) - } else { - Err(OpenRcServiceError::ServiceCommandFailed { - service_command: self.service_command.to_string(), - code: self.status.code(), - } - .into()) - } - } - - fn success(self) -> bool { - self.status.success() - } -} - -const RC_SERVICE_BIN: &str = "/sbin/rc-service"; -const RC_UPDATE_BIN: &str = "/sbin/rc-update"; - -#[derive(Debug, Copy, Clone)] -enum ServiceCommand { - CheckManager, - Stop(SystemService), - Restart(SystemService), - Enable(SystemService), - Disable(SystemService), - IsActive(SystemService), -} - -impl ServiceCommand { - fn to_string(self) -> String { - match self { - Self::CheckManager => format!("{} -l", RC_SERVICE_BIN), - Self::Stop(service) => format!( - "{} {} stop", - RC_SERVICE_BIN, - SystemService::as_service_name(service) - ), - Self::Restart(service) => { - format!( - "{} {} restart", - RC_SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::Enable(service) => format!( - "{} add {}", - RC_UPDATE_BIN, - SystemService::as_service_name(service) - ), - Self::Disable(service) => { - format!( - "{} delete {}", - RC_UPDATE_BIN, - SystemService::as_service_name(service) - ) - } - Self::IsActive(service) => { - format!( - "{} {} status", - RC_SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - } - } - - fn into_command(self) -> std::process::Command { - match self { - Self::CheckManager => CommandBuilder::new(RC_SERVICE_BIN) - .arg("-l") - .silent() - .build(), - Self::Stop(service) => CommandBuilder::new(RC_SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("stop") - .silent() - .build(), - Self::Restart(service) => CommandBuilder::new(RC_SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("restart") - .silent() - .build(), - Self::Enable(service) => CommandBuilder::new(RC_UPDATE_BIN) - .arg("add") - .arg(SystemService::as_service_name(service)) - .silent() - .build(), - Self::Disable(service) => CommandBuilder::new(RC_SERVICE_BIN) - .arg("delete") - .arg(SystemService::as_service_name(service)) - .silent() - .build(), - Self::IsActive(service) => CommandBuilder::new(RC_SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("status") - .silent() - .build(), - } - } -} diff --git a/crates/core/tedge/src/system_services/managers/systemd.rs b/crates/core/tedge/src/system_services/managers/systemd.rs deleted file mode 100644 index e5107b77..00000000 --- a/crates/core/tedge/src/system_services/managers/systemd.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::system_services::*; -use std::process::ExitStatus; -use tedge_users::*; - -#[derive(Debug)] -pub struct SystemdServiceManager { - systemctl_bin: String, - user_manager: UserManager, -} - -type ExitCode = i32; -type Error = SystemServiceError; - -const SYSTEMCTL_OK: ExitCode = 0; -const SYSTEMCTL_ERROR_GENERIC: ExitCode = 1; -const SYSTEMCTL_ERROR_UNIT_IS_NOT_ACTIVE: ExitCode = 3; -const SYSTEMCTL_ERROR_SERVICE_NOT_FOUND: ExitCode = 5; -const SYSTEMCTL_ERROR_SERVICE_NOT_LOADED: ExitCode = 5; - -const SYSTEMCTL_BIN: &str = "systemctl"; - -impl SystemdServiceManager { - pub fn new(user_manager: UserManager) -> Self { - Self { - systemctl_bin: SYSTEMCTL_BIN.into(), - user_manager, - } - } -} - -impl SystemServiceManager for SystemdServiceManager { - fn name(&self) -> &str { - "systemd" - } - - fn check_operational(&self) -> Result<(), SystemServiceError> { - match std::process::Command::new(&self.systemctl_bin) - .arg(SystemCtlParam::Version.as_str()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - { - Ok(status) if status.success() => Ok(()), - _ => Err(SystemServiceError::ServiceManagerUnavailable( - self.name().to_string(), - )), - } - } - - fn stop_service(&self, service: SystemService) -> Result<(), Error> { - let service_name = SystemService::as_service_name(service); - match self.call_systemd_subcmd_sudo(SystemCtlCmd::Stop, service_name)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(SystemdError::UnspecificError { - service: service_name, - cmd: SystemCtlCmd::Stop.as_str(), - hint: "Lacking permissions.", - } - .into()), - SYSTEMCTL_ERROR_SERVICE_NOT_LOADED => Err(SystemdError::ServiceNotLoaded { - service: service_name, - } - .into()), - code => Err(SystemdError::UnhandledReturnCode { code }.into()), - } - } - - // Note that restarting a unit with this command does not necessarily flush out all of the unit's resources before it is started again. - // For example, the per-service file descriptor storage facility (see FileDescriptorStoreMax= in systemd.service(5)) will remain intact - // as long as the unit has a job pending, and is only cleared when the unit is fully stopped and no jobs are pending anymore. - // If it is intended that the file descriptor store is flushed out, too, during a restart operation an explicit - // systemctl stop command followed by systemctl start should be issued. - fn restart_service(&self, service: SystemService) -> Result<(), Error> { - let service_name = SystemService::as_service_name(service); - match self.call_systemd_subcmd_sudo(SystemCtlCmd::Restart, service_name)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(SystemdError::UnspecificError { - service: service_name, - cmd: SystemCtlCmd::Restart.as_str(), - hint: "Lacking permissions or service's process exited with error code.", - } - .into()), - SYSTEMCTL_ERROR_SERVICE_NOT_FOUND => Err(SystemdError::ServiceNotFound { - service: service_name, - } - .into()), - code => Err(SystemdError::UnhandledReturnCode { code }.into()), - } - } - - fn enable_service(&self, service: SystemService) -> Result<(), Error> { - let service_name = SystemService::as_service_name(service); - match self.call_systemd_subcmd_sudo(SystemCtlCmd::Enable, service_name)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(SystemdError::UnspecificError { - service: service_name, - cmd: SystemCtlCmd::Enable.as_str(), - hint: "Lacking permissions.", - } - .into()), - code => Err(SystemdError::UnhandledReturnCode { code }.into()), - } - } - - fn disable_service(&self, service: SystemService) -> Result<(), Error> { - let service_name = SystemService::as_service_name(service); - match self.call_systemd_subcmd_sudo(SystemCtlCmd::Disable, service_name)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(SystemdError::UnspecificError { - service: service_name, - cmd: SystemCtlCmd::Disable.as_str(), - hint: "Lacking permissions.", - } - .into()), - code => Err(SystemdError::UnhandledReturnCode { code }.into()), - } - } - - fn is_service_running(&self, service: SystemService) -> Result { - let service_name = SystemService::as_service_name(service); - match self.call_systemd_subcmd(SystemCtlCmd::IsActive, service_name)? { - SYSTEMCTL_OK => Ok(true), - SYSTEMCTL_ERROR_UNIT_IS_NOT_ACTIVE => Ok(false), - code => Err(SystemdError::UnhandledReturnCode { code }.into()), - } - } -} - -impl SystemdServiceManager { - fn call_systemd_subcmd_sudo( - &self, - systemctl_subcmd: SystemCtlCmd, - arg: &str, - ) -> Result { - let _root_guard = self.user_manager.become_user(ROOT_USER); - self.call_systemd_subcmd(systemctl_subcmd, arg) - } - - fn call_systemd_subcmd(&self, systemctl_subcmd: SystemCtlCmd, arg: &str) -> Result { - cmd_nullstdio_args_with_code(&self.systemctl_bin, &[systemctl_subcmd.as_str(), arg])? - .code() - .ok_or(Error::UnexpectedExitStatus) - } -} - -fn cmd_nullstdio_args_with_code(command: &str, args: &[&str]) -> Result { - Ok(std::process::Command::new(command) - .args(args) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status()?) -} - -#[derive(Debug)] -enum SystemCtlCmd { - Enable, - Disable, - IsActive, - Stop, - Restart, -} - -impl SystemCtlCmd { - fn as_str(&self) -> &'static str { - match self { - SystemCtlCmd::Enable => "enable", - SystemCtlCmd::Disable => "disable", - SystemCtlCmd::IsActive => "is-active", - SystemCtlCmd::Stop => "stop", - SystemCtlCmd::Restart => "restart", - } - } -} - -impl From for String { - fn from(val: SystemCtlCmd) -> Self { - val.as_str().into() - } -} - -#[derive(Debug)] -enum SystemCtlParam { - Version, -} - -impl SystemCtlParam { - fn as_str(&self) -> &'static str { - match self { - SystemCtlParam::Version => "--version", - } - } -} - -impl From for String { - fn from(val: SystemCtlParam) -> Self { - val.as_str().into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cmd_nullstdio_args_expected_exit_code_zero() { - // There is a chance that this may fail on very embedded system which will not have 'ls' command on busybox. - assert_eq!( - cmd_nullstdio_args_with_code("ls", &[]).unwrap().code(), - Some(0) - ); - } - - #[test] - fn cmd_nullstdio_args_command_not_exists() { - assert!(cmd_nullstdio_args_with_code("test-command", &[]).is_err()) - } -} -- cgit v1.2.3