diff options
22 files changed, 687 insertions, 740 deletions
diff --git a/configuration/contrib/system/bsd/system.toml b/configuration/contrib/system/bsd/system.toml new file mode 100644 index 00000000..05f500f5 --- /dev/null +++ b/configuration/contrib/system/bsd/system.toml @@ -0,0 +1,8 @@ +[init] +name = "service(8)" +is_available = ["/usr/sbin/service", "-l"] +restart = ["/usr/sbin/service", "{}", "restart"] +stop = ["/usr/sbin/service", "{}", "stop"] +enable = ["/usr/sbin/service", "{}", "enable"] +disable = ["/usr/sbin/service", "{}", "forcedisable"] +is_active = ["/usr/sbin/service", "{}", "status"] diff --git a/configuration/contrib/system/openrc/system.toml b/configuration/contrib/system/openrc/system.toml new file mode 100644 index 00000000..cf3a72d0 --- /dev/null +++ b/configuration/contrib/system/openrc/system.toml @@ -0,0 +1,8 @@ +[init] +name = "OpenRC" +is_available = ["/sbin/rc-service", "-l"] +restart = ["/sbin/rc-service", "{}", "restart"] +stop = ["/sbin/rc-service", "{}", "stop"] +enable = ["/sbin/rc-update", "add", "{}"] +disable = ["/sbin/rc-update", "delete", "{}"] +is_active = ["/sbin/rc-service", "{}", "status"] diff --git a/configuration/contrib/system/systemd/system.toml b/configuration/contrib/system/systemd/system.toml new file mode 100644 index 00000000..bb1fc2c5 --- /dev/null +++ b/configuration/contrib/system/systemd/system.toml @@ -0,0 +1,8 @@ +[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", "{}"] 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<Box<dyn Command>, 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<Box<dyn Command>, 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<dyn SystemServiceManager>, 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<dyn SystemServiceManager> { - 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<OsStr>) -> CommandBuilder { - self.command.arg(arg); + pub fn args<I, S>(mut self, args: I) -> CommandBuilder + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + 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<i32>, + 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<i32>, + #[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<Arc<dyn SystemServiceManager>, 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<bool, SystemServiceError> { - 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<ServiceCommandExitStatus, SystemServiceError> { - 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<String>, + pub restart: Vec<String>, + pub stop: Vec<String>, + pub enable: Vec<String>, + pub disable: Vec<String>, + pub is_active: Vec<String>, +} + +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<Self, SystemServiceError> { + 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", "{}"] |