summaryrefslogtreecommitdiffstats
path: root/crates/core/tedge/src/cli/connect
diff options
context:
space:
mode:
authorLukasz Woznicki <75632179+makr11st@users.noreply.github.com>2021-11-24 20:54:56 +0000
committerGitHub <noreply@github.com>2021-11-24 20:54:56 +0000
commita4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch)
tree9583f187114913a92866571920dd3bb205bd50a3 /crates/core/tedge/src/cli/connect
parent8217e80670e76dbf9168780f5e0545355a39f8f3 (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.rs369
-rw-r--r--crates/core/tedge/src/cli/connect/bridge_config_azure.rs111
-rw-r--r--crates/core/tedge/src/cli/connect/bridge_config_c8y.rs144
-rw-r--r--crates/core/tedge/src/cli/connect/cli.rs48
-rw-r--r--crates/core/tedge/src/cli/connect/command.rs569
-rw-r--r--crates/core/tedge/src/cli/connect/common_mosquitto_config.rs248
-rw-r--r--crates/core/tedge/src/cli/connect/error.rs41
-rw-r--r--crates/core/tedge/src/cli/connect/mod.rs12
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 {