diff --git a/crates/common/tedge_config/src/system_services/manager.rs b/crates/common/tedge_config/src/system_services/manager.rs index 88524077f2..db4153ade5 100644 --- a/crates/common/tedge_config/src/system_services/manager.rs +++ b/crates/common/tedge_config/src/system_services/manager.rs @@ -14,22 +14,22 @@ pub trait SystemServiceManager: Debug { fn check_operational(&self) -> Result<(), SystemServiceError>; /// Stops the specified system service. - fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError>; + fn stop_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError>; /// Starts the specified system service. - fn start_service(&self, service: SystemService) -> Result<(), SystemServiceError>; + fn start_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError>; /// Restarts the specified system service. - fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError>; + fn restart_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError>; /// Enables the specified system service. This does not start the service, unless you reboot. - fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError>; + fn enable_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError>; /// Disables the specified system service. This does not stop the service. - fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError>; + fn disable_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError>; /// Queries status of the specified system service. "Running" here means the same as "active". - fn is_service_running(&self, service: SystemService) -> Result; + fn is_service_running(&self, service: SystemService<'_>) -> Result; } pub fn service_manager( diff --git a/crates/common/tedge_config/src/system_services/manager_ext.rs b/crates/common/tedge_config/src/system_services/manager_ext.rs index 36418d3ab1..94637f8b21 100644 --- a/crates/common/tedge_config/src/system_services/manager_ext.rs +++ b/crates/common/tedge_config/src/system_services/manager_ext.rs @@ -3,19 +3,19 @@ use anyhow::Context as _; /// Extension trait for `SystemServiceManager`. pub trait SystemServiceManagerExt { - fn start_and_enable_service(&self, service: SystemService) -> anyhow::Result<()>; - fn stop_and_disable_service(&self, service: SystemService) -> anyhow::Result<()>; + fn start_and_enable_service(&self, service: SystemService<'_>) -> anyhow::Result<()>; + fn stop_and_disable_service(&self, service: SystemService<'_>) -> anyhow::Result<()>; } impl SystemServiceManagerExt for &dyn SystemServiceManager { - fn start_and_enable_service(&self, service: SystemService) -> anyhow::Result<()> { + fn start_and_enable_service(&self, service: SystemService<'_>) -> anyhow::Result<()> { self.start_service(service) .with_context(|| format!("Failed to start {service}"))?; self.enable_service(service) .with_context(|| format!("Failed to enable {service}")) } - fn stop_and_disable_service(&self, service: SystemService) -> anyhow::Result<()> { + fn stop_and_disable_service(&self, service: SystemService<'_>) -> anyhow::Result<()> { self.stop_service(service) .with_context(|| format!("Failed to stop {service}"))?; self.disable_service(service) diff --git a/crates/common/tedge_config/src/system_services/managers/general_manager.rs b/crates/common/tedge_config/src/system_services/managers/general_manager.rs index fa772444c9..40658e8b36 100644 --- a/crates/common/tedge_config/src/system_services/managers/general_manager.rs +++ b/crates/common/tedge_config/src/system_services/managers/general_manager.rs @@ -47,37 +47,37 @@ impl SystemServiceManager for GeneralServiceManager { } } - fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + 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 start_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + fn start_service(&self, service: SystemService<'_>) -> Result<(), SystemServiceError> { let exec_command = ServiceCommand::Start(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> { + 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> { + 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> { + 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 { + 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()) @@ -109,11 +109,11 @@ impl ExecCommand { } } - fn try_new_with_placeholder( + fn try_new_with_placeholder<'a>( config: Vec, - service_cmd: ServiceCommand, + service_cmd: ServiceCommand<'a>, config_path: Utf8PathBuf, - service: SystemService, + service: SystemService<'a>, ) -> Result { let replaced = replace_with_service_name(&config, service_cmd, &config_path, service)?; Self::try_new(replaced, service_cmd, config_path) @@ -141,11 +141,11 @@ impl fmt::Display for ExecCommand { } } -fn replace_with_service_name( +fn replace_with_service_name<'a>( input_args: &[String], - service_cmd: ServiceCommand, + service_cmd: ServiceCommand<'a>, config_path: impl Into, - service: SystemService, + service: SystemService<'a>, ) -> Result, SystemServiceError> { if !input_args.iter().any(|s| s == "{}") { return Err(SystemServiceError::SystemConfigInvalidSyntax { @@ -158,7 +158,7 @@ fn replace_with_service_name( let mut args = input_args.to_owned(); for item in args.iter_mut() { if item == "{}" { - *item = SystemService::as_service_name(service).to_string(); + *item = service.to_string(); } } @@ -166,19 +166,19 @@ fn replace_with_service_name( } #[derive(Debug, Copy, Clone)] -enum ServiceCommand { +enum ServiceCommand<'a> { CheckManager, - Stop(SystemService), - Start(SystemService), - Restart(SystemService), - Enable(SystemService), - Disable(SystemService), - IsActive(SystemService), + Stop(SystemService<'a>), + Start(SystemService<'a>), + Restart(SystemService<'a>), + Enable(SystemService<'a>), + Disable(SystemService<'a>), + IsActive(SystemService<'a>), } -impl ServiceCommand { +impl<'a> ServiceCommand<'a> { fn try_exec_command( - &self, + self, service_manager: &GeneralServiceManager, ) -> Result { let config_path = service_manager.config_path.clone(); @@ -190,45 +190,45 @@ impl ServiceCommand { ), Self::Stop(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.stop.clone(), - ServiceCommand::Stop(*service), + ServiceCommand::Stop(service), config_path, - *service, + service, ), Self::Restart(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.restart.clone(), - ServiceCommand::Restart(*service), + ServiceCommand::Restart(service), config_path, - *service, + service, ), Self::Start(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.start.clone(), - ServiceCommand::Enable(*service), + ServiceCommand::Enable(service), config_path, - *service, + service, ), Self::Enable(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.enable.clone(), - ServiceCommand::Enable(*service), + ServiceCommand::Enable(service), config_path, - *service, + service, ), Self::Disable(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.disable.clone(), - ServiceCommand::Disable(*service), + ServiceCommand::Disable(service), config_path, - *service, + service, ), Self::IsActive(service) => ExecCommand::try_new_with_placeholder( service_manager.init_config.is_active.clone(), - ServiceCommand::IsActive(*service), + ServiceCommand::IsActive(service), config_path, - *service, + service, ), } } } -impl fmt::Display for ServiceCommand { +impl<'a> fmt::Display for ServiceCommand<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::CheckManager => write!(f, "is_available"), diff --git a/crates/common/tedge_config/src/system_services/services.rs b/crates/common/tedge_config/src/system_services/services.rs index 7b3bd186b4..768837bc38 100644 --- a/crates/common/tedge_config/src/system_services/services.rs +++ b/crates/common/tedge_config/src/system_services/services.rs @@ -1,25 +1,38 @@ +use std::fmt; + +use tedge_config_macros::ProfileName; + /// An enumeration of all supported system services. -#[derive(Debug, Copy, Clone, strum_macros::Display, strum_macros::IntoStaticStr)] -pub enum SystemService { +#[derive(Debug, Copy, Clone, strum_macros::IntoStaticStr)] +pub enum SystemService<'a> { #[strum(serialize = "mosquitto")] /// Mosquitto broker Mosquitto, #[strum(serialize = "tedge-mapper-az")] /// Azure TEdge mapper - TEdgeMapperAz, + TEdgeMapperAz(Option<&'a ProfileName>), #[strum(serialize = "tedge-mapper-aws")] /// AWS TEdge mapper - TEdgeMapperAws, + TEdgeMapperAws(Option<&'a ProfileName>), #[strum(serialize = "tedge-mapper-c8y")] /// Cumulocity TEdge mapper - TEdgeMapperC8y, + TEdgeMapperC8y(Option<&'a ProfileName>), #[strum(serialize = "tedge-agent")] /// TEdge SM agent TEdgeSMAgent, } -impl SystemService { - pub(crate) fn as_service_name(service: SystemService) -> &'static str { - service.into() +impl<'a> fmt::Display for SystemService<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Mosquitto => write!(f, "mosquitto"), + Self::TEdgeMapperAz(None) => write!(f, "tedge-mapper-az"), + Self::TEdgeMapperAz(Some(profile)) => write!(f, "tedge-mapper-az@{profile}"), + Self::TEdgeMapperAws(None) => write!(f, "tedge-mapper-aws"), + Self::TEdgeMapperAws(Some(profile)) => write!(f, "tedge-mapper-aws@{profile}"), + Self::TEdgeMapperC8y(None) => write!(f, "tedge-mapper-c8y"), + Self::TEdgeMapperC8y(Some(profile)) => write!(f, "tedge-mapper-c8y@{profile}"), + Self::TEdgeSMAgent => write!(f, "tedge-agent"), + } } } diff --git a/crates/core/tedge/src/bridge/aws.rs b/crates/core/tedge/src/bridge/aws.rs index 2b218c14d0..78ac315f2b 100644 --- a/crates/core/tedge/src/bridge/aws.rs +++ b/crates/core/tedge/src/bridge/aws.rs @@ -1,6 +1,7 @@ use super::BridgeConfig; use crate::bridge::config::BridgeLocation; use camino::Utf8PathBuf; +use tedge_config::ProfileName; use std::borrow::Cow; use tedge_config::HostPort; use tedge_config::TopicPrefix; @@ -18,6 +19,7 @@ pub struct BridgeConfigAwsParams { pub bridge_keyfile: Utf8PathBuf, pub bridge_location: BridgeLocation, pub topic_prefix: TopicPrefix, + pub profile_name: Option, } impl From for BridgeConfig { @@ -31,6 +33,7 @@ impl From for BridgeConfig { bridge_keyfile, bridge_location, topic_prefix, + profile_name, } = params; let user_name = remote_clientid.to_string(); @@ -54,13 +57,21 @@ impl From for BridgeConfig { Self { cloud_name: "aws".into(), config_file, - connection: "edge_to_aws".into(), + connection: if let Some(profile) = &profile_name { + format!("edge_to_aws@{profile}") + } else { + "edge_to_aws".into() + }, address: mqtt_host, remote_username: Some(user_name), remote_password: None, bridge_root_cert_path, remote_clientid, - local_clientid: "Aws".into(), + local_clientid: if let Some(profile) = &profile_name { + format!("Aws@{profile}") + } else { + "Aws".into() + }, bridge_certfile, bridge_keyfile, use_mapper: true, @@ -103,6 +114,7 @@ fn test_bridge_config_from_aws_params() -> anyhow::Result<()> { bridge_keyfile: "./test-private-key.pem".into(), bridge_location: BridgeLocation::Mosquitto, topic_prefix: "aws".try_into().unwrap(), + profile_name: None, }; let bridge = BridgeConfig::from(params); @@ -159,6 +171,7 @@ fn test_bridge_config_aws_custom_topic_prefix() -> anyhow::Result<()> { bridge_keyfile: "./test-private-key.pem".into(), bridge_location: BridgeLocation::Mosquitto, topic_prefix: "custom".try_into().unwrap(), + profile_name: Some("profile".parse().unwrap()), }; let bridge = BridgeConfig::from(params); @@ -166,13 +179,13 @@ fn test_bridge_config_aws_custom_topic_prefix() -> anyhow::Result<()> { let expected = BridgeConfig { cloud_name: "aws".into(), config_file: "aws-bridge.conf".into(), - connection: "edge_to_aws".into(), + connection: "edge_to_aws@profile".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("alpha".into()), remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), - local_clientid: "Aws".into(), + local_clientid: "Aws@profile".into(), bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), use_mapper: true, diff --git a/crates/core/tedge/src/bridge/azure.rs b/crates/core/tedge/src/bridge/azure.rs index 0dfeff5bfc..0dfc8ed600 100644 --- a/crates/core/tedge/src/bridge/azure.rs +++ b/crates/core/tedge/src/bridge/azure.rs @@ -1,6 +1,7 @@ use super::BridgeConfig; use crate::bridge::config::BridgeLocation; use camino::Utf8PathBuf; +use tedge_config::ProfileName; use std::borrow::Cow; use tedge_config::HostPort; use tedge_config::TopicPrefix; @@ -18,6 +19,7 @@ pub struct BridgeConfigAzureParams { pub bridge_keyfile: Utf8PathBuf, pub bridge_location: BridgeLocation, pub topic_prefix: TopicPrefix, + pub profile_name: Option, } impl From for BridgeConfig { @@ -31,6 +33,7 @@ impl From for BridgeConfig { bridge_keyfile, bridge_location, topic_prefix, + profile_name, } = params; let address = mqtt_host.clone(); @@ -46,13 +49,21 @@ impl From for BridgeConfig { Self { cloud_name: "az".into(), config_file, - connection: "edge_to_az".into(), + connection: if let Some(profile) = &profile_name { + format!("edge_to_az@{profile}") + } else { + "edge_to_az".into() + }, address, remote_username: Some(user_name), remote_password: None, bridge_root_cert_path, remote_clientid, - local_clientid: "Azure".into(), + local_clientid: if let Some(profile) = &profile_name { + format!("Azure@{profile}") + } else { + "Azure".into() + }, bridge_certfile, bridge_keyfile, use_mapper: true, @@ -100,6 +111,7 @@ fn test_bridge_config_from_azure_params() -> anyhow::Result<()> { bridge_keyfile: "./test-private-key.pem".into(), bridge_location: BridgeLocation::Mosquitto, topic_prefix: "az".try_into().unwrap(), + profile_name: None, }; let bridge = BridgeConfig::from(params); @@ -160,6 +172,7 @@ fn test_azure_bridge_config_with_custom_prefix() -> anyhow::Result<()> { bridge_keyfile: "./test-private-key.pem".into(), bridge_location: BridgeLocation::Mosquitto, topic_prefix: "custom".try_into().unwrap(), + profile_name: Some("profile".parse().unwrap()), }; let bridge = BridgeConfig::from(params); @@ -167,13 +180,13 @@ fn test_azure_bridge_config_with_custom_prefix() -> anyhow::Result<()> { let expected = BridgeConfig { cloud_name: "az".into(), config_file: "az-bridge.conf".into(), - connection: "edge_to_az".into(), + connection: "edge_to_az@profile".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), - local_clientid: "Azure".into(), + local_clientid: "Azure@profile".into(), bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), use_mapper: true, diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 57a153083e..766db61e0e 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -6,6 +6,7 @@ use std::process::Command; use tedge_config::auth_method::AuthMethod; use tedge_config::AutoFlag; use tedge_config::HostPort; +use tedge_config::ProfileName; use tedge_config::TemplatesSet; use tedge_config::TopicPrefix; use tedge_config::MQTT_TLS_PORT; @@ -28,6 +29,7 @@ pub struct BridgeConfigC8yParams { pub include_local_clean_session: AutoFlag, pub bridge_location: BridgeLocation, pub topic_prefix: TopicPrefix, + pub profile_name: Option, } impl From for BridgeConfig { @@ -46,6 +48,7 @@ impl From for BridgeConfig { include_local_clean_session, bridge_location, topic_prefix, + profile_name, } = params; let mut topics: Vec = vec![ @@ -143,13 +146,21 @@ impl From for BridgeConfig { Self { cloud_name: "c8y".into(), config_file, - connection: "edge_to_c8y".into(), + connection: if let Some(profile) = &profile_name { + format!("edge_to_c8y@{profile}") + } else { + "edge_to_c8y".into() + }, address: mqtt_host, remote_username, remote_password, bridge_root_cert_path, remote_clientid, - local_clientid: "Cumulocity".into(), + local_clientid: if let Some(profile) = &profile_name { + format!("c8y-bridge@{profile}") + } else { + "c8y-bridge".into() + }, bridge_certfile, bridge_keyfile, use_mapper: true, @@ -227,6 +238,7 @@ mod tests { include_local_clean_session: AutoFlag::False, bridge_location: BridgeLocation::Mosquitto, topic_prefix: "c8y".try_into().unwrap(), + profile_name: None, }; let bridge = BridgeConfig::from(params); @@ -240,7 +252,7 @@ mod tests { remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), - local_clientid: "Cumulocity".into(), + local_clientid: "c8y-bridge".into(), bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), use_mapper: true, @@ -303,7 +315,6 @@ mod tests { #[test] fn test_bridge_config_from_c8y_params_basic_auth() -> anyhow::Result<()> { - use std::convert::TryFrom; let params = BridgeConfigC8yParams { mqtt_host: HostPort::::try_from("test.test.io")?, config_file: "c8y-bridge.conf".into(), @@ -318,6 +329,7 @@ mod tests { include_local_clean_session: AutoFlag::False, bridge_location: BridgeLocation::Mosquitto, topic_prefix: "c8y".try_into().unwrap(), + profile_name: Some("profile".parse().unwrap()), }; let bridge = BridgeConfig::from(params); @@ -325,13 +337,13 @@ mod tests { let expected = BridgeConfig { cloud_name: "c8y".into(), config_file: "c8y-bridge.conf".into(), - connection: "edge_to_c8y".into(), + connection: "edge_to_c8y@profile".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("octocat".into()), remote_password: Some("abcd1234".into()), bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), - local_clientid: "Cumulocity".into(), + local_clientid: "c8y-bridge@profile".into(), bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), use_mapper: true, diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index fb85094b6e..e6b808347d 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -1,4 +1,6 @@ use crate::bridge::BridgeLocation; +use crate::cli::common::Cloud; +use anyhow::anyhow; use camino::Utf8PathBuf; use tedge_config::OptionalConfigError; use tedge_config::ProfileName; @@ -45,7 +47,6 @@ pub enum TEdgeCertCli { Remove, /// Upload root certificate - #[clap(subcommand)] Upload(UploadCertCli), } @@ -98,25 +99,26 @@ impl BuildCommand for TEdgeCertCli { } TEdgeCertCli::Upload(cmd) => { - let cmd = match cmd { - UploadCertCli::C8y { - username, - password, - profile, - } => UploadCertCmd { - device_id: config.device.id.try_read(&config)?.clone(), - path: config.device.cert_path.clone(), - host: config - .c8y - .try_get(profile.as_deref())? - .http - .or_err()? - .to_owned(), - cloud_root_certs: config.cloud_root_certs(), - username, - password, - }, - }; + let cmd = + match cmd.cloud { + Cloud::C8y(profile) => UploadCertCmd { + device_id: config.device.id.try_read(&config)?.clone(), + path: config.device.cert_path.clone(), + host: config + .c8y + .try_get(profile.as_deref())? + .http + .or_err()? + .to_owned(), + cloud_root_certs: config.cloud_root_certs(), + username: cmd.username, + password: cmd.password, + }, + cloud => return Err(anyhow!( + "Uploading certificates via the tedge cli isn't supported for {cloud}" + ) + .into()), + }; cmd.into_boxed() } TEdgeCertCli::Renew => { @@ -132,33 +134,29 @@ impl BuildCommand for TEdgeCertCli { } } -#[derive(clap::Subcommand, Debug)] -pub enum UploadCertCli { - /// Upload root certificate to Cumulocity +#[derive(clap::Args, Debug)] +pub struct UploadCertCli { + cloud: Cloud, + #[clap(long = "user")] + #[arg( + env = "C8Y_USER", + hide_env_values = true, + hide_default_value = true, + default_value = "" + )] + /// Provided username should be a Cumulocity IoT user with tenant management permissions. + /// You will be prompted for input if the value is not provided or is empty + username: String, + + #[clap(long = "password")] + #[arg(env = "C8Y_PASSWORD", hide_env_values = true, hide_default_value = true, default_value_t = std::env::var("C8YPASS").unwrap_or_default().to_string())] + // Note: Prefer C8Y_PASSWORD over the now deprecated C8YPASS env variable as the former is also supported by other tooling such as go-c8y-cli + /// Cumulocity IoT Password. + /// You will be prompted for input if the value is not provided or is empty /// - /// The command will upload root certificate to Cumulocity. - C8y { - #[clap(long = "user")] - #[arg( - env = "C8Y_USER", - hide_env_values = true, - hide_default_value = true, - default_value = "" - )] - /// Provided username should be a Cumulocity IoT user with tenant management permissions. - /// You will be prompted for input if the value is not provided or is empty - username: String, - - #[clap(long = "password")] - #[arg(env = "C8Y_PASSWORD", hide_env_values = true, hide_default_value = true, default_value_t = std::env::var("C8YPASS").unwrap_or_default().to_string())] - // Note: Prefer C8Y_PASSWORD over the now deprecated C8YPASS env variable as the former is also supported by other tooling such as go-c8y-cli - /// Cumulocity IoT Password. - /// You will be prompted for input if the value is not provided or is empty - /// - /// Notes: `C8YPASS` is deprecated. Please use the `C8Y_PASSWORD` env variable instead - password: String, - - #[clap(long, hide = true)] - profile: Option, - }, + /// Notes: `C8YPASS` is deprecated. Please use the `C8Y_PASSWORD` env variable instead + password: String, + + #[clap(long, hide = true)] + profile: Option, } diff --git a/crates/core/tedge/src/cli/common.rs b/crates/core/tedge/src/cli/common.rs index 7316333f01..fe361de200 100644 --- a/crates/core/tedge/src/cli/common.rs +++ b/crates/core/tedge/src/cli/common.rs @@ -1,33 +1,77 @@ use std::borrow::Cow; - +use std::str::FromStr; use tedge_config::system_services::SystemService; use tedge_config::ProfileName; -#[derive(Copy, Clone, Debug, strum_macros::Display, strum_macros::IntoStaticStr, PartialEq, Eq)] -pub enum Cloud { +pub type Cloud = MaybeBorrowedCloud<'static>; + +impl FromStr for Cloud { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + match (s, s.split_once("@")) { + (_, Some(("c8y", profile))) => Ok(Self::c8y(Some(profile.parse()?))), + ("c8y", None) => Ok(Self::c8y(None)), + (_, Some(("az", profile))) => Ok(Self::az(Some(profile.parse()?))), + ("az", None) => Ok(Self::Azure(None)), + (_, Some(("aws", profile))) => Ok(Self::aws(Some(profile.parse()?))), + ("aws", None) => Ok(Self::Aws(None)), + _ => todo!(), + } + } +} + +pub type CloudBorrow<'a> = MaybeBorrowedCloud<'a>; + +#[derive(Clone, Debug, strum_macros::Display, strum_macros::IntoStaticStr, PartialEq, Eq)] +pub enum MaybeBorrowedCloud<'a> { #[strum(serialize = "Cumulocity")] - C8y, - Azure, - Aws, + C8y(Option>), + Azure(Option>), + Aws(Option>), } impl Cloud { - pub fn mapper_service(&self) -> SystemService { + pub fn c8y(profile: Option) -> Self { + Self::C8y(profile.map(Cow::Owned)) + } + pub fn az(profile: Option) -> Self { + Self::Azure(profile.map(Cow::Owned)) + } + pub fn aws(profile: Option) -> Self { + Self::Aws(profile.map(Cow::Owned)) + } +} + +impl<'a> CloudBorrow<'a> { + pub fn c8y_borrowed(profile: Option<&'a ProfileName>) -> Self { + Self::C8y(profile.map(Cow::Borrowed)) + } + pub fn az_borrowed(profile: Option<&'a ProfileName>) -> Self { + Self::Azure(profile.map(Cow::Borrowed)) + } + pub fn aws_borrowed(profile: Option<&'a ProfileName>) -> Self { + Self::Aws(profile.map(Cow::Borrowed)) + } +} + +impl<'a> MaybeBorrowedCloud<'a> { + pub fn mapper_service(&self) -> SystemService<'_> { match self { - Cloud::Aws => SystemService::TEdgeMapperAws, - Cloud::Azure => SystemService::TEdgeMapperAz, - Cloud::C8y => SystemService::TEdgeMapperC8y, + Self::Aws(profile) => SystemService::TEdgeMapperAws(profile.as_deref()), + Self::Azure(profile) => SystemService::TEdgeMapperAz(profile.as_deref()), + Self::C8y(profile) => SystemService::TEdgeMapperC8y(profile.as_deref()), } } - pub fn bridge_config_filename(&self, profile: Option<&ProfileName>) -> Cow<'static, str> { - match (self, profile) { - (Self::C8y, None) => "c8y-bridge.conf".into(), - (Self::C8y, Some(profile)) => format!("c8y{profile}-bridge.conf").into(), - (Self::Aws, None) => "aws-bridge.conf".into(), - (Self::Aws, Some(profile)) => format!("aws{profile}-bridge.conf").into(), - (Self::Azure, None) => "az-bridge.conf".into(), - (Self::Azure, Some(profile)) => format!("az{profile}-bridge.conf").into(), + pub fn bridge_config_filename(&self) -> Cow<'static, str> { + match self { + Self::C8y(None) => "c8y-bridge.conf".into(), + Self::C8y(Some(profile)) => format!("c8y@{profile}-bridge.conf").into(), + Self::Aws(None) => "aws-bridge.conf".into(), + Self::Aws(Some(profile)) => format!("aws@{profile}-bridge.conf").into(), + Self::Azure(None) => "az-bridge.conf".into(), + Self::Azure(Some(profile)) => format!("az@{profile}-bridge.conf").into(), } } } diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index e807be5804..a7482fa52e 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -54,6 +54,8 @@ pub enum ConfigCmd { /// Prints all keys and descriptions with example values #[clap(long = "doc")] is_doc: bool, + + filter: Option, }, } @@ -90,10 +92,15 @@ impl BuildCommand for ConfigCmd { config_location, } .into_boxed()), - ConfigCmd::List { is_all, is_doc } => Ok(ListConfigCommand { + ConfigCmd::List { + is_all, + is_doc, + filter, + } => Ok(ListConfigCommand { is_all, is_doc, config: config_location.load()?, + filter, } .into_boxed()), } diff --git a/crates/core/tedge/src/cli/connect/cli.rs b/crates/core/tedge/src/cli/connect/cli.rs index e98247013c..8f23b5aaaf 100644 --- a/crates/core/tedge/src/cli/connect/cli.rs +++ b/crates/core/tedge/src/cli/connect/cli.rs @@ -1,109 +1,39 @@ -use tedge_config::system_services::service_manager; -use tedge_config::ProfileName; - use crate::cli::common::Cloud; use crate::cli::connect::*; use crate::command::BuildCommand; use crate::command::BuildContext; use crate::command::Command; +use tedge_config::system_services::service_manager; -#[derive(clap::Subcommand, Debug, Eq, PartialEq)] -pub enum TEdgeConnectOpt { - /// Create connection to Cumulocity - /// - /// The command will create config and start edge relay from the device to c8y instance - C8y { - /// Test connection to Cumulocity - #[clap(long = "test")] - is_test_connection: bool, - - /// Ignore connection registration and connection check - #[clap(long = "offline")] - offline_mode: bool, - - #[clap(long, hide = true)] - profile: Option, - }, - - /// Create connection to Azure - /// - /// The command will create config and start edge relay from the device to az instance - Az { - /// Test connection to Azure - #[clap(long = "test")] - is_test_connection: bool, - - /// Ignore connection registration and connection check - #[clap(long = "offline")] - offline_mode: bool, - - #[clap(long, hide = true)] - profile: Option, - }, - - /// Create connection to AWS - /// - /// The command will create config and start edge relay from the device to AWS instance - Aws { - /// Test connection to AWS - #[clap(long = "test")] - is_test_connection: bool, +#[derive(clap::Args, Debug, Eq, PartialEq)] +pub struct TEdgeConnectOpt { + /// The cloud you wish to connect to, e.g. `c8y`, `az`, or `aws` + cloud: Cloud, - /// Ignore connection registration and connection check - #[clap(long = "offline")] - offline_mode: bool, + /// Test an existing connection + #[clap(long = "test")] + is_test_connection: bool, - #[clap(long, hide = true)] - profile: Option, - }, + /// Ignore connection registration and connection check + #[clap(long = "offline")] + offline_mode: bool, } impl BuildCommand for TEdgeConnectOpt { fn build_command(self, context: BuildContext) -> Result, crate::ConfigError> { - Ok(match self { - TEdgeConnectOpt::C8y { - is_test_connection, - offline_mode, - profile, - } => ConnectCommand { - config_location: context.config_location.clone(), - config: context.load_config()?, - cloud: Cloud::C8y, - is_test_connection, - offline_mode, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - profile, - is_reconnect: false, - }, - TEdgeConnectOpt::Az { - is_test_connection, - offline_mode, - profile, - } => ConnectCommand { - config_location: context.config_location.clone(), - config: context.load_config()?, - cloud: Cloud::Azure, - is_test_connection, - offline_mode, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - profile, - is_reconnect: false, - }, - TEdgeConnectOpt::Aws { - is_test_connection, - offline_mode, - profile, - } => ConnectCommand { - config_location: context.config_location.clone(), - config: context.load_config()?, - cloud: Cloud::Aws, - is_test_connection, - offline_mode, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - profile, - is_reconnect: false, - }, - } - .into_boxed()) + let Self { + is_test_connection, + offline_mode, + cloud, + } = self; + Ok(Box::new(ConnectCommand { + config_location: context.config_location.clone(), + config: context.load_config()?, + cloud, + is_test_connection, + offline_mode, + service_manager: service_manager(&context.config_location.tedge_config_root_path)?, + is_reconnect: false, + })) } } diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 5102753d6b..935ed5aaf6 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -5,6 +5,7 @@ use crate::bridge::BridgeConfig; use crate::bridge::BridgeLocation; use crate::bridge::CommonMosquittoConfig; use crate::cli::common::Cloud; +use crate::cli::common::MaybeBorrowedCloud; use crate::cli::connect::jwt_token::*; use crate::cli::connect::*; use crate::cli::log::ConfigLogger; @@ -24,6 +25,7 @@ use rumqttc::Incoming; use rumqttc::Outgoing; use rumqttc::Packet; use rumqttc::QoS::AtLeastOnce; +use std::borrow::Cow; use std::net::TcpStream; use std::net::ToSocketAddrs; use std::path::Path; @@ -58,7 +60,6 @@ pub struct ConnectCommand { pub is_test_connection: bool, pub offline_mode: bool, pub service_manager: Arc, - pub profile: Option, pub is_reconnect: bool, } @@ -84,14 +85,14 @@ impl Command for ConnectCommand { impl ConnectCommand { fn execute_inner(&self) -> Result<(), Fancy> { let config = &self.config; - let bridge_config = bridge_config(config, self.cloud, self.profile.as_ref())?; + let bridge_config = bridge_config(config, &self.cloud)?; let updated_mosquitto_config = CommonMosquittoConfig::from_tedge_config(config); if self.is_test_connection { // If the bridge is part of the mapper, the bridge config file won't exist // TODO tidy me up once mosquitto is no longer required for bridge return if self.check_if_bridge_exists(&bridge_config) { - match self.check_connection(config, self.profile.as_ref()) { + match self.check_connection(config) { Ok(DeviceStatus::AlreadyExists) => { let cloud = bridge_config.cloud_name; match self.tenant_matches_configured_url(config)? { @@ -153,11 +154,9 @@ impl ConnectCommand { let mut connection_check_success = true; if !self.offline_mode { - match self.check_connection_with_retries( - config, - bridge_config.connection_check_attempts, - self.profile.as_ref(), - ) { + match self + .check_connection_with_retries(config, bridge_config.connection_check_attempts) + { Ok(DeviceStatus::AlreadyExists) => {} _ => { warning!( @@ -175,8 +174,8 @@ impl ConnectCommand { self.start_mapper(); } - if let Cloud::C8y = self.cloud { - let c8y_config = config.c8y.try_get(self.profile.as_deref())?; + if let Cloud::C8y(profile) = &self.cloud { + let c8y_config = config.c8y.try_get(profile.as_deref())?; let use_basic_auth = c8y_config .auth_method @@ -196,8 +195,8 @@ impl ConnectCommand { &self, config: &TEdgeConfig, ) -> Result, Fancy> { - if let Cloud::C8y = self.cloud { - let c8y_config = config.c8y.try_get(self.profile.as_deref())?; + if let Cloud::C8y(profile) = &self.cloud { + let c8y_config = config.c8y.try_get(profile.as_deref())?; let use_basic_auth = c8y_config .auth_method @@ -205,7 +204,7 @@ impl ConnectCommand { if !use_basic_auth && !self.offline_mode { tenant_matches_configured_url( config, - self.profile.as_deref(), + profile.as_ref().map(|g| &***g), &c8y_config .mqtt .or_none() @@ -225,10 +224,9 @@ impl ConnectCommand { &self, config: &TEdgeConfig, max_attempts: i32, - profile: Option<&ProfileName>, ) -> Result> { for i in 1..max_attempts { - let result = self.check_connection(config, profile); + let result = self.check_connection(config); if let Ok(DeviceStatus::AlreadyExists) = result { return result; } @@ -238,18 +236,14 @@ impl ConnectCommand { ); std::thread::sleep(std::time::Duration::from_secs(2)); } - self.check_connection(config, profile) + self.check_connection(config) } - fn check_connection( - &self, - config: &TEdgeConfig, - profile: Option<&ProfileName>, - ) -> Result> { + fn check_connection(&self, config: &TEdgeConfig) -> Result> { let spinner = Spinner::start("Verifying device is connected to cloud"); - let res = match self.cloud { - Cloud::Azure => check_device_status_azure(config, profile), - Cloud::Aws => check_device_status_aws(config, profile), - Cloud::C8y => check_device_status_c8y(config, profile), + let res = match &self.cloud { + Cloud::Azure(profile) => check_device_status_azure(config, profile.as_deref()), + Cloud::Aws(profile) => check_device_status_aws(config, profile.as_deref()), + Cloud::C8y(profile) => check_device_status_c8y(config, profile.as_deref()), }; spinner.finish(res) } @@ -280,52 +274,53 @@ impl ConnectCommand { pub fn bridge_config( config: &TEdgeConfig, - cloud: self::Cloud, - profile: Option<&ProfileName>, + cloud: &MaybeBorrowedCloud<'_>, ) -> Result { let bridge_location = match config.mqtt.bridge.built_in { true => BridgeLocation::BuiltIn, false => BridgeLocation::Mosquitto, }; match cloud { - Cloud::Azure => { - let az_config = config.az.try_get(profile)?; + MaybeBorrowedCloud::Azure(profile) => { + let az_config = config.az.try_get(profile.as_deref())?; let params = BridgeConfigAzureParams { mqtt_host: HostPort::::try_from( az_config.url.or_config_not_set()?.as_str(), ) .map_err(TEdgeConfigError::from)?, - config_file: Cloud::Azure.bridge_config_filename(profile), + config_file: cloud.bridge_config_filename(), bridge_root_cert_path: az_config.root_cert_path.clone(), remote_clientid: config.device.id.try_read(config)?.clone(), bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), bridge_location, topic_prefix: az_config.bridge.topic_prefix.clone(), + profile_name: profile.as_ref().map(<_>::clone).map(Cow::into_owned), }; Ok(BridgeConfig::from(params)) } - Cloud::Aws => { - let aws_config = config.aws.try_get(profile)?; + MaybeBorrowedCloud::Aws(profile) => { + let aws_config = config.aws.try_get(profile.as_deref())?; let params = BridgeConfigAwsParams { mqtt_host: HostPort::::try_from( aws_config.url.or_config_not_set()?.as_str(), ) .map_err(TEdgeConfigError::from)?, - config_file: Cloud::Aws.bridge_config_filename(profile), + config_file: cloud.bridge_config_filename(), bridge_root_cert_path: aws_config.root_cert_path.clone(), remote_clientid: config.device.id.try_read(config)?.clone(), bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), bridge_location, topic_prefix: aws_config.bridge.topic_prefix.clone(), + profile_name: profile.as_ref().map(<_>::clone).map(Cow::into_owned), }; Ok(BridgeConfig::from(params)) } - Cloud::C8y => { - let c8y_config = config.c8y.try_get(profile)?; + MaybeBorrowedCloud::C8y(profile) => { + let c8y_config = config.c8y.try_get(profile.as_deref())?; let (remote_username, remote_password) = match c8y_config.auth_method.to_type(&c8y_config.credentials_path) { @@ -339,7 +334,7 @@ pub fn bridge_config( let params = BridgeConfigC8yParams { mqtt_host: c8y_config.mqtt.or_config_not_set()?.clone(), - config_file: Cloud::C8y.bridge_config_filename(profile), + config_file: cloud.bridge_config_filename(), bridge_root_cert_path: c8y_config.root_cert_path.clone(), remote_clientid: config.device.id.try_read(config)?.clone(), remote_username, @@ -351,6 +346,7 @@ pub fn bridge_config( include_local_clean_session: c8y_config.bridge.include.local_cleansession.clone(), bridge_location, topic_prefix: c8y_config.bridge.topic_prefix.clone(), + profile_name: profile.as_ref().map(<_>::clone).map(Cow::into_owned), }; Ok(BridgeConfig::from(params)) diff --git a/crates/core/tedge/src/cli/disconnect/cli.rs b/crates/core/tedge/src/cli/disconnect/cli.rs index 7781debd36..2a72b59c26 100644 --- a/crates/core/tedge/src/cli/disconnect/cli.rs +++ b/crates/core/tedge/src/cli/disconnect/cli.rs @@ -2,51 +2,20 @@ use crate::cli::common::Cloud; use crate::cli::disconnect::disconnect_bridge::*; use crate::command::*; use tedge_config::system_services::service_manager; -use tedge_config::ProfileName; -#[derive(clap::Subcommand, Debug)] -pub enum TEdgeDisconnectBridgeCli { - /// Remove bridge connection to Cumulocity. - C8y { - #[clap(long, hide = true)] - profile: Option, - }, - /// Remove bridge connection to Azure. - Az { - #[clap(long, hide = true)] - profile: Option, - }, - /// Remove bridge connection to AWS. - Aws { - #[clap(long, hide = true)] - profile: Option, - }, +#[derive(clap::Args, Debug)] +pub struct TEdgeDisconnectBridgeCli { + /// The cloud to remove the connection from + cloud: Cloud, } impl BuildCommand for TEdgeDisconnectBridgeCli { fn build_command(self, context: BuildContext) -> Result, crate::ConfigError> { - let cmd = match self { - TEdgeDisconnectBridgeCli::C8y { profile } => DisconnectBridgeCommand { - config_location: context.config_location.clone(), - profile, - cloud: Cloud::C8y, - use_mapper: true, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - }, - TEdgeDisconnectBridgeCli::Az { profile } => DisconnectBridgeCommand { - config_location: context.config_location.clone(), - profile, - cloud: Cloud::Azure, - use_mapper: true, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - }, - TEdgeDisconnectBridgeCli::Aws { profile } => DisconnectBridgeCommand { - config_location: context.config_location.clone(), - profile, - cloud: Cloud::Aws, - use_mapper: true, - service_manager: service_manager(&context.config_location.tedge_config_root_path)?, - }, + let cmd = DisconnectBridgeCommand { + config_location: context.config_location.clone(), + cloud: self.cloud, + use_mapper: true, + service_manager: service_manager(&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 a9b0b50dc8..739ef04bd7 100644 --- a/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs +++ b/crates/core/tedge/src/cli/disconnect/disconnect_bridge.rs @@ -7,7 +7,6 @@ use crate::log::MaybeFancy; use anyhow::Context; use std::sync::Arc; use tedge_config::system_services::*; -use tedge_config::ProfileName; use tedge_config::TEdgeConfigLocation; use which::which; @@ -16,7 +15,6 @@ const TEDGE_BRIDGE_CONF_DIR_PATH: &str = "mosquitto-conf"; #[derive(Debug)] pub struct DisconnectBridgeCommand { pub config_location: TEdgeConfigLocation, - pub profile: Option, pub cloud: Cloud, pub use_mapper: bool, pub service_manager: Arc, @@ -91,7 +89,7 @@ impl DisconnectBridgeCommand { } fn remove_bridge_config_file(&self) -> Result<(), DisconnectBridgeError> { - let config_file = self.cloud.bridge_config_filename(self.profile.as_ref()); + let config_file = self.cloud.bridge_config_filename(); let bridge_conf_path = self .config_location .tedge_config_root_path diff --git a/crates/core/tedge/src/cli/mod.rs b/crates/core/tedge/src/cli/mod.rs index 94b4148139..bf0428dc92 100644 --- a/crates/core/tedge/src/cli/mod.rs +++ b/crates/core/tedge/src/cli/mod.rs @@ -106,16 +106,13 @@ pub enum TEdgeOpt { #[clap(subcommand)] Config(config::ConfigCmd), - /// Connect to connector provider - #[clap(subcommand)] + /// Connect to cloud provider Connect(connect::TEdgeConnectOpt), /// Remove bridge connection for a provider - #[clap(subcommand)] Disconnect(disconnect::TEdgeDisconnectBridgeCli), /// Reconnect command, calls disconnect followed by connect - #[clap(subcommand)] Reconnect(reconnect::TEdgeReconnectCli), /// Refresh all currently active mosquitto bridges diff --git a/crates/core/tedge/src/cli/reconnect/cli.rs b/crates/core/tedge/src/cli/reconnect/cli.rs index 22feb57c01..190a52dfb7 100644 --- a/crates/core/tedge/src/cli/reconnect/cli.rs +++ b/crates/core/tedge/src/cli/reconnect/cli.rs @@ -2,59 +2,22 @@ use super::command::ReconnectBridgeCommand; use crate::cli::common::Cloud; use crate::command::*; use tedge_config::system_services::service_manager; -use tedge_config::ProfileName; -#[derive(clap::Subcommand, Debug)] -pub enum TEdgeReconnectCli { - /// Remove bridge connection to Cumulocity. - C8y { - #[clap(long, hide = true)] - profile: Option, - }, - /// Remove bridge connection to Azure. - Az { - #[clap(long, hide = true)] - profile: Option, - }, - /// Remove bridge connection to AWS. - Aws { - #[clap(long, hide = true)] - profile: Option, - }, +#[derive(clap::Args, Debug)] +pub struct TEdgeReconnectCli { + /// The cloud you wish to re-establish the connection to + cloud: Cloud, } impl BuildCommand for TEdgeReconnectCli { fn build_command(self, context: BuildContext) -> Result, crate::ConfigError> { - let config_location = context.config_location.clone(); - let config = context.load_config()?; - let service_manager = service_manager(&context.config_location.tedge_config_root_path)?; - - let cmd = match self { - TEdgeReconnectCli::C8y { profile } => ReconnectBridgeCommand { - config_location, - config, - service_manager, - cloud: Cloud::C8y, - use_mapper: true, - profile, - }, - TEdgeReconnectCli::Az { profile } => ReconnectBridgeCommand { - config_location, - config, - service_manager, - cloud: Cloud::Azure, - use_mapper: true, - profile, - }, - TEdgeReconnectCli::Aws { profile } => ReconnectBridgeCommand { - config_location, - config, - service_manager, - cloud: Cloud::Aws, - use_mapper: true, - profile, - }, - }; - Ok(cmd.into_boxed()) + Ok(ReconnectBridgeCommand { + config: context.load_config()?, + service_manager: service_manager(&context.config_location.tedge_config_root_path)?, + config_location: context.config_location, + cloud: self.cloud, + use_mapper: true, + } + .into_boxed()) } } diff --git a/crates/core/tedge/src/cli/reconnect/command.rs b/crates/core/tedge/src/cli/reconnect/command.rs index ca16ea8c77..7f1b7de991 100644 --- a/crates/core/tedge/src/cli/reconnect/command.rs +++ b/crates/core/tedge/src/cli/reconnect/command.rs @@ -5,7 +5,6 @@ use crate::command::Command; use crate::log::MaybeFancy; use std::sync::Arc; use tedge_config::system_services::SystemServiceManager; -use tedge_config::ProfileName; use tedge_config::TEdgeConfig; use tedge_config::TEdgeConfigLocation; @@ -15,7 +14,6 @@ pub struct ReconnectBridgeCommand { pub cloud: Cloud, pub use_mapper: bool, pub service_manager: Arc, - pub profile: Option, } impl Command for ReconnectBridgeCommand { @@ -39,8 +37,7 @@ impl From<&ReconnectBridgeCommand> for DisconnectBridgeCommand { fn from(reconnect_cmd: &ReconnectBridgeCommand) -> Self { DisconnectBridgeCommand { config_location: reconnect_cmd.config_location.clone(), - profile: reconnect_cmd.profile.clone(), - cloud: reconnect_cmd.cloud, + cloud: reconnect_cmd.cloud.clone(), use_mapper: reconnect_cmd.use_mapper, service_manager: reconnect_cmd.service_manager.clone(), } @@ -52,11 +49,10 @@ impl From<&ReconnectBridgeCommand> for ConnectCommand { ConnectCommand { config_location: reconnect_cmd.config_location.clone(), config: reconnect_cmd.config.clone(), - cloud: reconnect_cmd.cloud, + cloud: reconnect_cmd.cloud.clone(), is_test_connection: false, offline_mode: false, service_manager: reconnect_cmd.service_manager.clone(), - profile: reconnect_cmd.profile.clone(), is_reconnect: true, } } diff --git a/crates/core/tedge/src/cli/refresh_bridges.rs b/crates/core/tedge/src/cli/refresh_bridges.rs index de95a50235..f755fc8e2f 100644 --- a/crates/core/tedge/src/cli/refresh_bridges.rs +++ b/crates/core/tedge/src/cli/refresh_bridges.rs @@ -1,13 +1,11 @@ -use std::sync::Arc; - use camino::Utf8PathBuf; +use std::sync::Arc; use tedge_config::system_services::SystemService; use tedge_config::system_services::SystemServiceManager; -use tedge_config::ProfileName; use tedge_config::TEdgeConfig; use tedge_config::TEdgeConfigLocation; -use super::common::Cloud; +use super::common::CloudBorrow; use super::connect::ConnectError; use super::log::MaybeFancy; use crate::bridge::BridgeConfig; @@ -62,22 +60,22 @@ impl RefreshBridgesCmd { common_mosquitto_config.save(&self.config_location)?; if !self.config.mqtt.bridge.built_in { - for (cloud, profile) in &clouds { + for cloud in &clouds { println!("Refreshing bridge {cloud}"); - let bridge_config = super::connect::bridge_config(&self.config, *cloud, *profile)?; + let bridge_config = super::connect::bridge_config(&self.config, &cloud)?; refresh_bridge(&bridge_config, &self.config_location)?; } } - for (cloud, profile) in possible_clouds(&self.config) { + for cloud in possible_clouds(&self.config) { // (attempt to) reassert ownership of the certificate and key // This is necessary when upgrading from the mosquitto bridge to the built-in bridge - if let Ok(bridge_config) = super::connect::bridge_config(&self.config, cloud, profile) { + if let Ok(bridge_config) = super::connect::bridge_config(&self.config, &cloud) { super::connect::chown_certificate_and_key(&bridge_config); if bridge_config.bridge_location == BridgeLocation::BuiltIn - && clouds.contains(&(cloud, profile)) + && clouds.contains(&cloud) { println!( "Deleting mosquitto bridge configuration for {cloud} in favour of built-in bridge" @@ -98,22 +96,20 @@ impl RefreshBridgesCmd { fn established_bridges<'a>( config_location: &TEdgeConfigLocation, config: &'a TEdgeConfig, -) -> Vec<(Cloud, Option<&'a ProfileName>)> { +) -> Vec> { // if the bridge configuration file doesn't exist, then the bridge doesn't exist and we shouldn't try to update it possible_clouds(config) - .filter(|(cloud, profile)| { - get_bridge_config_file_path_cloud(config_location, *cloud, *profile).exists() - }) + .filter(|cloud| get_bridge_config_file_path_cloud(config_location, cloud).exists()) .collect() } -fn possible_clouds(config: &TEdgeConfig) -> impl Iterator)> { +fn possible_clouds<'a>(config: &'a TEdgeConfig) -> impl Iterator> { config .c8y .keys() - .map(|profile| (Cloud::C8y, profile)) - .chain(config.az.keys().map(|profile| (Cloud::Azure, profile))) - .chain(config.aws.keys().map(|profile| (Cloud::Aws, profile))) + .map(CloudBorrow::c8y_borrowed) + .chain(config.az.keys().map(CloudBorrow::az_borrowed)) + .chain(config.aws.keys().map(CloudBorrow::aws_borrowed)) } pub fn refresh_bridge( @@ -128,11 +124,10 @@ pub fn refresh_bridge( pub fn get_bridge_config_file_path_cloud( config_location: &TEdgeConfigLocation, - cloud: Cloud, - profile: Option<&ProfileName>, + cloud: &CloudBorrow<'_>, ) -> Utf8PathBuf { config_location .tedge_config_root_path .join(TEDGE_BRIDGE_CONF_DIR_PATH) - .join(&*cloud.bridge_config_filename(profile)) + .join(&*cloud.bridge_config_filename()) } diff --git a/crates/core/tedge/src/error.rs b/crates/core/tedge/src/error.rs index 874734db12..12de5e6882 100644 --- a/crates/core/tedge/src/error.rs +++ b/crates/core/tedge/src/error.rs @@ -38,4 +38,7 @@ pub enum TEdgeError { #[error(transparent)] FromCredentialsFileError(#[from] c8y_api::http_proxy::CredentialsFileError), + + #[error(transparent)] + FromAnyhow(#[from] anyhow::Error), } diff --git a/crates/core/tedge/src/main.rs b/crates/core/tedge/src/main.rs index 3d1669fcaf..19283b421d 100644 --- a/crates/core/tedge/src/main.rs +++ b/crates/core/tedge/src/main.rs @@ -3,6 +3,8 @@ use anyhow::Context; use cap::Cap; +use clap::error::ErrorFormatter; +use clap::error::RichFormatter; use clap::Parser; use std::alloc; use std::future::Future; @@ -129,5 +131,13 @@ fn parse_multicall_if_known(executable_name: &Option) -> T { .as_deref() .map_or(false, |name| cmd.find_subcommand(name).is_some()); let cmd = cmd.multicall(is_known_subcommand); - T::from_arg_matches(&cmd.get_matches()).expect("get_matches panics if invalid arguments are provided, so we won't have arg matches to convert") + + let cmd2 = cmd.clone(); + match T::from_arg_matches(&cmd.get_matches()) { + Ok(t) => t, + Err(e) => { + eprintln!("{}", RichFormatter::format_error(&e.with_cmd(&cmd2))); + std::process::exit(1); + } + } } diff --git a/crates/core/tedge_mapper/src/lib.rs b/crates/core/tedge_mapper/src/lib.rs index 55e74b7803..b83b53e9bc 100644 --- a/crates/core/tedge_mapper/src/lib.rs +++ b/crates/core/tedge_mapper/src/lib.rs @@ -3,10 +3,14 @@ use crate::az::mapper::AzureMapper; use crate::c8y::mapper::CumulocityMapper; use crate::collectd::mapper::CollectdMapper; use crate::core::component::TEdgeComponent; +use anyhow::bail; use anyhow::Context; +use clap::Command; +use clap::FromArgMatches; use clap::Parser; use flockfile::check_another_instance_is_not_running; use std::fmt; +use std::str::FromStr; use tedge_config::get_config_dir; use tedge_config::system_services::log_init; use tedge_config::system_services::LogConfigArgs; @@ -36,19 +40,16 @@ macro_rules! read_and_set_var { }; } -fn lookup_component( - component_name: &MapperName, - profile: Option, -) -> Box { +fn lookup_component(component_name: MapperName) -> Box { match component_name { - MapperName::Az => Box::new(AzureMapper { + MapperName::Az(profile) => Box::new(AzureMapper { profile: read_and_set_var!(profile, "AZ_PROFILE"), }), - MapperName::Aws => Box::new(AwsMapper { + MapperName::Aws(profile) => Box::new(AwsMapper { profile: read_and_set_var!(profile, "AWS_PROFILE"), }), MapperName::Collectd => Box::new(CollectdMapper), - MapperName::C8y => Box::new(CumulocityMapper { + MapperName::C8y(profile) => Box::new(CumulocityMapper { profile: read_and_set_var!(profile, "C8Y_PROFILE"), }), } @@ -87,32 +88,82 @@ pub struct MapperOpt { hide_default_value = true, )] pub config_dir: PathBuf, - - #[clap(long, global = true, hide = true)] - pub profile: Option, } -#[derive(Debug, clap::Subcommand)] +#[derive(Debug, Clone)] pub enum MapperName { - Az, - Aws, - C8y, + Az(Option), + Aws(Option), + C8y(Option), Collectd, } +impl FromStr for MapperName { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match (s, s.split_once("@")) { + ("az", _) => Ok(Self::Az(None)), + ("aws", _) => Ok(Self::Aws(None)), + ("c8y", _) => Ok(Self::C8y(None)), + ("collectd", _) => Ok(Self::Collectd), + (_, Some(("az", profile))) => Ok(Self::Az(Some(profile.parse()?))), + (_, Some(("aws", profile))) => Ok(Self::Aws(Some(profile.parse()?))), + (_, Some(("c8y", profile))) => Ok(Self::C8y(Some(profile.parse()?))), + _ => bail!("Unknown subcommand `{s}`"), + } + } +} + +impl FromArgMatches for MapperName { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + match matches.subcommand() { + Some((cmd, _)) => cmd.parse() + .map_err(|_| clap::Error::raw(clap::error::ErrorKind::InvalidSubcommand, "Valid subcommands are `c8y`, `aws` and `az`")), + None => Err(clap::Error::raw( + clap::error::ErrorKind::MissingSubcommand, + "Valid subcommands are `c8y`, `aws` and `az`", + )), + } + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + todo!() + } +} + +impl clap::Subcommand for MapperName { + fn augment_subcommands(cmd: clap::Command) -> clap::Command { + cmd.subcommand(Command::new("c8y")) + .subcommand(Command::new("c8y@")) + .subcommand_required(true) + .allow_external_subcommands(true) + } + + fn augment_subcommands_for_update(cmd: clap::Command) -> clap::Command { + todo!() + } + + fn has_subcommand(name: &str) -> bool { + name.parse::().is_ok() + } +} + impl fmt::Display for MapperName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - MapperName::Az => write!(f, "tedge-mapper-az"), - MapperName::Aws => write!(f, "tedge-mapper-aws"), - MapperName::C8y => write!(f, "tedge-mapper-c8y"), + MapperName::Az(None) => write!(f, "tedge-mapper-az"), + MapperName::Az(Some(profile)) => write!(f, "tedge-mapper-az@{profile}"), + MapperName::Aws(None) => write!(f, "tedge-mapper-aws"), + MapperName::Aws(Some(profile)) => write!(f, "tedge-mapper-aws@{profile}"), + MapperName::C8y(None) => write!(f, "tedge-mapper-c8y"), + MapperName::C8y(Some(profile)) => write!(f, "tedge-mapper-c8y@{profile}"), MapperName::Collectd => write!(f, "tedge-mapper-collectd"), } } } pub async fn run(mapper_opt: MapperOpt) -> anyhow::Result<()> { - let component = lookup_component(&mapper_opt.name, mapper_opt.profile.clone()); + let component = lookup_component(mapper_opt.name.clone()); let tedge_config_location = tedge_config::TEdgeConfigLocation::from_custom_root(&mapper_opt.config_dir);