diff options
author | Lukasz Woznicki <75632179+makr11st@users.noreply.github.com> | 2021-11-24 20:54:56 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-24 20:54:56 +0000 |
commit | a4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch) | |
tree | 9583f187114913a92866571920dd3bb205bd50a3 /crates/core/tedge/src/cli/connect | |
parent | 8217e80670e76dbf9168780f5e0545355a39f8f3 (diff) |
Restructure directories of the workspace (#559)
* Restructure directories of the workspace
* Rename c8y_translator_lib to c8y_translator
* Update comment on how to get dummy plugin path
Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/tedge/src/cli/connect')
-rw-r--r-- | crates/core/tedge/src/cli/connect/bridge_config.rs | 369 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/bridge_config_azure.rs | 111 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/bridge_config_c8y.rs | 144 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/cli.rs | 48 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/command.rs | 569 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/common_mosquitto_config.rs | 248 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/error.rs | 41 | ||||
-rw-r--r-- | crates/core/tedge/src/cli/connect/mod.rs | 12 |
8 files changed, 1542 insertions, 0 deletions
diff --git a/crates/core/tedge/src/cli/connect/bridge_config.rs b/crates/core/tedge/src/cli/connect/bridge_config.rs new file mode 100644 index 00000000..b819a64b --- /dev/null +++ b/crates/core/tedge/src/cli/connect/bridge_config.rs @@ -0,0 +1,369 @@ +use crate::cli::connect::ConnectError; + +use tedge_config::FilePath; +use url::Url; + +#[derive(Debug, PartialEq)] +pub struct BridgeConfig { + pub cloud_name: String, + pub config_file: String, + pub connection: String, + pub address: String, + pub remote_username: Option<String>, + pub bridge_root_cert_path: FilePath, + pub remote_clientid: String, + pub local_clientid: String, + pub bridge_certfile: FilePath, + pub bridge_keyfile: FilePath, + pub use_mapper: bool, + pub use_agent: bool, + pub try_private: bool, + pub start_type: String, + pub clean_session: bool, + pub notifications: bool, + pub bridge_attempt_unsubscribe: bool, + pub topics: Vec<String>, +} + +impl BridgeConfig { + pub fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> { + writeln!(writer, "### Bridge")?; + writeln!(writer, "connection {}", self.connection)?; + match &self.remote_username { + Some(name) => { + writeln!(writer, "remote_username {}", name)?; + } + None => {} + } + writeln!(writer, "address {}", self.address)?; + + // XXX: This has to go away + if std::fs::metadata(&self.bridge_root_cert_path)?.is_dir() { + writeln!(writer, "bridge_capath {}", self.bridge_root_cert_path)?; + } else { + writeln!(writer, "bridge_cafile {}", self.bridge_root_cert_path)?; + } + + writeln!(writer, "remote_clientid {}", self.remote_clientid)?; + writeln!(writer, "local_clientid {}", self.local_clientid)?; + writeln!(writer, "bridge_certfile {}", self.bridge_certfile)?; + writeln!(writer, "bridge_keyfile {}", self.bridge_keyfile)?; + writeln!(writer, "try_private {}", self.try_private)?; + writeln!(writer, "start_type {}", self.start_type)?; + writeln!(writer, "cleansession {}", self.clean_session)?; + writeln!(writer, "notifications {}", self.notifications)?; + writeln!( + writer, + "bridge_attempt_unsubscribe {}", + self.bridge_attempt_unsubscribe + )?; + + writeln!(writer, "\n### Topics",)?; + for topic in &self.topics { + writeln!(writer, "topic {}", topic)?; + } + + Ok(()) + } + + pub fn validate(&self) -> Result<(), ConnectError> { + // XXX: This is actually wrong. Our address looks like this: `domain:port` + // `Url::parse` will treat `domain` as `schema` ... + Url::parse(&self.address)?; + + if !self.bridge_root_cert_path.as_ref().exists() { + return Err(ConnectError::Certificate); + } + + if !self.bridge_certfile.as_ref().exists() { + return Err(ConnectError::Certificate); + } + + if !self.bridge_keyfile.as_ref().exists() { + return Err(ConnectError::Certificate); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_serialize_with_cafile_correctly() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; + let bridge_root_cert_path: FilePath = file.path().into(); + + let bridge = BridgeConfig { + cloud_name: "test".into(), + config_file: "test-bridge.conf".into(), + connection: "edge_to_test".into(), + address: "test.test.io:8883".into(), + remote_username: None, + bridge_root_cert_path: bridge_root_cert_path.clone(), + remote_clientid: "alpha".into(), + local_clientid: "test".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: false, + use_agent: false, + topics: vec![], + try_private: false, + start_type: "automatic".into(), + clean_session: true, + notifications: false, + bridge_attempt_unsubscribe: false, + }; + + let mut serialized_config = Vec::<u8>::new(); + bridge.serialize(&mut serialized_config)?; + + let bridge_cafile = format!("bridge_cafile {}", bridge_root_cert_path); + let mut expected = r#"### Bridge +connection edge_to_test +address test.test.io:8883 +"# + .to_owned(); + + expected.push_str(&bridge_cafile); + expected.push_str( + r#" +remote_clientid alpha +local_clientid test +bridge_certfile ./test-certificate.pem +bridge_keyfile ./test-private-key.pem +try_private false +start_type automatic +cleansession true +notifications false +bridge_attempt_unsubscribe false + +### Topics +"#, + ); + + assert_eq!(serialized_config, expected.as_bytes()); + + Ok(()) + } + + #[test] + fn test_serialize_with_capath_correctly() -> anyhow::Result<()> { + let dir = tempfile::TempDir::new()?; + let bridge_root_cert_path: FilePath = dir.path().into(); + + let bridge = BridgeConfig { + cloud_name: "test".into(), + config_file: "test-bridge.conf".into(), + connection: "edge_to_test".into(), + address: "test.test.io:8883".into(), + remote_username: None, + bridge_root_cert_path: bridge_root_cert_path.clone(), + remote_clientid: "alpha".into(), + local_clientid: "test".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: false, + use_agent: false, + topics: vec![], + try_private: false, + start_type: "automatic".into(), + clean_session: true, + notifications: false, + bridge_attempt_unsubscribe: false, + }; + let mut serialized_config = Vec::<u8>::new(); + bridge.serialize(&mut serialized_config)?; + + let bridge_capath = format!("bridge_capath {}", bridge_root_cert_path); + let mut expected = r#"### Bridge +connection edge_to_test +address test.test.io:8883 +"# + .to_owned(); + + expected.push_str(&bridge_capath); + expected.push_str( + r#" +remote_clientid alpha +local_clientid test +bridge_certfile ./test-certificate.pem +bridge_keyfile ./test-private-key.pem +try_private false +start_type automatic +cleansession true +notifications false +bridge_attempt_unsubscribe false + +### Topics +"#, + ); + + assert_eq!(serialized_config, expected.as_bytes()); + + Ok(()) + } + + #[test] + fn test_serialize() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; + let bridge_root_cert_path: FilePath = file.path().into(); + + let config = BridgeConfig { + cloud_name: "az".into(), + config_file: "az-bridge.conf".into(), + connection: "edge_to_az".into(), + address: "test.test.io:8883".into(), + remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), + bridge_root_cert_path: bridge_root_cert_path.clone(), + remote_clientid: "alpha".into(), + local_clientid: "Azure".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: false, + use_agent: false, + topics: vec![ + r#"messages/events/ out 1 az/ devices/alpha/"#.into(), + r##"messages/devicebound/# out 1 az/ devices/alpha/"##.into(), + ], + try_private: false, + start_type: "automatic".into(), + clean_session: true, + notifications: false, + bridge_attempt_unsubscribe: false, + }; + + let mut buffer = Vec::new(); + config.serialize(&mut buffer)?; + + let contents = String::from_utf8(buffer)?; + let config_set: std::collections::HashSet<&str> = contents + .lines() + .filter(|str| !str.is_empty() && !str.starts_with('#')) + .collect(); + + let mut expected = std::collections::HashSet::new(); + expected.insert("connection edge_to_az"); + expected.insert("remote_username test.test.io/alpha/?api-version=2018-06-30"); + expected.insert("address test.test.io:8883"); + let bridge_capath = format!("bridge_cafile {}", bridge_root_cert_path); + expected.insert(&bridge_capath); + expected.insert("remote_clientid alpha"); + expected.insert("local_clientid Azure"); + expected.insert("bridge_certfile ./test-certificate.pem"); + expected.insert("bridge_keyfile ./test-private-key.pem"); + expected.insert("start_type automatic"); + expected.insert("try_private false"); + expected.insert("cleansession true"); + expected.insert("notifications false"); + expected.insert("bridge_attempt_unsubscribe false"); + + expected.insert("topic messages/events/ out 1 az/ devices/alpha/"); + expected.insert("topic messages/devicebound/# out 1 az/ devices/alpha/"); + assert_eq!(config_set, expected); + Ok(()) + } + + #[test] + fn test_validate_ok() -> anyhow::Result<()> { + let ca_file = tempfile::NamedTempFile::new()?; + let bridge_ca_path: FilePath = ca_file.path().into(); + + let cert_file = tempfile::NamedTempFile::new()?; + let bridge_certfile: FilePath = cert_file.path().into(); + + let key_file = tempfile::NamedTempFile::new()?; + let bridge_keyfile: FilePath = key_file.path().into(); + + let correct_url = "http://test.com"; + + let config = BridgeConfig { + address: correct_url.into(), + bridge_root_cert_path: bridge_ca_path, + bridge_certfile, + bridge_keyfile, + ..default_bridge_config() + }; + + assert!(config.validate().is_ok()); + + Ok(()) + } + + // XXX: This test is flawed as it is not clear what it tests. + // It can fail due to either `incorrect_url` OR `non_existent_path`. + #[test] + fn test_validate_wrong_url() { + let incorrect_url = "noturl"; + let non_existent_path = "/path/that/does/not/exist"; + + let config = BridgeConfig { + address: incorrect_url.into(), + bridge_certfile: non_existent_path.into(), + bridge_keyfile: non_existent_path.into(), + ..default_bridge_config() + }; + + assert!(config.validate().is_err()); + } + + #[test] + fn test_validate_wrong_cert_path() { + let correct_url = "http://test.com"; + let non_existent_path = "/path/that/does/not/exist"; + + let config = BridgeConfig { + address: correct_url.into(), + bridge_certfile: non_existent_path.into(), + bridge_keyfile: non_existent_path.into(), + ..default_bridge_config() + }; + + assert!(config.validate().is_err()); + } + + #[test] + fn test_validate_wrong_key_path() -> anyhow::Result<()> { + let cert_file = tempfile::NamedTempFile::new()?; + let bridge_certfile: FilePath = cert_file.path().into(); + let correct_url = "http://test.com"; + let non_existent_path = "/path/that/does/not/exist"; + + let config = BridgeConfig { + address: correct_url.into(), + bridge_certfile, + bridge_keyfile: non_existent_path.into(), + ..default_bridge_config() + }; + + assert!(config.validate().is_err()); + + Ok(()) + } + + fn default_bridge_config() -> BridgeConfig { + BridgeConfig { + cloud_name: "az/c8y".into(), + config_file: "cfg".to_string(), + connection: "edge_to_az/c8y".into(), + address: "".into(), + remote_username: None, + bridge_root_cert_path: "".into(), + bridge_certfile: "".into(), + bridge_keyfile: "".into(), + remote_clientid: "".into(), + local_clientid: "".into(), + use_mapper: true, + use_agent: true, + try_private: false, + start_type: "automatic".into(), + clean_session: true, + notifications: false, + bridge_attempt_unsubscribe: false, + topics: vec![], + } + } +} diff --git a/crates/core/tedge/src/cli/connect/bridge_config_azure.rs b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs new file mode 100644 index 00000000..0aa5ac1b --- /dev/null +++ b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs @@ -0,0 +1,111 @@ +use crate::cli::connect::BridgeConfig; +use tedge_config::{ConnectUrl, FilePath}; + +#[derive(Debug, PartialEq)] +pub struct BridgeConfigAzureParams { + pub connect_url: ConnectUrl, + pub mqtt_tls_port: u16, + pub config_file: String, + pub remote_clientid: String, + pub bridge_root_cert_path: FilePath, + pub bridge_certfile: FilePath, + pub bridge_keyfile: FilePath, +} + +impl From<BridgeConfigAzureParams> for BridgeConfig { + fn from(params: BridgeConfigAzureParams) -> Self { + let BridgeConfigAzureParams { + connect_url, + mqtt_tls_port, + config_file, + bridge_root_cert_path, + remote_clientid, + bridge_certfile, + bridge_keyfile, + } = params; + + let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port); + let user_name = format!( + "{}/{}/?api-version=2018-06-30", + connect_url.as_str(), + remote_clientid + ); + let pub_msg_topic = format!("messages/events/ out 1 az/ devices/{}/", remote_clientid); + let sub_msg_topic = format!( + "messages/devicebound/# out 1 az/ devices/{}/", + remote_clientid + ); + Self { + cloud_name: "az".into(), + config_file, + connection: "edge_to_az".into(), + address, + remote_username: Some(user_name), + bridge_root_cert_path, + remote_clientid, + local_clientid: "Azure".into(), + bridge_certfile, + bridge_keyfile, + use_mapper: true, + use_agent: false, + try_private: false, + start_type: "automatic".into(), + clean_session: false, + notifications: false, + bridge_attempt_unsubscribe: false, + topics: vec![ + pub_msg_topic, + sub_msg_topic, + r##"twin/res/# in 1 az/ $iothub/"##.into(), + r#"twin/GET/?$rid=1 out 1 az/ $iothub/"#.into(), + ], + } + } +} + +#[test] +fn test_bridge_config_from_azure_params() -> anyhow::Result<()> { + use std::convert::TryFrom; + + let params = BridgeConfigAzureParams { + connect_url: ConnectUrl::try_from("test.test.io")?, + mqtt_tls_port: 8883, + config_file: "az-bridge.conf".into(), + remote_clientid: "alpha".into(), + bridge_root_cert_path: "./test_root.pem".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + }; + + let bridge = BridgeConfig::from(params); + + let expected = BridgeConfig { + cloud_name: "az".into(), + config_file: "az-bridge.conf".to_string(), + connection: "edge_to_az".into(), + address: "test.test.io:8883".into(), + remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), + bridge_root_cert_path: "./test_root.pem".into(), + remote_clientid: "alpha".into(), + local_clientid: "Azure".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: true, + use_agent: false, + topics: vec![ + r#"messages/events/ out 1 az/ devices/alpha/"#.into(), + r##"messages/devicebound/# out 1 az/ devices/alpha/"##.into(), + r##"twin/res/# in 1 az/ $iothub/"##.into(), + r#"twin/GET/?$rid=1 out 1 az/ $iothub/"#.into(), + ], + try_private: false, + start_type: "automatic".into(), + clean_session: false, + notifications: false, + bridge_attempt_unsubscribe: false, + }; + + assert_eq!(bridge, expected); + + Ok(()) +} diff --git a/crates/core/tedge/src/cli/connect/bridge_config_c8y.rs b/crates/core/tedge/src/cli/connect/bridge_config_c8y.rs new file mode 100644 index 00000000..574b1543 --- /dev/null +++ b/crates/core/tedge/src/cli/connect/bridge_config_c8y.rs @@ -0,0 +1,144 @@ +use crate::cli::connect::BridgeConfig; +use tedge_config::{ConnectUrl, FilePath}; + +#[derive(Debug, PartialEq)] +pub struct BridgeConfigC8yParams { + pub connect_url: ConnectUrl, + pub mqtt_tls_port: u16, + pub config_file: String, + pub remote_clientid: String, + pub bridge_root_cert_path: FilePath, + pub bridge_certfile: FilePath, + pub bridge_keyfile: FilePath, +} + +impl From<BridgeConfigC8yParams> for BridgeConfig { + fn from(params: BridgeConfigC8yParams) -> Self { + let BridgeConfigC8yParams { + connect_url, + mqtt_tls_port, + config_file, + bridge_root_cert_path, + remote_clientid, + bridge_certfile, + bridge_keyfile, + } = params; + let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port); + + Self { + cloud_name: "c8y".into(), + config_file, + connection: "edge_to_c8y".into(), + address, + remote_username: None, + bridge_root_cert_path, + remote_clientid, + local_clientid: "Cumulocity".into(), + bridge_certfile, + bridge_keyfile, + use_mapper: true, + use_agent: true, + try_private: false, + start_type: "automatic".into(), + clean_session: false, + notifications: false, + bridge_attempt_unsubscribe: false, + topics: vec![ + // Registration + r#"s/dcr in 2 c8y/ """#.into(), + r#"s/ucr out 2 c8y/ """#.into(), + // Templates + r#"s/dt in 2 c8y/ """#.into(), + r#"s/ut/# out 2 c8y/ """#.into(), + // Static templates + r#"s/us/# out 2 c8y/ """#.into(), + r#"t/us/# out 2 c8y/ """#.into(), + r#"q/us/# out 2 c8y/ """#.into(), + r#"c/us/# out 2 c8y/ """#.into(), + r#"s/ds in 2 c8y/ """#.into(), + // Debug + r#"s/e in 0 c8y/ """#.into(), + // SmartRest2 + r#"s/uc/# out 2 c8y/ """#.into(), + r#"t/uc/# out 2 c8y/ """#.into(), + r#"q/uc/# out 2 c8y/ """#.into(), + r#"c/uc/# out 2 c8y/ """#.into(), + r#"s/dc/# in 2 c8y/ """#.into(), + // c8y JSON + r#"measurement/measurements/create out 2 c8y/ """#.into(), + r#"error in 2 c8y/ """#.into(), + // c8y JWT token retrieval + r#"s/uat/# out 2 c8y/ """#.into(), + r#"s/dat/# in 2 c8y/ """#.into(), + ], + } + } +} + +#[test] +fn test_bridge_config_from_c8y_params() -> anyhow::Result<()> { + use std::convert::TryFrom; + let params = BridgeConfigC8yParams { + connect_url: ConnectUrl::try_from("test.test.io")?, + mqtt_tls_port: 8883, + config_file: "c8y-bridge.conf".into(), + remote_clientid: "alpha".into(), + bridge_root_cert_path: "./test_root.pem".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + }; + + let bridge = BridgeConfig::from(params); + + let expected = BridgeConfig { + cloud_name: "c8y".into(), + config_file: "c8y-bridge.conf".into(), + connection: "edge_to_c8y".into(), + address: "test.test.io:8883".into(), + remote_username: None, + bridge_root_cert_path: "./test_root.pem".into(), + remote_clientid: "alpha".into(), + local_clientid: "Cumulocity".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: true, + use_agent: true, + topics: vec![ + // Registration + r#"s/dcr in 2 c8y/ """#.into(), + r#"s/ucr out 2 c8y/ """#.into(), + // Templates + r#"s/dt in 2 c8y/ """#.into(), + r#"s/ut/# out 2 c8y/ """#.into(), + // Static templates + r#"s/us/# out 2 c8y/ """#.into(), + r#"t/us/# out 2 c8y/ """#.into(), + r#"q/us/# out 2 c8y/ """#.into(), + r#"c/us/# out 2 c8y/ """#.into(), + r#"s/ds in 2 c8y/ """#.into(), + // Debug + r#"s/e in 0 c8y/ """#.into(), + // SmartRest2 + r#"s/uc/# out 2 c8y/ """#.into(), + r#"t/uc/# out 2 c8y/ """#.into(), + r#"q/uc/# out 2 c8y/ """#.into(), + r#"c/uc/# out 2 c8y/ """#.into(), + r#"s/dc/# in 2 c8y/ """#.into(), + // c8y JSON + r#"measurement/measurements/create out 2 c8y/ """#.into(), + r#"error in 2 c8y/ """#.into(), + // c8y JWT token retrieval + r#"s/uat/# out 2 c8y/ """#.into(), + r#"s/dat/# in 2 c8y/ """#.into(), + ], + try_private: false, + start_type: "automatic".into(), + clean_session: false, + notifications: false, + bridge_attempt_unsubscribe: false, + }; + + assert_eq!(bridge, expected); + + Ok(()) +} diff --git a/crates/core/tedge/src/cli/connect/cli.rs b/crates/core/tedge/src/cli/connect/cli.rs new file mode 100644 index 00000000..42e68885 --- /dev/null +++ b/crates/core/tedge/src/cli/connect/cli.rs @@ -0,0 +1,48 @@ +use crate::cli::connect::*; +use crate::command::{BuildCommand, BuildContext, Command}; +use structopt::StructOpt; + +#[derive(StructOpt, Debug, 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 + #[structopt(long = "test")] + is_test_connection: bool, + }, + + /// Create connection to Azure + /// + /// The command will create config and start edge relay from the device to az instance + Az { + /// Test connection to Azure + #[structopt(long = "test")] + is_test_connection: bool, + }, +} + +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_repository: context.config_repository, + cloud: Cloud::C8y, + common_mosquitto_config: CommonMosquittoConfig::default(), + is_test_connection, + service_manager: context.service_manager.clone(), + }, + TEdgeConnectOpt::Az { is_test_connection } => ConnectCommand { + config_location: context.config_location, + config_repository: context.config_repository, + cloud: Cloud::Azure, + common_mosquitto_config: CommonMosquittoConfig::default(), + is_test_connection, + service_manager: context.service_manager.clone(), + }, + } + .into_boxed()) + } +} diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs new file mode 100644 index 00000000..b2813963 --- /dev/null +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -0,0 +1,569 @@ +use crate::{cli::connect::*, command::Command, system_services::*, ConfigError}; +use rumqttc::QoS::AtLeastOnce; +use rumqttc::{Event, Incoming, MqttOptions, Outgoing, Packet}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; +use tedge_config::*; +use tedge_utils::paths::{create_directories, ok_if_not_found, DraftFile}; +use which::which; + +const DEFAULT_HOST: &str = "localhost"; +const WAIT_FOR_CHECK_SECONDS: u64 = 10; +const C8Y_CONFIG_FILENAME: &str = "c8y-bridge.conf"; +const AZURE_CONFIG_FILENAME: &str = "az-bridge.conf"; +const RESPONSE_TIMEOUT: Duration = Duration::from_secs(10); +const MOSQUITTO_RESTART_TIMEOUT_SECONDS: u64 = 5; +const MQTT_TLS_PORT: u16 = 8883; +const TEDGE_BRIDGE_CONF_DIR_PATH: &str = "mosquitto-conf"; + +pub struct ConnectCommand { + pub config_location: TEdgeConfigLocation, + pub config_repository: TEdgeConfigRepository, + pub cloud: Cloud, + pub common_mosquitto_config: CommonMosquittoConfig, + pub is_test_connection: bool, + pub service_manager: Arc<dyn SystemServiceManager>, +} + +pub enum DeviceStatus { + MightBeNew, + AlreadyExists, + Unknown, +} + +#[derive(Debug)] +pub enum Cloud { + Azure, + C8y, +} + +impl Cloud { + fn dependent_mapper_service(&self) -> SystemService { + match self { + Cloud::Azure => SystemService::TEdgeMapperAz, + Cloud::C8y => SystemService::TEdgeMapperC8y, + } + } +} + +impl Cloud { + fn as_str(&self) -> &'static str { + match self { + Self::Azure => "Azure", + Self::C8y => "Cumulocity", + } + } +} + +impl Command for ConnectCommand { + fn description(&self) -> String { + if self.is_test_connection { + format!("test connection to {} cloud.", self.cloud.as_str()) + } else { + format!("connect {} cloud.", self.cloud.as_str()) + } + } + + fn execute(&self) -> anyhow::Result<()> { + let mut config = self.config_repository.load()?; + + if self.is_test_connection { + let br_config = self.bridge_config(&config)?; + if self.check_if_bridge_exists(br_config) { + return match self.check_connection(&config) { + Ok(_) => Ok(()), + Err(err) => Err(err.into()), + }; + } else { + return Err((ConnectError::DeviceNotConnected { + cloud: self.cloud.as_str().into(), + }) + .into()); + } + } + + // XXX: Do we really need to persist the defaults? + match self.cloud { + Cloud::Azure => assign_default(&mut config, AzureRootCertPathSetting)?, + Cloud::C8y => assign_default(&mut config, C8yRootCertPathSetting)?, + } + let bridge_config = self.bridge_config(&config)?; + let updated_mosquitto_config = self + .common_mosquitto_config + .clone() + .with_internal_opts(config.query(MqttPortSetting)?.into()) + .with_external_opts( + config.query(MqttExternalPortSetting).ok().map(|x| x.into()), + config.query(MqttExternalBindAddressSetting).ok(), + config.query(MqttExternalBindInterfaceSetting).ok(), + config + .query(MqttExternalCAPathSetting) + .ok() + .map(|x| x.to_string()), + config + .query(MqttExternalCertfileSetting) + .ok() + .map(|x| x.to_string()), + config + .query(MqttExternalKeyfileSetting) + .ok() + .map(|x| x.to_string()), + ); + self.config_repository.store(&config)?; + + new_bridge( + &bridge_config, + &self.cloud, + &updated_mosquitto_config, + self.service_manager.as_ref(), + &self.config_location, + )?; + + match self.check_connection(&config) { + Ok(DeviceStatus::AlreadyExists) => {} + Ok(DeviceStatus::MightBeNew) | Ok(DeviceStatus::Unknown) => { + if let Cloud::C8y = self.cloud { + println!("Restarting mosquitto to resubscribe to bridged inbound cloud topics after device creation"); + restart_mosquitto( + &bridge_config, + self.service_manager.as_ref(), + &self.config_location, + )?; + } + } + Err(_) => { + println!( + "Warning: Bridge has been configured, but {} connection check failed.\n", + self.cloud.as_str() + ); + } + } + + if let Cloud::C8y = self.cloud { + enable_software_management(&bridge_config, self.service_manager.as_ref()); + } + + Ok(()) + } +} + +impl ConnectCommand { + fn bridge_config(&self, config: &TEdgeConfig) -> Result<BridgeConfig, ConfigError> { + match self.cloud { + Cloud::Azure => { + let params = BridgeConfigAzureParams { |