From 59a057f6797eb4615ac85d8f756e23097d073d49 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 15 Feb 2024 12:34:26 +0000 Subject: [PATCH 01/20] Create PoC for bridge connection in tedge-mapper-c8y --- Cargo.lock | 20 ++ Cargo.toml | 1 + crates/core/tedge_mapper/Cargo.toml | 1 + crates/core/tedge_mapper/src/c8y/mapper.rs | 3 + .../extensions/tedge_mqtt_bridge/Cargo.toml | 35 +++ .../extensions/tedge_mqtt_bridge/src/lib.rs | 214 ++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 crates/extensions/tedge_mqtt_bridge/Cargo.toml create mode 100644 crates/extensions/tedge_mqtt_bridge/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 75217f52761..5f3c8733330 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3792,6 +3792,7 @@ dependencies = [ "tedge_file_system_ext", "tedge_health_ext", "tedge_http_ext", + "tedge_mqtt_bridge", "tedge_mqtt_ext", "tedge_signal_ext", "tedge_timer_ext", @@ -4056,6 +4057,25 @@ dependencies = [ "toml 0.7.8", ] +[[package]] +name = "tedge_mqtt_bridge" +version = "1.0.0" +dependencies = [ + "assert-json-diff", + "async-trait", + "certificate", + "futures", + "mqtt_channel", + "mqtt_tests", + "rumqttc", + "serde_json", + "tedge_actors", + "tedge_config", + "tedge_utils", + "tokio", + "tracing", +] + [[package]] name = "tedge_mqtt_ext" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4ced29ce063..bfa89767d5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,6 +152,7 @@ tedge_file_system_ext = { path = "crates/extensions/tedge_file_system_ext" } tedge_health_ext = { path = "crates/extensions/tedge_health_ext" } tedge_http_ext = { path = "crates/extensions/tedge_http_ext" } tedge_log_manager = { path = "crates/extensions/tedge_log_manager" } +tedge_mqtt_bridge = { path = "crates/extensions/tedge_mqtt_bridge" } tedge_mqtt_ext = { path = "crates/extensions/tedge_mqtt_ext" } tedge_script_ext = { path = "crates/extensions/tedge_script_ext" } tedge_signal_ext = { path = "crates/extensions/tedge_signal_ext" } diff --git a/crates/core/tedge_mapper/Cargo.toml b/crates/core/tedge_mapper/Cargo.toml index c039f563364..391281ec778 100644 --- a/crates/core/tedge_mapper/Cargo.toml +++ b/crates/core/tedge_mapper/Cargo.toml @@ -32,6 +32,7 @@ tedge_downloader_ext = { workspace = true } tedge_file_system_ext = { workspace = true } tedge_health_ext = { workspace = true } tedge_http_ext = { workspace = true } +tedge_mqtt_bridge = { workspace = true } tedge_mqtt_ext = { workspace = true } tedge_signal_ext = { workspace = true } tedge_timer_ext = { workspace = true } diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index c7a42ba510a..04d9c9e59c2 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -17,6 +17,7 @@ use tedge_config::TEdgeConfig; use tedge_downloader_ext::DownloaderActor; use tedge_file_system_ext::FsWatchActorBuilder; use tedge_http_ext::HttpActor; +use tedge_mqtt_bridge::MqttBridgeActorBuilder; use tedge_mqtt_ext::MqttActorBuilder; use tedge_timer_ext::TimerActor; use tedge_uploader_ext::UploaderActor; @@ -36,6 +37,7 @@ impl TEdgeComponent for CumulocityMapper { start_basic_actors(self.session_name(), &tedge_config).await?; let mqtt_config = tedge_config.mqtt_config()?; + let bridge_actor = MqttBridgeActorBuilder::new(&tedge_config).await; let mut jwt_actor = C8YJwtRetriever::builder(mqtt_config.clone()); let mut http_actor = HttpActor::new().builder(); let c8y_http_config = (&tedge_config).try_into()?; @@ -84,6 +86,7 @@ impl TEdgeComponent for CumulocityMapper { runtime.spawn(uploader_actor).await?; runtime.spawn(downloader_actor).await?; runtime.spawn(old_to_new_agent_adapter).await?; + runtime.spawn(bridge_actor).await?; runtime.run_to_completion().await?; Ok(()) diff --git a/crates/extensions/tedge_mqtt_bridge/Cargo.toml b/crates/extensions/tedge_mqtt_bridge/Cargo.toml new file mode 100644 index 00000000000..d88b68b85be --- /dev/null +++ b/crates/extensions/tedge_mqtt_bridge/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "tedge_mqtt_bridge" +description = "thin-edge extension adding a bridge MQTT connection to the desired cloud provider" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } + +[features] +# No features on by default +default = [] +test-helpers = ["dep:assert-json-diff"] + +[dependencies] +assert-json-diff = { workspace = true, optional = true } +async-trait = { workspace = true } +certificate = { workspace = true } +futures = { workspace = true } +mqtt_channel = { workspace = true } +rumqttc = { workspace = true } +serde_json = { workspace = true } +tedge_actors = { workspace = true } +tedge_config = { workspace = true } +tedge_utils = { workspace = true } +tokio = { workspace = true, default_features = false, features = ["macros"] } +tracing = { workspace = true } + +[dev-dependencies] +mqtt_tests = { path = "../../tests/mqtt_tests" } + +[lints] +workspace = true diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs new file mode 100644 index 00000000000..05969ae7da4 --- /dev/null +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -0,0 +1,214 @@ +use async_trait::async_trait; +use certificate::parse_root_certificate::create_tls_config; +use futures::SinkExt; +use futures::StreamExt; +use rumqttc::AsyncClient; +use rumqttc::Event; +use rumqttc::EventLoop; +use rumqttc::Incoming; +use rumqttc::MqttOptions; +use rumqttc::Outgoing; +use rumqttc::PubAck; +use rumqttc::PubRec; +use rumqttc::Publish; +use rumqttc::Transport; +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::Infallible; +use tedge_actors::futures::channel::mpsc; +use tedge_actors::Actor; +use tedge_actors::Builder; +use tedge_actors::DynSender; +use tedge_actors::RuntimeError; +use tedge_actors::RuntimeRequest; +use tedge_actors::RuntimeRequestSink; +use tracing::error; + +pub type MqttConfig = mqtt_channel::Config; +pub type MqttMessage = Message; + +pub use mqtt_channel::DebugPayload; +pub use mqtt_channel::Message; +pub use mqtt_channel::MqttError; +pub use mqtt_channel::QoS; +pub use mqtt_channel::Topic; +pub use mqtt_channel::TopicFilter; +use tedge_config::TEdgeConfig; + +pub struct MqttBridgeActorBuilder { + signal_sender: mpsc::Sender, +} + +impl MqttBridgeActorBuilder { + pub async fn new(tedge_config: &TEdgeConfig) -> Self { + let tls_config = create_tls_config( + tedge_config.c8y.root_cert_path.clone().into(), + tedge_config.device.key_path.clone().into(), + tedge_config.device.cert_path.clone().into(), + ) + .unwrap(); + let (signal_sender, _signal_receiver) = mpsc::channel(10); + + let prefix = "c8y"; + let mut local_config = MqttOptions::new( + format!("tedge-mapper-bridge-{prefix}"), + &tedge_config.mqtt.client.host, + tedge_config.mqtt.client.port.into(), + ); + // TODO cope with secured mosquitto + local_config.set_manual_acks(true); + let mut cloud_config = MqttOptions::new( + tedge_config.device.id.try_read(tedge_config).unwrap(), + tedge_config + .c8y + .mqtt + .or_config_not_set() + .unwrap() + .host() + .to_string(), + 8883, + ); + cloud_config.set_manual_acks(true); + + cloud_config.set_transport(Transport::tls_with_config(tls_config.into())); + let topic_prefix = format!("{prefix}/"); + let (local_client, local_event_loop) = AsyncClient::new(local_config, 10); + let (cloud_client, cloud_event_loop) = AsyncClient::new(cloud_config, 10); + + // TODO support non c8y clouds + local_client + .subscribe(format!("{topic_prefix}#"), QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("s/dt", QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("s/dat", QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("s/ds", QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("s/e", QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("s/dc/#", QoS::AtLeastOnce) + .await + .unwrap(); + cloud_client + .subscribe("error", QoS::AtLeastOnce) + .await + .unwrap(); + + let (tx_pubs_from_cloud, rx_pubs_from_cloud) = mpsc::channel(10); + let (tx_pubs_from_local, rx_pubs_from_local) = mpsc::channel(10); + tokio::spawn(one_way_bridge( + local_event_loop, + cloud_client, + move |topic| topic.strip_prefix(&topic_prefix).unwrap().into(), + tx_pubs_from_local, + rx_pubs_from_cloud, + )); + tokio::spawn(one_way_bridge( + cloud_event_loop, + local_client, + move |topic| format!("{prefix}/{topic}").into(), + tx_pubs_from_cloud, + rx_pubs_from_local, + )); + + Self { signal_sender } + } + + pub(crate) fn build_actor(self) -> MqttBridgeActor { + MqttBridgeActor {} + } +} + +async fn one_way_bridge Fn(&'a str) -> Cow<'a, str>>( + mut recv_event_loop: EventLoop, + target: AsyncClient, + transform_topic: F, + mut tx_pubs: mpsc::Sender, + mut rx_pubs: mpsc::Receiver, +) { + let mut forward_pkid_to_received_msg = HashMap::new(); + loop { + let notification = match recv_event_loop.poll().await { + Ok(notification) => notification, + Err(err) => { + error!("MQTT bridge connection error: {err}"); + continue; + } + }; + match notification { + // Forwarding messages from event loop to target + Event::Incoming(Incoming::Publish(publish)) => { + target + .publish( + transform_topic(&publish.topic), + publish.qos, + publish.retain, + publish.payload.clone(), + ) + .await + .unwrap(); + tx_pubs.send(publish).await.unwrap(); + } + // Forwarding acks from event loop to target + Event::Incoming( + Incoming::PubAck(PubAck { pkid: ack_pkid }) + | Incoming::PubRec(PubRec { pkid: ack_pkid }), + ) => { + target + .ack(&forward_pkid_to_received_msg.remove(&ack_pkid).unwrap()) + .await + .unwrap(); + } + Event::Outgoing(Outgoing::Publish(pkid)) => { + if let Some(msg) = rx_pubs.next().await { + forward_pkid_to_received_msg.insert(pkid, msg); + } else { + break; + } + } + _ => {} + } + } +} + +impl Builder for MqttBridgeActorBuilder { + type Error = Infallible; + + fn try_build(self) -> Result { + Ok(self.build()) + } + + fn build(self) -> MqttBridgeActor { + self.build_actor() + } +} + +impl RuntimeRequestSink for MqttBridgeActorBuilder { + fn get_signal_sender(&self) -> DynSender { + Box::new(self.signal_sender.clone()) + } +} + +pub struct MqttBridgeActor {} + +#[async_trait] +impl Actor for MqttBridgeActor { + fn name(&self) -> &str { + "MQTT-Bridge" + } + + async fn run(mut self) -> Result<(), RuntimeError> { + Ok(()) + } +} From 169f41ed38e9788d9b5c494d9b7bb51a2dc98c91 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Mon, 26 Feb 2024 14:36:37 +0000 Subject: [PATCH 02/20] Add support for the mapper to control topic subscriptions in the bridge --- .../src/tedge_config_cli/tedge_config.rs | 1 + crates/core/tedge_mapper/src/c8y/mapper.rs | 22 +++++++++++- .../extensions/tedge_mqtt_bridge/src/lib.rs | 34 +++++-------------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index 642cee5cb53..35c6f5ac5e6 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -42,6 +42,7 @@ impl OptionalConfigError for OptionalConfig { } } +#[derive(Clone)] pub struct TEdgeConfig(TEdgeConfigReader); impl std::ops::Deref for TEdgeConfig { diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 04d9c9e59c2..4ac209ed512 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -37,7 +37,27 @@ impl TEdgeComponent for CumulocityMapper { start_basic_actors(self.session_name(), &tedge_config).await?; let mqtt_config = tedge_config.mqtt_config()?; - let bridge_actor = MqttBridgeActorBuilder::new(&tedge_config).await; + let custom_topics = tedge_config + .c8y + .smartrest + .templates + .0 + .iter() + .map(|id| format!("s/dc/{id}")); + let smartrest_topics: Vec = [ + "s/dt", + "s/dat", + "s/ds", + "s/e", + "s/dc/#", + "devicecontrol/notifications", + "error", + ] + .into_iter() + .map(<_>::to_owned) + .chain(custom_topics) + .collect(); + let bridge_actor = MqttBridgeActorBuilder::new(&tedge_config, &smartrest_topics).await; let mut jwt_actor = C8YJwtRetriever::builder(mqtt_config.clone()); let mut http_actor = HttpActor::new().builder(); let c8y_http_config = (&tedge_config).try_into()?; diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 05969ae7da4..7379eb5f3ab 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -40,13 +40,13 @@ pub struct MqttBridgeActorBuilder { } impl MqttBridgeActorBuilder { - pub async fn new(tedge_config: &TEdgeConfig) -> Self { + pub async fn new(tedge_config: &TEdgeConfig, cloud_topics: &[impl AsRef]) -> Self { let tls_config = create_tls_config( tedge_config.c8y.root_cert_path.clone().into(), tedge_config.device.key_path.clone().into(), tedge_config.device.cert_path.clone().into(), ) - .unwrap(); + .unwrap(); let (signal_sender, _signal_receiver) = mpsc::channel(10); let prefix = "c8y"; @@ -80,30 +80,12 @@ impl MqttBridgeActorBuilder { .subscribe(format!("{topic_prefix}#"), QoS::AtLeastOnce) .await .unwrap(); - cloud_client - .subscribe("s/dt", QoS::AtLeastOnce) - .await - .unwrap(); - cloud_client - .subscribe("s/dat", QoS::AtLeastOnce) - .await - .unwrap(); - cloud_client - .subscribe("s/ds", QoS::AtLeastOnce) - .await - .unwrap(); - cloud_client - .subscribe("s/e", QoS::AtLeastOnce) - .await - .unwrap(); - cloud_client - .subscribe("s/dc/#", QoS::AtLeastOnce) - .await - .unwrap(); - cloud_client - .subscribe("error", QoS::AtLeastOnce) - .await - .unwrap(); + for topic in cloud_topics { + cloud_client + .subscribe(topic.as_ref(), QoS::AtLeastOnce) + .await + .unwrap(); + } let (tx_pubs_from_cloud, rx_pubs_from_cloud) = mpsc::channel(10); let (tx_pubs_from_local, rx_pubs_from_local) = mpsc::channel(10); From 8946fd34ea6db3125d357779ff9d55c953376b26 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Mon, 26 Feb 2024 16:32:13 +0000 Subject: [PATCH 03/20] Control bridge prefix --- crates/extensions/tedge_mqtt_bridge/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 7379eb5f3ab..50df7e39157 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -49,7 +49,8 @@ impl MqttBridgeActorBuilder { .unwrap(); let (signal_sender, _signal_receiver) = mpsc::channel(10); - let prefix = "c8y"; + // TODO move this somewhere sensible, and make sure we validate it + let prefix = std::env::var("TEDGE_BRIDGE_PREFIX").unwrap_or_else(|_| "c8y".to_owned()); let mut local_config = MqttOptions::new( format!("tedge-mapper-bridge-{prefix}"), &tedge_config.mqtt.client.host, From 66c10bc29b8328d559d4bbac274f4c24e1c6e3df Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 27 Feb 2024 07:47:49 +0000 Subject: [PATCH 04/20] Skip re-logging already observed error messages --- crates/extensions/tedge_mqtt_bridge/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 50df7e39157..e6722529fa9 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -76,7 +76,6 @@ impl MqttBridgeActorBuilder { let (local_client, local_event_loop) = AsyncClient::new(local_config, 10); let (cloud_client, cloud_event_loop) = AsyncClient::new(cloud_config, 10); - // TODO support non c8y clouds local_client .subscribe(format!("{topic_prefix}#"), QoS::AtLeastOnce) .await @@ -121,11 +120,16 @@ async fn one_way_bridge Fn(&'a str) -> Cow<'a, str>>( mut rx_pubs: mpsc::Receiver, ) { let mut forward_pkid_to_received_msg = HashMap::new(); + let mut last_err = None; loop { let notification = match recv_event_loop.poll().await { Ok(notification) => notification, Err(err) => { - error!("MQTT bridge connection error: {err}"); + let err = err.to_string(); + if last_err.as_ref() != Some(&err) { + error!("MQTT bridge connection error: {err}"); + last_err = Some(err); + } continue; } }; From 466e179b8919138619d8d826d368d9392fcdea2a Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 27 Feb 2024 14:05:33 +0000 Subject: [PATCH 05/20] Make topic prefix configurable in mapper --- .../src/tedge_config_cli/tedge_config.rs | 74 ++++++++++- crates/core/c8y_api/src/http_proxy.rs | 20 ++- .../core/c8y_api/src/json_c8y_deserializer.rs | 18 ++- .../core/c8y_api/src/smartrest/inventory.rs | 10 +- .../src/smartrest/smartrest_serializer.rs | 17 +-- crates/core/c8y_api/src/smartrest/topic.rs | 94 ++++++++------ crates/core/c8y_api/src/utils.rs | 9 +- crates/core/tedge/src/cli/connect/command.rs | 9 +- .../core/tedge/src/cli/connect/jwt_token.rs | 9 +- crates/core/tedge_api/src/alarm.rs | 2 +- crates/core/tedge_mapper/src/c8y/mapper.rs | 14 ++- .../c8y_config_manager/src/actor.rs | 28 +++-- .../c8y_config_manager/src/child_device.rs | 7 +- .../c8y_config_manager/src/config.rs | 9 +- .../c8y_config_manager/src/download.rs | 15 ++- .../extensions/c8y_config_manager/src/lib.rs | 2 +- .../c8y_config_manager/src/plugin_config.rs | 11 +- .../c8y_config_manager/src/tests.rs | 59 ++++----- .../c8y_config_manager/src/upload.rs | 26 ++-- .../c8y_firmware_manager/src/actor.rs | 17 ++- .../c8y_firmware_manager/src/config.rs | 9 +- .../c8y_firmware_manager/src/lib.rs | 18 ++- .../c8y_firmware_manager/src/tests.rs | 5 +- .../c8y_http_proxy/src/credentials.rs | 4 +- .../extensions/c8y_log_manager/src/actor.rs | 17 ++- .../extensions/c8y_log_manager/src/config.rs | 4 + crates/extensions/c8y_log_manager/src/lib.rs | 8 +- .../c8y_mapper_ext/src/alarm_converter.rs | 13 +- .../extensions/c8y_mapper_ext/src/config.rs | 13 +- .../c8y_mapper_ext/src/converter.rs | 116 +++++++++++------- .../c8y_mapper_ext/src/inventory.rs | 6 +- .../src/operations/config_snapshot.rs | 12 +- .../src/operations/config_update.rs | 4 +- .../src/operations/firmware_update.rs | 7 +- .../src/operations/log_upload.rs | 4 +- .../c8y_mapper_ext/src/service_monitor.rs | 97 ++++++++------- crates/extensions/c8y_mapper_ext/src/tests.rs | 29 +++-- .../extensions/tedge_mqtt_bridge/src/lib.rs | 7 +- plugins/c8y_configuration_plugin/src/lib.rs | 3 +- plugins/c8y_firmware_plugin/src/lib.rs | 5 +- plugins/c8y_log_plugin/src/main.rs | 5 +- 41 files changed, 541 insertions(+), 295 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index 35c6f5ac5e6..da843929961 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -14,13 +14,20 @@ use camino::Utf8PathBuf; use certificate::CertificateError; use certificate::PemCertificate; use doku::Document; +use doku::Type; use once_cell::sync::Lazy; +use serde::Deserialize; use std::borrow::Cow; +use std::convert::Infallible; +use std::fmt; +use std::fmt::Formatter; use std::io::Read; use std::net::IpAddr; use std::net::Ipv4Addr; use std::num::NonZeroU16; +use std::ops::Deref; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use tedge_config_macros::all_or_nothing; use tedge_config_macros::define_tedge_config; @@ -444,7 +451,11 @@ define_tedge_config! { #[tedge_config(note = "If set to 'auto', this cleans the local session accordingly the detected version of mosquitto.")] #[tedge_config(example = "auto", default(variable = "AutoFlag::Auto"))] local_cleansession: AutoFlag, - } + }, + + // TODO validation + #[tedge_config(example = "c8y", default(value = "c8y"))] + topic_prefix: TopicPrefix, }, entity_store: { @@ -794,6 +805,64 @@ define_tedge_config! { } +// TODO doc comment +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, serde::Serialize)] +#[serde(from = "String", into = "Arc")] +pub struct TopicPrefix(Arc); + +impl Document for TopicPrefix { + fn ty() -> Type { + String::ty() + } +} + +// TODO actual validation +// TODO make sure we don't allow c8y-internal either, or az, or aws as those are all used +impl From for TopicPrefix { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl From<&str> for TopicPrefix { + fn from(value: &str) -> Self { + Self(value.into()) + } +} + +impl FromStr for TopicPrefix { + type Err = Infallible; + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + +impl From for Arc { + fn from(value: TopicPrefix) -> Self { + value.0 + } +} + +// TODO is deref actually right here +impl Deref for TopicPrefix { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TopicPrefix { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for TopicPrefix { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + fn default_http_bind_address(dto: &TEdgeConfigDto) -> IpAddr { let external_address = dto.mqtt.external.bind.address; external_address @@ -813,7 +882,8 @@ fn device_id(reader: &TEdgeConfigReader) -> Result { fn cert_error_into_config_error(key: &'static str, err: CertificateError) -> ReadError { match &err { CertificateError::IoError(io_err) => match io_err.kind() { - std::io::ErrorKind::NotFound => ReadError::ReadOnlyNotFound { key, + std::io::ErrorKind::NotFound => ReadError::ReadOnlyNotFound { + key, message: concat!( "The device id is read from the device certificate.\n", "To set 'device.id' to some , you can use `tedge cert create --device-id `.", diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index fe7aeed9153..5f0c86dc9c0 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::time::Duration; use tedge_config::mqtt_config::MqttConfigBuildError; use tedge_config::TEdgeConfig; +use tedge_config::TopicPrefix; use tracing::error; use tracing::info; @@ -112,33 +113,41 @@ impl C8yEndPoint { pub struct C8yMqttJwtTokenRetriever { mqtt_config: mqtt_channel::Config, + topic_prefix: TopicPrefix, } impl C8yMqttJwtTokenRetriever { pub fn from_tedge_config(tedge_config: &TEdgeConfig) -> Result { let mqtt_config = tedge_config.mqtt_config()?; - Ok(Self::new(mqtt_config)) + Ok(Self::new( + mqtt_config, + tedge_config.c8y.bridge.topic_prefix.clone(), + )) } - pub fn new(mqtt_config: mqtt_channel::Config) -> Self { - let topic = TopicFilter::new_unchecked("c8y/s/dat"); + pub fn new(mqtt_config: mqtt_channel::Config, topic_prefix: TopicPrefix) -> Self { + let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); let mqtt_config = mqtt_config .with_no_session() // Ignore any already published tokens, possibly stale. .with_subscriptions(topic); - C8yMqttJwtTokenRetriever { mqtt_config } + C8yMqttJwtTokenRetriever { + mqtt_config, + topic_prefix, + } } pub async fn get_jwt_token(&mut self) -> Result { let mut mqtt_con = Connection::new(&self.mqtt_config).await?; + let pub_topic = format!("{}/s/uat", self.topic_prefix); tokio::time::sleep(Duration::from_millis(20)).await; for _ in 0..3 { mqtt_con .published .publish( - mqtt_channel::Message::new(&Topic::new_unchecked("c8y/s/uat"), "".to_string()) + mqtt_channel::Message::new(&Topic::new_unchecked(&pub_topic), "".to_string()) .with_qos(mqtt_channel::QoS::AtMostOnce), ) .await?; @@ -184,7 +193,6 @@ pub enum JwtError { #[cfg(test)] mod tests { - use super::*; use test_case::test_case; diff --git a/crates/core/c8y_api/src/json_c8y_deserializer.rs b/crates/core/c8y_api/src/json_c8y_deserializer.rs index 7ebf31c4d1a..9f370e55b0d 100644 --- a/crates/core/c8y_api/src/json_c8y_deserializer.rs +++ b/crates/core/c8y_api/src/json_c8y_deserializer.rs @@ -7,21 +7,22 @@ use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::SoftwareModule; use tedge_api::SoftwareModuleUpdate; use tedge_api::SoftwareUpdateCommand; +use tedge_config::TopicPrefix; use time::OffsetDateTime; pub struct C8yDeviceControlTopic; impl C8yDeviceControlTopic { - pub fn topic() -> Topic { - Topic::new_unchecked(Self::name()) + pub fn topic(prefix: &TopicPrefix) -> Topic { + Topic::new_unchecked(&Self::name(prefix)) } - pub fn accept(topic: &Topic) -> bool { - topic.name.starts_with(Self::name()) + pub fn accept(topic: &Topic, prefix: &TopicPrefix) -> bool { + topic.name.starts_with(&Self::name(prefix)) } - pub fn name() -> &'static str { - "c8y/devicecontrol/notifications" + pub fn name(prefix: &TopicPrefix) -> String { + format!("{prefix}/devicecontrol/notifications") } } @@ -422,10 +423,15 @@ pub trait C8yDeviceControlOperationHelper { } impl C8yDeviceControlOperationHelper for C8yRestart {} + impl C8yDeviceControlOperationHelper for C8ySoftwareUpdate {} + impl C8yDeviceControlOperationHelper for C8yLogfileRequest {} + impl C8yDeviceControlOperationHelper for C8yUploadConfigFile {} + impl C8yDeviceControlOperationHelper for C8yDownloadConfigFile {} + impl C8yDeviceControlOperationHelper for C8yFirmware {} #[derive(thiserror::Error, Debug)] diff --git a/crates/core/c8y_api/src/smartrest/inventory.rs b/crates/core/c8y_api/src/smartrest/inventory.rs index e1d8257e233..04b792c5718 100644 --- a/crates/core/c8y_api/src/smartrest/inventory.rs +++ b/crates/core/c8y_api/src/smartrest/inventory.rs @@ -12,6 +12,7 @@ use crate::smartrest::csv::fields_to_csv_string; use crate::smartrest::topic::publish_topic_from_ancestors; use mqtt_channel::Message; +use tedge_config::TopicPrefix; use super::message::sanitize_for_smartrest; @@ -23,6 +24,7 @@ pub fn child_device_creation_message( device_name: Option<&str>, device_type: Option<&str>, ancestors: &[String], + prefix: &TopicPrefix, ) -> Result { if child_id.is_empty() { return Err(InvalidValueError { @@ -44,7 +46,7 @@ pub fn child_device_creation_message( } Ok(Message::new( - &publish_topic_from_ancestors(ancestors), + &publish_topic_from_ancestors(ancestors, prefix), // XXX: if any arguments contain commas, output will be wrong format!( "101,{},{},{}", @@ -64,6 +66,7 @@ pub fn service_creation_message( service_type: &str, service_status: &str, ancestors: &[String], + prefix: &TopicPrefix, ) -> Result { // TODO: most of this noise can be eliminated by implementing `Serialize`/`Deserialize` for smartrest format if service_id.is_empty() { @@ -92,7 +95,7 @@ pub fn service_creation_message( } Ok(Message::new( - &publish_topic_from_ancestors(ancestors), + &publish_topic_from_ancestors(ancestors, prefix), fields_to_csv_string(&[ "102", service_id, @@ -116,8 +119,9 @@ pub fn service_creation_message( pub fn service_status_update_message( external_ids: &[impl AsRef], service_status: &str, + prefix: &TopicPrefix, ) -> Message { - let topic = publish_topic_from_ancestors(external_ids); + let topic = publish_topic_from_ancestors(external_ids, prefix); let service_status = sanitize_for_smartrest(service_status, super::message::MAX_PAYLOAD_LIMIT_IN_BYTES); diff --git a/crates/core/c8y_api/src/smartrest/smartrest_serializer.rs b/crates/core/c8y_api/src/smartrest/smartrest_serializer.rs index f36bda2a382..8fa2cc4e017 100644 --- a/crates/core/c8y_api/src/smartrest/smartrest_serializer.rs +++ b/crates/core/c8y_api/src/smartrest/smartrest_serializer.rs @@ -7,6 +7,7 @@ use serde::ser::SerializeSeq; use serde::Deserialize; use serde::Serialize; use serde::Serializer; +use tedge_config::TopicPrefix; use tracing::warn; pub type SmartRest = String; @@ -204,20 +205,20 @@ where /// Helper to generate a SmartREST operation status message pub trait OperationStatusMessage { - fn executing() -> Message { - Self::create_message(Self::status_executing()) + fn executing(prefix: &TopicPrefix) -> Message { + Self::create_message(Self::status_executing(), prefix) } - fn successful(parameter: Option<&str>) -> Message { - Self::create_message(Self::status_successful(parameter)) + fn successful(parameter: Option<&str>, prefix: &TopicPrefix) -> Message { + Self::create_message(Self::status_successful(parameter), prefix) } - fn failed(failure_reason: &str) -> Message { - Self::create_message(Self::status_failed(failure_reason)) + fn failed(failure_reason: &str, prefix: &TopicPrefix) -> Message { + Self::create_message(Self::status_failed(failure_reason), prefix) } - fn create_message(payload: SmartRest) -> Message { - let topic = C8yTopic::SmartRestResponse.to_topic().unwrap(); // never fail + fn create_message(payload: SmartRest, prefix: &TopicPrefix) -> Message { + let topic = C8yTopic::SmartRestResponse.to_topic(prefix).unwrap(); // never fail Message::new(&topic, payload) } diff --git a/crates/core/c8y_api/src/smartrest/topic.rs b/crates/core/c8y_api/src/smartrest/topic.rs index 8b5c79cf32c..82af739a33a 100644 --- a/crates/core/c8y_api/src/smartrest/topic.rs +++ b/crates/core/c8y_api/src/smartrest/topic.rs @@ -4,64 +4,62 @@ use mqtt_channel::Topic; use mqtt_channel::TopicFilter; use tedge_api::entity_store::EntityMetadata; use tedge_api::entity_store::EntityType; +use tedge_config::TopicPrefix; -pub const SMARTREST_PUBLISH_TOPIC: &str = "c8y/s/us"; -pub const SMARTREST_SUBSCRIBE_TOPIC: &str = "c8y/s/ds"; +const SMARTREST_PUBLISH_TOPIC: &str = "s/us"; +const SMARTREST_SUBSCRIBE_TOPIC: &str = "s/ds"; #[derive(Debug, Clone, Eq, PartialEq)] pub enum C8yTopic { SmartRestRequest, SmartRestResponse, ChildSmartRestResponse(String), - OperationTopic(String), + // OperationTopic(String), } impl C8yTopic { /// Return the c8y SmartRest response topic for the given entity - pub fn smartrest_response_topic(entity: &EntityMetadata) -> Option { + pub fn smartrest_response_topic( + entity: &EntityMetadata, + prefix: &TopicPrefix, + ) -> Option { match entity.r#type { - EntityType::MainDevice => Some(C8yTopic::upstream_topic()), + EntityType::MainDevice => Some(C8yTopic::upstream_topic(prefix)), EntityType::ChildDevice | EntityType::Service => { Self::ChildSmartRestResponse(entity.external_id.clone().into()) - .to_topic() + .to_topic(prefix) .ok() } } } - pub fn to_topic(&self) -> Result { - Topic::new(self.to_string().as_str()) + pub fn to_topic(&self, prefix: &TopicPrefix) -> Result { + Topic::new(self.with_prefix(prefix).as_str()) } - pub fn upstream_topic() -> Topic { - Topic::new_unchecked(SMARTREST_PUBLISH_TOPIC) + pub fn upstream_topic(prefix: &TopicPrefix) -> Topic { + Topic::new_unchecked(&Self::SmartRestResponse.with_prefix(prefix)) } - pub fn downstream_topic() -> Topic { - Topic::new_unchecked(SMARTREST_SUBSCRIBE_TOPIC) + pub fn downstream_topic(prefix: &TopicPrefix) -> Topic { + Topic::new_unchecked(&Self::SmartRestRequest.with_prefix(prefix)) } - pub fn accept(topic: &Topic) -> bool { - topic.name.starts_with("c8y") - } -} - -impl ToString for C8yTopic { - fn to_string(&self) -> String { + pub fn with_prefix(&self, prefix: &TopicPrefix) -> String { match self { - Self::SmartRestRequest => SMARTREST_SUBSCRIBE_TOPIC.into(), - Self::SmartRestResponse => SMARTREST_PUBLISH_TOPIC.into(), + Self::SmartRestRequest => format!("{prefix}/{SMARTREST_SUBSCRIBE_TOPIC}"), + Self::SmartRestResponse => format!("{prefix}/{SMARTREST_PUBLISH_TOPIC}"), Self::ChildSmartRestResponse(child_id) => { - format!("{}/{}", SMARTREST_PUBLISH_TOPIC, child_id) - } - Self::OperationTopic(name) => name.into(), + format!("{prefix}/{SMARTREST_PUBLISH_TOPIC}/{child_id}") + } // Self::OperationTopic(name) => name.into(), } } -} -impl From for TopicFilter { - fn from(val: C8yTopic) -> Self { - val.to_string().as_str().try_into().expect("infallible") + pub fn to_topic_filter(&self, prefix: &TopicPrefix) -> TopicFilter { + self.with_prefix(prefix) + .as_str() + .try_into() + .expect("infallible") } } @@ -93,8 +91,8 @@ impl From<&C8yAlarm> for C8yTopic { /// - `["main"]` -> `c8y/s/us` /// - `["child1", "main"]` -> `c8y/s/us/child1` /// - `["child2", "child1", "main"]` -> `c8y/s/us/child1/child2` -pub fn publish_topic_from_ancestors(ancestors: &[impl AsRef]) -> Topic { - let mut target_topic = SMARTREST_PUBLISH_TOPIC.to_string(); +pub fn publish_topic_from_ancestors(ancestors: &[impl AsRef], prefix: &TopicPrefix) -> Topic { + let mut target_topic = format!("{prefix}/{SMARTREST_PUBLISH_TOPIC}"); for ancestor in ancestors.iter().rev().skip(1) { // Skipping the last ancestor as it is the main device represented by the root topic itself target_topic.push('/'); @@ -111,20 +109,38 @@ mod tests { #[test] fn convert_c8y_topic_to_str() { - assert_eq!(&C8yTopic::SmartRestRequest.to_string(), "c8y/s/ds"); - assert_eq!(&C8yTopic::SmartRestResponse.to_string(), "c8y/s/us"); assert_eq!( - &C8yTopic::ChildSmartRestResponse("child-id".into()).to_string(), - "c8y/s/us/child-id" + &C8yTopic::SmartRestRequest.with_prefix(&"c8y".into()), + "c8y/s/ds" + ); + assert_eq!( + &C8yTopic::SmartRestResponse.with_prefix(&"c8y".into()), + "c8y/s/us" ); + assert_eq!( + &C8yTopic::ChildSmartRestResponse("child-id".into()).with_prefix(&"custom".into()), + "custom/s/us/child-id" + ); + } + + #[test] + fn topic_methods() { + assert_eq!( + C8yTopic::upstream_topic(&"c8y-local".into()), + Topic::new_unchecked("c8y-local/s/us") + ); + assert_eq!( + C8yTopic::downstream_topic(&"custom-topic".into()), + Topic::new_unchecked("custom-topic/s/ds") + ) } - #[test_case(&["main"], "c8y/s/us")] - #[test_case(&["foo"], "c8y/s/us")] - #[test_case(&["child1", "main"], "c8y/s/us/child1")] - #[test_case(&["child3", "child2", "child1", "main"], "c8y/s/us/child1/child2/child3")] + #[test_case(& ["main"], "c8y2/s/us")] + #[test_case(& ["foo"], "c8y2/s/us")] + #[test_case(& ["child1", "main"], "c8y2/s/us/child1")] + #[test_case(& ["child3", "child2", "child1", "main"], "c8y2/s/us/child1/child2/child3")] fn topic_from_ancestors(ancestors: &[&str], topic: &str) { - let nested_child_topic = publish_topic_from_ancestors(ancestors); + let nested_child_topic = publish_topic_from_ancestors(ancestors, &"c8y2".into()); assert_eq!(nested_child_topic, Topic::new_unchecked(topic)); } } diff --git a/crates/core/c8y_api/src/utils.rs b/crates/core/c8y_api/src/utils.rs index bee53405c34..e42dd0f4b18 100644 --- a/crates/core/c8y_api/src/utils.rs +++ b/crates/core/c8y_api/src/utils.rs @@ -1,5 +1,4 @@ pub mod bridge { - use mqtt_channel::Message; // FIXME: doesn't account for custom topic root, use MQTT scheme API here @@ -18,13 +17,13 @@ pub mod bridge { } pub mod child_device { - use crate::smartrest::topic::SMARTREST_PUBLISH_TOPIC; + use crate::smartrest::topic::C8yTopic; use mqtt_channel::Message; - use mqtt_channel::Topic; + use tedge_config::TopicPrefix; - pub fn new_child_device_message(child_id: &str) -> Message { + pub fn new_child_device_message(child_id: &str, prefix: &TopicPrefix) -> Message { Message::new( - &Topic::new_unchecked(SMARTREST_PUBLISH_TOPIC), + &C8yTopic::upstream_topic(prefix), format!("101,{child_id},{child_id},thin-edge.io-child"), ) } diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 220293ae814..2ac0319adce 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -215,8 +215,9 @@ pub fn bridge_config( // Check the connection by using the jwt token retrieval over the mqtt. // If successful in getting the jwt token '71,xxxxx', the connection is established. fn check_device_status_c8y(tedge_config: &TEdgeConfig) -> Result { - const C8Y_TOPIC_BUILTIN_JWT_TOKEN_DOWNSTREAM: &str = "c8y/s/dat"; - const C8Y_TOPIC_BUILTIN_JWT_TOKEN_UPSTREAM: &str = "c8y/s/uat"; + let prefix = &tedge_config.c8y.bridge.topic_prefix; + let c8y_topic_builtin_jwt_token_downstream = format!("{prefix}/s/dat"); + let c8y_topic_builtin_jwt_token_upstream = format!("{prefix}/s/uat"); const CLIENT_ID: &str = "check_connection_c8y"; let mut mqtt_options = tedge_config @@ -234,14 +235,14 @@ fn check_device_status_c8y(tedge_config: &TEdgeConfig) -> Result { // We are ready to get the response, hence send the request client.publish( - C8Y_TOPIC_BUILTIN_JWT_TOKEN_UPSTREAM, + &c8y_topic_builtin_jwt_token_upstream, rumqttc::QoS::AtMostOnce, false, "", diff --git a/crates/core/tedge/src/cli/connect/jwt_token.rs b/crates/core/tedge/src/cli/connect/jwt_token.rs index 36cd65c2384..d203eb4b2b2 100644 --- a/crates/core/tedge/src/cli/connect/jwt_token.rs +++ b/crates/core/tedge/src/cli/connect/jwt_token.rs @@ -9,8 +9,9 @@ use rumqttc::QoS::AtLeastOnce; use tedge_config::TEdgeConfig; pub(crate) fn get_connected_c8y_url(tedge_config: &TEdgeConfig) -> Result { - const C8Y_TOPIC_BUILTIN_JWT_TOKEN_UPSTREAM: &str = "c8y/s/uat"; - const C8Y_TOPIC_BUILTIN_JWT_TOKEN_DOWNSTREAM: &str = "c8y/s/dat"; + let prefix = &tedge_config.c8y.bridge.topic_prefix; + let c8y_topic_builtin_jwt_token_upstream = format!("{prefix}/s/uat"); + let c8y_topic_builtin_jwt_token_downstream = format!("{prefix}/s/dat"); const CLIENT_ID: &str = "get_jwt_token_c8y"; let mut mqtt_options = tedge_config @@ -27,14 +28,14 @@ pub(crate) fn get_connected_c8y_url(tedge_config: &TEdgeConfig) -> Result { // We are ready to get the response, hence send the request client.publish( - C8Y_TOPIC_BUILTIN_JWT_TOKEN_UPSTREAM, + &c8y_topic_builtin_jwt_token_upstream, rumqttc::QoS::AtMostOnce, false, "", diff --git a/crates/core/tedge_api/src/alarm.rs b/crates/core/tedge_api/src/alarm.rs index 621cc2ec7b8..347b044db28 100644 --- a/crates/core/tedge_api/src/alarm.rs +++ b/crates/core/tedge_api/src/alarm.rs @@ -136,7 +136,7 @@ mod tests { data: Some(ThinEdgeAlarmData { severity: Some("high".into()), text: Some("I raised it".into()), - time: Some(datetime ! (2021 - 04 - 23 19: 00: 00 + 05: 00)), + time: Some(datetime!(2021-04-23 19:00:00+05:00)), extras: hashmap!{}, }), }; diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 4ac209ed512..81cc9749d58 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -53,12 +53,15 @@ impl TEdgeComponent for CumulocityMapper { "devicecontrol/notifications", "error", ] - .into_iter() - .map(<_>::to_owned) - .chain(custom_topics) - .collect(); + .into_iter() + .map(<_>::to_owned) + .chain(custom_topics) + .collect(); let bridge_actor = MqttBridgeActorBuilder::new(&tedge_config, &smartrest_topics).await; - let mut jwt_actor = C8YJwtRetriever::builder(mqtt_config.clone()); + let mut jwt_actor = C8YJwtRetriever::builder( + mqtt_config.clone(), + tedge_config.c8y.bridge.topic_prefix.clone(), + ); let mut http_actor = HttpActor::new().builder(); let c8y_http_config = (&tedge_config).try_into()?; let mut c8y_http_proxy_actor = @@ -146,6 +149,7 @@ pub fn service_monitor_client_config(tedge_config: &TEdgeConfig) -> Result Result<(), ConfigManagementError> { self.plugin_config = PluginConfig::new(self.config.plugin_config_path.as_path()); - let message = self.plugin_config.to_supported_config_types_message()?; + let message = self + .plugin_config + .to_supported_config_types_message(&self.config.c8y_prefix)?; self.messages.send(message.into()).await?; Ok(()) } async fn get_pending_operations_from_cloud(&mut self) -> Result<(), ConfigManagementError> { // Get pending operations - let message = MqttMessage::new(&C8yTopic::SmartRestResponse.to_topic()?, "500"); + let message = MqttMessage::new( + &C8yTopic::SmartRestResponse.to_topic(&self.config.c8y_prefix)?, + "500", + ); self.messages.send(message.into()).await?; Ok(()) } @@ -288,14 +298,16 @@ impl ConfigManagerActor { op_state: ActiveOperationState, failure_reason: &str, message_box: &mut ConfigManagerMessageBox, + prefix: &TopicPrefix, ) -> Result<(), ConfigManagementError> { // Fail the operation in the cloud by sending EXECUTING and FAILED responses back to back let executing_msg; let failed_msg; if let Some(child_id) = child_id { - let c8y_child_topic = - Topic::new_unchecked(&C8yTopic::ChildSmartRestResponse(child_id).to_string()); + let c8y_child_topic = Topic::new_unchecked( + &C8yTopic::ChildSmartRestResponse(child_id).with_prefix(prefix), + ); match config_operation { ConfigOperation::Snapshot => { @@ -322,12 +334,12 @@ impl ConfigManagerActor { } else { match config_operation { ConfigOperation::Snapshot => { - executing_msg = UploadConfigFileStatusMessage::executing(); - failed_msg = UploadConfigFileStatusMessage::failed(failure_reason); + executing_msg = UploadConfigFileStatusMessage::executing(prefix); + failed_msg = UploadConfigFileStatusMessage::failed(failure_reason, prefix); } ConfigOperation::Update => { - executing_msg = DownloadConfigFileStatusMessage::executing(); - failed_msg = UploadConfigFileStatusMessage::failed(failure_reason); + executing_msg = DownloadConfigFileStatusMessage::executing(prefix); + failed_msg = UploadConfigFileStatusMessage::failed(failure_reason, prefix); } }; } diff --git a/crates/extensions/c8y_config_manager/src/child_device.rs b/crates/extensions/c8y_config_manager/src/child_device.rs index 8898672477e..d14b818236c 100644 --- a/crates/extensions/c8y_config_manager/src/child_device.rs +++ b/crates/extensions/c8y_config_manager/src/child_device.rs @@ -6,6 +6,7 @@ use std::fs; use std::path::PathBuf; use std::time::Duration; use tedge_api::OperationStatus; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::Topic; use tedge_mqtt_ext::TopicFilter; @@ -88,13 +89,13 @@ impl ConfigOperationResponse { } } - pub fn get_child_topic(&self) -> String { + pub fn get_child_topic(&self, prefix: &TopicPrefix) -> String { match self { ConfigOperationResponse::Update { child_id, .. } => { - C8yTopic::ChildSmartRestResponse(child_id.to_owned()).to_string() + C8yTopic::ChildSmartRestResponse(child_id.to_owned()).with_prefix(prefix) } ConfigOperationResponse::Snapshot { child_id, .. } => { - C8yTopic::ChildSmartRestResponse(child_id.to_owned()).to_string() + C8yTopic::ChildSmartRestResponse(child_id.to_owned()).with_prefix(prefix) } } } diff --git a/crates/extensions/c8y_config_manager/src/config.rs b/crates/extensions/c8y_config_manager/src/config.rs index c43939499c0..d12f7309191 100644 --- a/crates/extensions/c8y_config_manager/src/config.rs +++ b/crates/extensions/c8y_config_manager/src/config.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use tedge_api::path::DataDir; use tedge_config::ReadError; use tedge_config::TEdgeConfig; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::TopicFilter; pub const DEFAULT_PLUGIN_CONFIG_FILE_NAME: &str = "c8y-configuration-plugin.toml"; @@ -29,6 +30,7 @@ pub struct ConfigManagerConfig { pub c8y_request_topics: TopicFilter, pub config_snapshot_response_topics: TopicFilter, pub config_update_response_topics: TopicFilter, + pub c8y_prefix: TopicPrefix, } impl ConfigManagerConfig { @@ -42,6 +44,7 @@ impl ConfigManagerConfig { mqtt_port: u16, tedge_http_host: Arc, tedge_http_port: u16, + c8y_prefix: TopicPrefix, ) -> Self { let tedge_http_host = format!("{}:{}", tedge_http_host, tedge_http_port).into(); @@ -51,7 +54,8 @@ impl ConfigManagerConfig { let file_transfer_dir = data_dir.file_transfer_dir().into(); - let c8y_request_topics: TopicFilter = C8yTopic::SmartRestRequest.into(); + let c8y_request_topics: TopicFilter = + C8yTopic::SmartRestRequest.to_topic_filter(&c8y_prefix); let config_snapshot_response_topics: TopicFilter = ConfigOperationResponseTopic::SnapshotResponse.into(); let config_update_response_topics: TopicFilter = @@ -71,6 +75,7 @@ impl ConfigManagerConfig { c8y_request_topics, config_snapshot_response_topics, config_update_response_topics, + c8y_prefix, } } @@ -86,6 +91,7 @@ impl ConfigManagerConfig { let mqtt_port = tedge_config.mqtt.client.port.get(); let tedge_http_host = tedge_config.http.client.host.clone(); let tedge_http_port = tedge_config.http.client.port; + let c8y_prefix = tedge_config.c8y.bridge.topic_prefix.clone(); let config = ConfigManagerConfig::new( config_dir, @@ -96,6 +102,7 @@ impl ConfigManagerConfig { mqtt_port, tedge_http_host, tedge_http_port, + c8y_prefix, ); Ok(config) } diff --git a/crates/extensions/c8y_config_manager/src/download.rs b/crates/extensions/c8y_config_manager/src/download.rs index 96806807c41..8cf677463be 100644 --- a/crates/extensions/c8y_config_manager/src/download.rs +++ b/crates/extensions/c8y_config_manager/src/download.rs @@ -77,7 +77,7 @@ impl ConfigDownloadManager { smartrest_request: SmartRestConfigDownloadRequest, message_box: &mut ConfigManagerMessageBox, ) -> Result<(), ConfigManagementError> { - let executing_message = DownloadConfigFileStatusMessage::executing(); + let executing_message = DownloadConfigFileStatusMessage::executing(&self.config.c8y_prefix); message_box.mqtt_publisher.send(executing_message).await?; let target_config_type = smartrest_request.config_type.clone(); @@ -102,7 +102,8 @@ impl ConfigDownloadManager { Ok(_) => { info!("The configuration download for '{target_config_type}' is successful."); - let successful_message = DownloadConfigFileStatusMessage::successful(None); + let successful_message = + DownloadConfigFileStatusMessage::successful(None, &self.config.c8y_prefix); message_box.mqtt_publisher.send(successful_message).await?; let notification_message = get_file_change_notification_message( @@ -118,7 +119,10 @@ impl ConfigDownloadManager { Err(err) => { error!("The configuration download for '{target_config_type}' failed.",); - let failed_message = DownloadConfigFileStatusMessage::failed(&err.to_string()); + let failed_message = DownloadConfigFileStatusMessage::failed( + &err.to_string(), + &self.config.c8y_prefix, + ); message_box.mqtt_publisher.send(failed_message).await?; Err(err) } @@ -196,6 +200,7 @@ impl ConfigDownloadManager { ActiveOperationState::Pending, &failure_reason, message_box, + &self.config.c8y_prefix, ) .await?; } else { @@ -231,7 +236,8 @@ impl ConfigDownloadManager { config_response: &ConfigOperationResponse, message_box: &mut ConfigManagerMessageBox, ) -> Result, ConfigManagementError> { - let c8y_child_topic = Topic::new_unchecked(&config_response.get_child_topic()); + let c8y_child_topic = + Topic::new_unchecked(&config_response.get_child_topic(&self.config.c8y_prefix)); let child_device_payload = config_response.get_payload(); let child_id = config_response.get_child_id(); let config_type = config_response.get_config_type(); @@ -325,6 +331,7 @@ impl ConfigDownloadManager { operation_state, &format!("Timeout due to lack of response from child device: {child_id} for config type: {config_type}"), message_box, + &self.config.c8y_prefix, ).await } else { // Ignore the timeout as the operation has already completed. diff --git a/crates/extensions/c8y_config_manager/src/lib.rs b/crates/extensions/c8y_config_manager/src/lib.rs index 37c1993d747..e4ae158b081 100644 --- a/crates/extensions/c8y_config_manager/src/lib.rs +++ b/crates/extensions/c8y_config_manager/src/lib.rs @@ -51,7 +51,7 @@ pub struct ConfigManagerBuilder { impl ConfigManagerConfig { pub fn subscriptions(&self) -> TopicFilter { vec![ - "c8y/s/ds", + &format!("{}/s/ds", self.c8y_prefix), "tedge/+/commands/res/config_snapshot", "tedge/+/commands/res/config_update", ] diff --git a/crates/extensions/c8y_config_manager/src/plugin_config.rs b/crates/extensions/c8y_config_manager/src/plugin_config.rs index 13adaa24eb5..7c6fdeb322a 100644 --- a/crates/extensions/c8y_config_manager/src/plugin_config.rs +++ b/crates/extensions/c8y_config_manager/src/plugin_config.rs @@ -9,6 +9,7 @@ use std::fs; use std::hash::Hash; use std::hash::Hasher; use std::path::Path; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::MqttError; use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::Topic; @@ -147,16 +148,20 @@ impl PluginConfig { self } - pub fn to_supported_config_types_message(&self) -> Result { - let topic = C8yTopic::SmartRestResponse.to_topic()?; + pub fn to_supported_config_types_message( + &self, + prefix: &TopicPrefix, + ) -> Result { + let topic = C8yTopic::SmartRestResponse.to_topic(prefix)?; Ok(MqttMessage::new(&topic, self.to_smartrest_payload())) } pub fn to_supported_config_types_message_for_child( &self, child_id: &str, + prefix: &TopicPrefix, ) -> Result { - let topic_str = &format!("c8y/s/us/{child_id}"); + let topic_str = &format!("{prefix}/s/us/{child_id}"); let topic = Topic::new(topic_str)?; Ok(MqttMessage::new(&topic, self.to_smartrest_payload())) } diff --git a/crates/extensions/c8y_config_manager/src/tests.rs b/crates/extensions/c8y_config_manager/src/tests.rs index 97154145c66..e11b1110c2e 100644 --- a/crates/extensions/c8y_config_manager/src/tests.rs +++ b/crates/extensions/c8y_config_manager/src/tests.rs @@ -54,10 +54,10 @@ async fn test_config_plugin_init() -> Result<(), DynError> { mqtt_message_box .assert_received([ MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, format!("119,c8y-configuration-plugin,{test_config_type}"), // Supported config types ), - MqttMessage::new(&C8yTopic::SmartRestResponse.to_topic().unwrap(), "500"), // Get pending operations + MqttMessage::new(&C8yTopic::SmartRestResponse.to_topic(&"c8y".into()).unwrap(), "500"), // Get pending operations ]) .await; Ok(()) @@ -95,7 +95,7 @@ async fn test_config_upload_tedge_device() -> Result<(), DynError> { // Assert EXECUTING SmartREST MQTT message mqtt_message_box .assert_received([MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", )]) .await; @@ -117,7 +117,7 @@ async fn test_config_upload_tedge_device() -> Result<(), DynError> { // Assert SUCCESSFUL SmartREST MQTT message mqtt_message_box .assert_received([MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "503,c8y_UploadConfigFile,test-url", )]) .await; @@ -151,7 +151,7 @@ async fn test_config_download_tedge_device() -> Result<(), DynError> { let download_url = "http://test.domain.com"; mqtt_message_box .send(MqttMessage::new( - &C8yTopic::SmartRestRequest.to_topic().unwrap(), + &C8yTopic::SmartRestRequest.to_topic(&"c8y".into()).unwrap(), format!("524,{device_id},{download_url},{test_config_type}"), )) .await?; @@ -159,7 +159,7 @@ async fn test_config_download_tedge_device() -> Result<(), DynError> { // Assert EXECUTING SmartREST MQTT message mqtt_message_box .assert_received([MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "501,c8y_DownloadConfigFile", )]) .await; @@ -181,7 +181,7 @@ async fn test_config_download_tedge_device() -> Result<(), DynError> { // Assert SUCCESSFUL SmartREST MQTT message mqtt_message_box .assert_received([MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "503,c8y_DownloadConfigFile", )]) .await; @@ -282,7 +282,7 @@ async fn test_child_device_config_upload_executing_response_mapping() -> Result< mqtt_message_box .with_timeout(TEST_TIMEOUT) .assert_received([MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", )]) .await; @@ -333,11 +333,11 @@ async fn test_child_device_config_upload_failed_response_mapping() -> Result<(), .with_timeout(TEST_TIMEOUT) .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_UploadConfigFile,upload failed", ), ]) @@ -385,11 +385,11 @@ async fn test_invalid_config_snapshot_response_child_device() -> Result<(), DynE .assert_received( [ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_UploadConfigFile,Failed to parse response from child device with: expected value at line 1 column 1", ), ], @@ -450,11 +450,11 @@ async fn test_timeout_on_no_config_snapshot_response_child_device() -> Result<() mqtt_message_box .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_UploadConfigFile,Timeout due to lack of response from child device: child-aa for config type: file_a", ), ], @@ -524,11 +524,11 @@ async fn test_child_device_successful_config_snapshot_response_mapping() -> Resu .with_timeout(TEST_TIMEOUT) .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "503,c8y_UploadConfigFile,test-url", ), ]) @@ -598,11 +598,11 @@ async fn test_child_config_snapshot_successful_response_without_uploaded_file_ma .with_timeout(TEST_TIMEOUT) .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_UploadConfigFile,Failed with file not found", ), ]) @@ -638,7 +638,7 @@ async fn test_child_device_config_download_request_mapping() -> Result<(), DynEr let download_url = "http://test.domain.com"; let c8y_config_download_msg = MqttMessage::new( - &C8yTopic::SmartRestRequest.to_topic().unwrap(), + &C8yTopic::SmartRestRequest.to_topic(&"c8y".into()).unwrap(), format!("524,{child_device_id},{download_url},{test_config_type}"), ); mqtt_message_box.send(c8y_config_download_msg).await?; @@ -721,7 +721,7 @@ async fn test_child_device_config_update_executing_response_mapping() -> Result< mqtt_message_box .with_timeout(TEST_TIMEOUT) .assert_received([MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_DownloadConfigFile", )]) .await; @@ -771,11 +771,11 @@ async fn test_child_device_config_update_successful_response_mapping() -> Result .with_timeout(TEST_TIMEOUT) .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_DownloadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "503,c8y_DownloadConfigFile", ), ]) @@ -827,11 +827,11 @@ async fn test_child_device_config_update_failed_response_mapping() -> Result<(), .with_timeout(TEST_TIMEOUT) .assert_received([ MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_DownloadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_DownloadConfigFile,download failed", ), ]) @@ -867,7 +867,7 @@ async fn test_child_device_config_download_fail_with_broken_url() -> Result<(), let download_url = "bad-url"; let c8y_config_download_msg = MqttMessage::new( - &C8yTopic::SmartRestRequest.to_topic().unwrap(), + &C8yTopic::SmartRestRequest.to_topic(&"c8y".into()).unwrap(), format!("524,{child_device_id},{download_url},{test_config_type}"), ); mqtt_message_box.send(c8y_config_download_msg).await?; @@ -894,11 +894,11 @@ async fn test_child_device_config_download_fail_with_broken_url() -> Result<(), mqtt_message_box .with_timeout(TEST_TIMEOUT) .assert_received([MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "501,c8y_DownloadConfigFile", ), MqttMessage::new( - &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic()?, + &C8yTopic::ChildSmartRestResponse(child_device_id.into()).to_topic(&"c8y".into())?, "502,c8y_DownloadConfigFile,Downloading the config file update from bad-url failed with Failed with file not found", ), ], @@ -953,11 +953,11 @@ async fn test_multiline_smartrest_requests() -> Result<(), DynError> { mqtt_message_box .assert_received([ MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "501,c8y_UploadConfigFile", ), MqttMessage::new( - &C8yTopic::SmartRestResponse.to_topic()?, + &C8yTopic::SmartRestResponse.to_topic(&"c8y".into())?, "503,c8y_UploadConfigFile,test-url", ), ]) @@ -990,6 +990,7 @@ async fn spawn_config_manager( mqtt_port, tedge_host.into(), tedge_http_port, + "c8y".into(), ); let mut mqtt_builder: SimpleMessageBoxBuilder = diff --git a/crates/extensions/c8y_config_manager/src/upload.rs b/crates/extensions/c8y_config_manager/src/upload.rs index 4460c9e834d..7d39d8a0322 100644 --- a/crates/extensions/c8y_config_manager/src/upload.rs +++ b/crates/extensions/c8y_config_manager/src/upload.rs @@ -76,7 +76,7 @@ impl ConfigUploadManager { message_box: &mut ConfigManagerMessageBox, ) -> Result<(), ConfigManagementError> { // set config upload request to executing - let msg = UploadConfigFileStatusMessage::executing(); + let msg = UploadConfigFileStatusMessage::executing(&self.config.c8y_prefix); message_box.mqtt_publisher.send(msg).await?; let plugin_config = PluginConfig::new(&self.config.plugin_config_path); @@ -102,14 +102,19 @@ impl ConfigUploadManager { Ok(upload_event_url) => { info!("The configuration upload for '{target_config_type}' is successful."); - let successful_message = - UploadConfigFileStatusMessage::successful(Some(&upload_event_url)); + let successful_message = UploadConfigFileStatusMessage::successful( + Some(&upload_event_url), + &self.config.c8y_prefix, + ); message_box.mqtt_publisher.send(successful_message).await?; } Err(err) => { error!("The configuration upload for '{target_config_type}' failed.",); - let failed_message = UploadConfigFileStatusMessage::failed(&err.to_string()); + let failed_message = UploadConfigFileStatusMessage::failed( + &err.to_string(), + &self.config.c8y_prefix, + ); message_box.mqtt_publisher.send(failed_message).await?; } } @@ -177,7 +182,8 @@ impl ConfigUploadManager { message_box: &mut ConfigManagerMessageBox, ) -> Result, ConfigManagementError> { let payload = config_response.get_payload(); - let c8y_child_topic = Topic::new_unchecked(&config_response.get_child_topic()); + let c8y_child_topic = + Topic::new_unchecked(&config_response.get_child_topic(&self.config.c8y_prefix)); let config_dir = self.config.config_dir.display(); let child_id = config_response.get_child_id(); let config_type = config_response.get_config_type(); @@ -310,8 +316,10 @@ impl ConfigUploadManager { ))); // Publish supported configuration types for child devices - let message = child_plugin_config - .to_supported_config_types_message_for_child(&config_response.get_child_id())?; + let message = child_plugin_config.to_supported_config_types_message_for_child( + &config_response.get_child_id(), + &self.config.c8y_prefix, + )?; Ok(vec![message]) } } @@ -321,7 +329,8 @@ impl ConfigUploadManager { config_response: &ConfigOperationResponse, message_box: &mut ConfigManagerMessageBox, ) -> Result { - let c8y_child_topic = Topic::new_unchecked(&config_response.get_child_topic()); + let c8y_child_topic = + Topic::new_unchecked(&config_response.get_child_topic(&self.config.c8y_prefix)); let uploaded_config_file_path = config_response .file_transfer_repository_full_path(self.config.file_transfer_dir.clone()); @@ -383,6 +392,7 @@ impl ConfigUploadManager { operation_state, &format!("Timeout due to lack of response from child device: {child_id} for config type: {config_type}"), message_box, + &self.config.c8y_prefix, ).await } else { // Ignore the timeout as the operation has already completed. diff --git a/crates/extensions/c8y_firmware_manager/src/actor.rs b/crates/extensions/c8y_firmware_manager/src/actor.rs index ff0f8f7e7c5..9cc0e652a0d 100644 --- a/crates/extensions/c8y_firmware_manager/src/actor.rs +++ b/crates/extensions/c8y_firmware_manager/src/actor.rs @@ -637,7 +637,8 @@ impl FirmwareManagerActor { child_id: &str, ) -> Result<(), FirmwareManagementError> { let c8y_child_topic = Topic::new_unchecked( - &C8yTopic::ChildSmartRestResponse(child_id.to_string()).to_string(), + &C8yTopic::ChildSmartRestResponse(child_id.to_string()) + .with_prefix(&self.config.c8y_prefix), ); let executing_msg = MqttMessage::new( &c8y_child_topic, @@ -652,7 +653,8 @@ impl FirmwareManagerActor { child_id: &str, ) -> Result<(), FirmwareManagementError> { let c8y_child_topic = Topic::new_unchecked( - &C8yTopic::ChildSmartRestResponse(child_id.to_string()).to_string(), + &C8yTopic::ChildSmartRestResponse(child_id.to_string()) + .with_prefix(&self.config.c8y_prefix), ); let successful_msg = MqttMessage::new( &c8y_child_topic, @@ -668,7 +670,8 @@ impl FirmwareManagerActor { failure_reason: &str, ) -> Result<(), FirmwareManagementError> { let c8y_child_topic = Topic::new_unchecked( - &C8yTopic::ChildSmartRestResponse(child_id.to_string()).to_string(), + &C8yTopic::ChildSmartRestResponse(child_id.to_string()) + .with_prefix(&self.config.c8y_prefix), ); let failed_msg = MqttMessage::new( &c8y_child_topic, @@ -683,7 +686,8 @@ impl FirmwareManagerActor { operation_entry: &FirmwareOperationEntry, ) -> Result<(), FirmwareManagementError> { let c8y_child_topic = Topic::new_unchecked( - &C8yTopic::ChildSmartRestResponse(operation_entry.child_id.clone()).to_string(), + &C8yTopic::ChildSmartRestResponse(operation_entry.child_id.clone()) + .with_prefix(&self.config.c8y_prefix), ); let installed_firmware_payload = format!( "115,{},{},{}", @@ -732,7 +736,10 @@ impl FirmwareManagerActor { // Candidate to be removed since another actor should be in charge of this. async fn get_pending_operations_from_cloud(&mut self) -> Result<(), FirmwareManagementError> { - let message = MqttMessage::new(&C8yTopic::SmartRestResponse.to_topic()?, "500"); + let message = MqttMessage::new( + &C8yTopic::SmartRestResponse.to_topic(&self.config.c8y_prefix)?, + "500", + ); self.message_box.mqtt_publisher.send(message).await?; Ok(()) } diff --git a/crates/extensions/c8y_firmware_manager/src/config.rs b/crates/extensions/c8y_firmware_manager/src/config.rs index 8b084c32398..31415960df1 100644 --- a/crates/extensions/c8y_firmware_manager/src/config.rs +++ b/crates/extensions/c8y_firmware_manager/src/config.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use std::time::Duration; use tedge_api::path::DataDir; use tedge_config::TEdgeConfig; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::TopicFilter; const FIRMWARE_UPDATE_RESPONSE_TOPICS: &str = "tedge/+/commands/res/firmware_update"; @@ -24,9 +25,11 @@ pub struct FirmwareManagerConfig { pub firmware_update_response_topics: TopicFilter, pub timeout_sec: Duration, pub c8y_end_point: C8yEndPoint, + pub c8y_prefix: TopicPrefix, } impl FirmwareManagerConfig { + #[allow(clippy::too_many_arguments)] pub fn new( tedge_device_id: String, local_http_host: Arc, @@ -35,10 +38,11 @@ impl FirmwareManagerConfig { data_dir: DataDir, timeout_sec: Duration, c8y_url: String, + c8y_prefix: TopicPrefix, ) -> Self { let local_http_host = format!("{}:{}", local_http_host, local_http_port).into(); - let c8y_request_topics = C8yTopic::SmartRestRequest.into(); + let c8y_request_topics = C8yTopic::SmartRestRequest.to_topic_filter(&c8y_prefix); let firmware_update_response_topics = TopicFilter::new_unchecked(FIRMWARE_UPDATE_RESPONSE_TOPICS); @@ -53,6 +57,7 @@ impl FirmwareManagerConfig { firmware_update_response_topics, timeout_sec, c8y_end_point, + c8y_prefix, } } @@ -67,6 +72,7 @@ impl FirmwareManagerConfig { let timeout_sec = tedge_config.firmware.child.update.timeout.duration(); let c8y_url = tedge_config.c8y.http.or_config_not_set()?.to_string(); + let c8y_prefix = tedge_config.c8y.bridge.topic_prefix.clone(); Ok(Self::new( tedge_device_id, @@ -76,6 +82,7 @@ impl FirmwareManagerConfig { data_dir, timeout_sec, c8y_url, + c8y_prefix, )) } diff --git a/crates/extensions/c8y_firmware_manager/src/lib.rs b/crates/extensions/c8y_firmware_manager/src/lib.rs index bccffa73a60..a3a5ed14b0d 100644 --- a/crates/extensions/c8y_firmware_manager/src/lib.rs +++ b/crates/extensions/c8y_firmware_manager/src/lib.rs @@ -28,6 +28,7 @@ use tedge_actors::RuntimeRequest; use tedge_actors::RuntimeRequestSink; use tedge_actors::ServiceProvider; use tedge_api::path::DataDir; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::TopicFilter; use tedge_timer_ext::SetTimeout; @@ -62,8 +63,10 @@ impl FirmwareManagerBuilder { signal_receiver, ); - let mqtt_publisher = - mqtt_actor.connect_consumer(Self::subscriptions(), input_sender.clone().into()); + let mqtt_publisher = mqtt_actor.connect_consumer( + Self::subscriptions(&config.c8y_prefix), + input_sender.clone().into(), + ); let jwt_retriever = JwtRetriever::new("Firmware => JWT", jwt_actor); let timer_sender = timer_actor.connect_consumer(NoConfig, input_sender.clone().into()); let download_sender = downloader_actor.connect_consumer(NoConfig, input_sender.into()); @@ -85,10 +88,13 @@ impl FirmwareManagerBuilder { Ok(()) } - pub fn subscriptions() -> TopicFilter { - vec!["c8y/s/ds", "tedge/+/commands/res/firmware_update"] - .try_into() - .expect("Infallible") + fn subscriptions(prefix: &TopicPrefix) -> TopicFilter { + vec![ + &format!("{prefix}/s/ds"), + "tedge/+/commands/res/firmware_update", + ] + .try_into() + .expect("Infallible") } } diff --git a/crates/extensions/c8y_firmware_manager/src/tests.rs b/crates/extensions/c8y_firmware_manager/src/tests.rs index 303b0c65cbd..d8ab5727e72 100644 --- a/crates/extensions/c8y_firmware_manager/src/tests.rs +++ b/crates/extensions/c8y_firmware_manager/src/tests.rs @@ -603,7 +603,7 @@ async fn required_init_state_recreated_on_startup() -> Result<(), DynError> { // Assert that the startup succeeds and the plugin requests for pending operations mqtt_message_box - .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(), "500")]) + .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(&"c8y".into()), "500")]) .await; for dir in ["cache", "firmware", "file-transfer"] { @@ -630,7 +630,7 @@ async fn required_init_state_recreated_on_startup() -> Result<(), DynError> { // Assert that the startup succeeds again and the mapper requests for pending operations mqtt_message_box - .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(), "500")]) + .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(&"c8y".into()), "500")]) .await; // Assert that all the required directories are recreated @@ -717,6 +717,7 @@ async fn spawn_firmware_manager( tmp_dir.utf8_path_buf().into(), timeout_sec, C8Y_HOST.into(), + "c8y".into(), ); let mut mqtt_builder: SimpleMessageBoxBuilder = diff --git a/crates/extensions/c8y_http_proxy/src/credentials.rs b/crates/extensions/c8y_http_proxy/src/credentials.rs index 2a36d23d98e..e8f096f4ae2 100644 --- a/crates/extensions/c8y_http_proxy/src/credentials.rs +++ b/crates/extensions/c8y_http_proxy/src/credentials.rs @@ -6,6 +6,7 @@ use tedge_actors::Sequential; use tedge_actors::Server; use tedge_actors::ServerActorBuilder; use tedge_actors::ServerConfig; +use tedge_config::TopicPrefix; pub type JwtRequest = (); pub type JwtResult = Result; @@ -21,8 +22,9 @@ pub struct C8YJwtRetriever { impl C8YJwtRetriever { pub fn builder( mqtt_config: mqtt_channel::Config, + topic_prefix: TopicPrefix, ) -> ServerActorBuilder { - let mqtt_retriever = C8yMqttJwtTokenRetriever::new(mqtt_config); + let mqtt_retriever = C8yMqttJwtTokenRetriever::new(mqtt_config, topic_prefix); let server = C8YJwtRetriever { mqtt_retriever }; ServerActorBuilder::new(server, &ServerConfig::default(), Sequential) } diff --git a/crates/extensions/c8y_log_manager/src/actor.rs b/crates/extensions/c8y_log_manager/src/actor.rs index 7036545be02..afb5375e5d5 100644 --- a/crates/extensions/c8y_log_manager/src/actor.rs +++ b/crates/extensions/c8y_log_manager/src/actor.rs @@ -112,7 +112,7 @@ impl LogManagerActor { &mut self, smartrest_request: &SmartRestLogRequest, ) -> Result<(), anyhow::Error> { - let executing = LogfileRequest::executing(); + let executing = LogfileRequest::executing(&self.config.c8y_prefix); self.mqtt_publisher.send(executing).await?; let log_path = log_manager::new_read_logs( @@ -135,7 +135,8 @@ impl LogManagerActor { ) .await?; - let successful = LogfileRequest::successful(Some(&upload_event_url)); + let successful = + LogfileRequest::successful(Some(&upload_event_url), &self.config.c8y_prefix); self.mqtt_publisher.send(successful).await?; std::fs::remove_file(log_path)?; @@ -154,7 +155,7 @@ impl LogManagerActor { Ok(()) => Ok(()), Err(error) => { let error_message = format!("Handling of operation failed with {}", error); - let failed_msg = LogfileRequest::failed(&error_message); + let failed_msg = LogfileRequest::failed(&error_message, &self.config.c8y_prefix); self.mqtt_publisher.send(failed_msg).await?; error!( "Handling of operation for log type {} failed with: {}", @@ -198,7 +199,7 @@ impl LogManagerActor { /// updates the log types on Cumulocity /// sends 118,typeA,typeB,... on mqtt pub async fn publish_supported_log_types(&mut self) -> Result<(), anyhow::Error> { - let topic = C8yTopic::SmartRestResponse.to_topic()?; + let topic = C8yTopic::SmartRestResponse.to_topic(&self.config.c8y_prefix)?; let mut config_types = self.plugin_config.get_all_file_types(); config_types.sort(); let supported_config_types = config_types.join(","); @@ -209,7 +210,10 @@ impl LogManagerActor { async fn get_pending_operations_from_cloud(&mut self) -> Result<(), anyhow::Error> { // Get pending operations - let msg = MqttMessage::new(&C8yTopic::SmartRestResponse.to_topic()?, "500"); + let msg = MqttMessage::new( + &C8yTopic::SmartRestResponse.to_topic(&self.config.c8y_prefix)?, + "500", + ); self.mqtt_publisher.send(msg).await?; Ok(()) } @@ -356,6 +360,7 @@ mod tests { ops_dir: temp_dir.to_path_buf(), plugin_config_dir: temp_dir.to_path_buf(), plugin_config_path: temp_dir.join("c8y-log-plugin.toml"), + c8y_prefix: "c8y".into(), }; let mut mqtt_builder: SimpleMessageBoxBuilder = @@ -459,7 +464,7 @@ mod tests { Some(C8YRestRequest::UploadLogBinary(UploadLogBinary { log_type: "type_two".to_string(), log_content: "filename: file_c\nSome content\n".to_string(), - device_id: "SUT".into() + device_id: "SUT".into(), })) ); diff --git a/crates/extensions/c8y_log_manager/src/config.rs b/crates/extensions/c8y_log_manager/src/config.rs index 483b967a460..ecb09633671 100644 --- a/crates/extensions/c8y_log_manager/src/config.rs +++ b/crates/extensions/c8y_log_manager/src/config.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::sync::Arc; use tedge_config::ReadError; use tedge_config::TEdgeConfig; +use tedge_config::TopicPrefix; pub const DEFAULT_PLUGIN_CONFIG_FILE_NAME: &str = "c8y-log-plugin.toml"; pub const DEFAULT_PLUGIN_CONFIG_DIR_NAME: &str = "c8y/"; @@ -21,6 +22,7 @@ pub struct LogManagerConfig { pub ops_dir: PathBuf, pub plugin_config_dir: PathBuf, pub plugin_config_path: PathBuf, + pub c8y_prefix: TopicPrefix, } impl LogManagerConfig { @@ -34,6 +36,7 @@ impl LogManagerConfig { let log_dir = tedge_config.logs.path.as_std_path().to_path_buf(); let mqtt_host = tedge_config.mqtt.client.host.clone(); let mqtt_port = u16::from(tedge_config.mqtt.client.port); + let c8y_prefix = tedge_config.c8y.bridge.topic_prefix.clone(); let tedge_http_host = tedge_config.http.client.host.clone(); let tedge_http_port = tedge_config.http.client.port; @@ -56,6 +59,7 @@ impl LogManagerConfig { ops_dir, plugin_config_dir, plugin_config_path, + c8y_prefix, }) } } diff --git a/crates/extensions/c8y_log_manager/src/lib.rs b/crates/extensions/c8y_log_manager/src/lib.rs index 79e4b3b2d4a..876ea645381 100644 --- a/crates/extensions/c8y_log_manager/src/lib.rs +++ b/crates/extensions/c8y_log_manager/src/lib.rs @@ -23,11 +23,13 @@ use tedge_actors::RuntimeRequest; use tedge_actors::RuntimeRequestSink; use tedge_actors::ServiceProvider; use tedge_actors::SimpleMessageBoxBuilder; +use tedge_config::TopicPrefix; use tedge_file_system_ext::FsWatchEvent; use tedge_mqtt_ext::*; use tedge_utils::file::create_directory_with_defaults; use tedge_utils::file::create_file_with_defaults; use tedge_utils::file::FileError; + /// This is an actor builder. pub struct LogManagerBuilder { config: LogManagerConfig, @@ -50,7 +52,7 @@ impl LogManagerBuilder { let box_builder = SimpleMessageBoxBuilder::new("C8Y Log Manager", 16); let http_proxy = C8YHttpProxy::new("LogManager => C8Y", http); let mqtt_publisher = mqtt.connect_consumer( - LogManagerBuilder::subscriptions(), + LogManagerBuilder::subscriptions(&config.c8y_prefix), adapt(&box_builder.get_sender()), ); fs_notify.register_peer( @@ -88,10 +90,10 @@ impl LogManagerBuilder { } /// List of MQTT topic filters the log actor has to subscribe to - fn subscriptions() -> TopicFilter { + fn subscriptions(prefix: &TopicPrefix) -> TopicFilter { vec![ // subscribing to c8y smartrest requests - C8yTopic::SmartRestRequest.to_string().as_ref(), + &C8yTopic::SmartRestRequest.with_prefix(prefix), // subscribing also to c8y bridge health topic to know when the bridge is up C8Y_BRIDGE_HEALTH_TOPIC, ] diff --git a/crates/extensions/c8y_mapper_ext/src/alarm_converter.rs b/crates/extensions/c8y_mapper_ext/src/alarm_converter.rs index acb39e00629..6e1a0ccd03d 100644 --- a/crates/extensions/c8y_mapper_ext/src/alarm_converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/alarm_converter.rs @@ -7,13 +7,14 @@ use tedge_api::alarm::ThinEdgeAlarm; use tedge_api::alarm::ThinEdgeAlarmDeserializerError; use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::EntityStore; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::Message; use tedge_mqtt_ext::Topic; use crate::error::ConversionError; const INTERNAL_ALARMS_TOPIC: &str = "c8y-internal/alarms/"; -const C8Y_JSON_MQTT_ALARMS_TOPIC: &str = "c8y/alarm/alarms/create"; +const C8Y_JSON_MQTT_ALARMS_TOPIC: &str = "alarm/alarms/create"; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum AlarmConverter { @@ -38,6 +39,7 @@ impl AlarmConverter { input_message: &Message, alarm_type: &str, entity_store: &EntityStore, + c8y_prefix: &TopicPrefix, ) -> Result, ConversionError> { let mut output_messages: Vec = Vec::new(); match self { @@ -68,14 +70,17 @@ impl AlarmConverter { { // JSON over MQTT let cumulocity_alarm_json = serde_json::to_string(&c8y_create_alarm)?; - let c8y_alarm_topic = Topic::new_unchecked(C8Y_JSON_MQTT_ALARMS_TOPIC); + let c8y_json_mqtt_alarms_topic = + format!("{c8y_prefix}/{C8Y_JSON_MQTT_ALARMS_TOPIC}"); + let c8y_alarm_topic = Topic::new_unchecked(&c8y_json_mqtt_alarms_topic); output_messages.push(Message::new(&c8y_alarm_topic, cumulocity_alarm_json)); } _ => { // SmartREST let smartrest_alarm = alarm::serialize_alarm(&c8y_alarm)?; - let smartrest_topic = - C8yTopic::from(&c8y_alarm).to_topic().expect("Infallible"); + let smartrest_topic = C8yTopic::from(&c8y_alarm) + .to_topic(c8y_prefix) + .expect("Infallible"); output_messages.push(Message::new(&smartrest_topic, smartrest_alarm)); } } diff --git a/crates/extensions/c8y_mapper_ext/src/config.rs b/crates/extensions/c8y_mapper_ext/src/config.rs index e8598bf6b9d..410f5bd0093 100644 --- a/crates/extensions/c8y_mapper_ext/src/config.rs +++ b/crates/extensions/c8y_mapper_ext/src/config.rs @@ -22,6 +22,7 @@ use tedge_config::ConfigNotSet; use tedge_config::ReadError; use tedge_config::TEdgeConfig; use tedge_config::TEdgeConfigReaderService; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::TopicFilter; use tracing::log::warn; @@ -51,6 +52,7 @@ pub struct C8yMapperConfig { pub mqtt_schema: MqttSchema, pub enable_auto_register: bool, pub clean_start: bool, + pub c8y_prefix: TopicPrefix, } impl C8yMapperConfig { @@ -74,6 +76,7 @@ impl C8yMapperConfig { mqtt_schema: MqttSchema, enable_auto_register: bool, clean_start: bool, + c8y_prefix: TopicPrefix, ) -> Self { let ops_dir = config_dir .join(SUPPORTED_OPERATIONS_DIRECTORY) @@ -101,6 +104,7 @@ impl C8yMapperConfig { mqtt_schema, enable_auto_register, clean_start, + c8y_prefix, } } @@ -139,8 +143,9 @@ impl C8yMapperConfig { config_update: tedge_config.c8y.enable.config_update, firmware_update: tedge_config.c8y.enable.firmware_update, }; + let c8y_prefix = tedge_config.c8y.bridge.topic_prefix.clone(); - let mut topics = Self::default_internal_topic_filter(&config_dir)?; + let mut topics = Self::default_internal_topic_filter(&config_dir, &c8y_prefix)?; let enable_auto_register = tedge_config.c8y.entity_store.auto_register; let clean_start = tedge_config.c8y.entity_store.clean_start; @@ -199,16 +204,18 @@ impl C8yMapperConfig { mqtt_schema, enable_auto_register, clean_start, + c8y_prefix, )) } pub fn default_internal_topic_filter( config_dir: &Path, + prefix: &TopicPrefix, ) -> Result { let mut topic_filter: TopicFilter = vec![ "c8y-internal/alarms/+/+/+/+/+/a/+", - C8yTopic::SmartRestRequest.to_string().as_str(), - C8yDeviceControlTopic::name(), + C8yTopic::SmartRestRequest.with_prefix(prefix).as_str(), + &C8yDeviceControlTopic::name(prefix), ] .try_into() .expect("topics that mapper should subscribe to"); diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index d3817216380..a5503642f59 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -44,7 +44,6 @@ use c8y_api::smartrest::smartrest_serializer::EmbeddedCsv; use c8y_api::smartrest::smartrest_serializer::TextOrCsv; use c8y_api::smartrest::topic::publish_topic_from_ancestors; use c8y_api::smartrest::topic::C8yTopic; -use c8y_api::smartrest::topic::SMARTREST_PUBLISH_TOPIC; use c8y_auth_proxy::url::ProxyUrlGenerator; use c8y_http_proxy::handle::C8YHttpProxy; use c8y_http_proxy::messages::CreateEvent; @@ -84,6 +83,7 @@ use tedge_api::pending_entity_store::PendingEntityData; use tedge_api::DownloadInfo; use tedge_api::EntityStore; use tedge_config::TEdgeConfigError; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::Message; use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::Topic; @@ -248,8 +248,9 @@ impl CumulocityConverter { let mqtt_schema = config.mqtt_schema.clone(); + let prefix = &config.c8y_prefix; let mapper_config = MapperConfig { - out_topic: Topic::new_unchecked("c8y/measurement/measurements/create"), + out_topic: Topic::new_unchecked(&format!("{prefix}/measurement/measurements/create")), errors_topic: mqtt_schema.error_topic(), }; @@ -325,6 +326,7 @@ impl CumulocityConverter { display_name, display_type, &ancestors_external_ids, + &self.config.c8y_prefix, ) .context("Could not create device creation message")?; Ok(vec![child_creation_message]) @@ -343,6 +345,7 @@ impl CumulocityConverter { display_type.unwrap_or(&self.service_type), "up", &ancestors_external_ids, + &self.config.c8y_prefix, ) .context("Could not create service creation message")?; Ok(vec![service_creation_message]) @@ -361,7 +364,10 @@ impl CumulocityConverter { let mut ancestors_external_ids = self.entity_store.ancestors_external_ids(entity_topic_id)?; ancestors_external_ids.insert(0, entity.external_id.as_ref().into()); - Ok(publish_topic_from_ancestors(&ancestors_external_ids)) + Ok(publish_topic_from_ancestors( + &ancestors_external_ids, + &self.config.c8y_prefix, + )) } /// Generates external ID of the given entity. @@ -488,7 +494,7 @@ impl CumulocityConverter { // If the message doesn't contain any fields other than `text` and `time`, convert to SmartREST let message = if c8y_event.extras.is_empty() { let smartrest_event = Self::serialize_to_smartrest(&c8y_event)?; - let smartrest_topic = Topic::new_unchecked(SMARTREST_PUBLISH_TOPIC); + let smartrest_topic = C8yTopic::upstream_topic(&self.config.c8y_prefix); Message::new(&smartrest_topic, smartrest_event) } else { // If the message contains extra fields other than `text` and `time`, convert to Cumulocity JSON @@ -529,6 +535,7 @@ impl CumulocityConverter { input, alarm_type, &self.entity_store, + &self.config.c8y_prefix, )?; Ok(mqtt_messages) @@ -549,6 +556,7 @@ impl CumulocityConverter { entity_metadata, &ancestors_external_ids, message, + &self.config.c8y_prefix, )) } @@ -660,7 +668,9 @@ impl CumulocityConverter { .. }, ) => { - let topic = C8yTopic::SmartRestResponse.to_topic().unwrap(); + let topic = C8yTopic::SmartRestResponse + .to_topic(&self.config.c8y_prefix) + .unwrap(); let msg1 = Message::new(&topic, set_operation_executing(operation)); let msg2 = Message::new(&topic, fail_operation(operation, &err.to_string())); error!("{err}"); @@ -809,13 +819,14 @@ impl CumulocityConverter { Ok(child_process) => { let op_name = operation_name.to_owned(); let mut mqtt_publisher = self.mqtt_publisher.clone(); + let c8y_prefix = self.config.c8y_prefix.clone(); tokio::spawn(async move { let op_name = op_name.as_str(); let logger = log_file.buffer(); // mqtt client publishes executing - let topic = C8yTopic::SmartRestResponse.to_topic().unwrap(); + let topic = C8yTopic::SmartRestResponse.to_topic(&c8y_prefix).unwrap(); let executing_str = set_operation_executing(op_name); mqtt_publisher .send(Message::new(&topic, executing_str.as_str())) @@ -1200,10 +1211,12 @@ impl CumulocityConverter { self.alarm_converter.process_internal_alarm(message); Ok(vec![]) } - topic if C8yDeviceControlTopic::accept(topic) => { + topic if C8yDeviceControlTopic::accept(topic, &self.config.c8y_prefix) => { self.parse_c8y_devicecontrol_topic(message).await } - topic if C8yTopic::accept(topic) => self.parse_c8y_smartrest_topics(message).await, + topic if topic.name.starts_with(self.config.c8y_prefix.as_str()) => { + self.parse_c8y_smartrest_topics(message).await + } _ => { error!("Unsupported topic: {}", message.topic.name); Ok(vec![]) @@ -1216,11 +1229,13 @@ impl CumulocityConverter { fn try_init_messages(&mut self) -> Result, ConversionError> { let mut messages = self.parse_base_inventory_file()?; - let supported_operations_message = self.create_supported_operations(&self.ops_dir)?; + let supported_operations_message = + self.create_supported_operations(&self.ops_dir, &self.config.c8y_prefix)?; let device_data_message = self.inventory_device_type_update_message()?; - let pending_operations_message = create_get_pending_operations_message()?; + let pending_operations_message = + create_get_pending_operations_message(&self.config.c8y_prefix)?; messages.append(&mut vec![ supported_operations_message, @@ -1230,14 +1245,18 @@ impl CumulocityConverter { Ok(messages) } - fn create_supported_operations(&self, path: &Path) -> Result { + fn create_supported_operations( + &self, + path: &Path, + prefix: &TopicPrefix, + ) -> Result { let topic = if is_child_operation_path(path) { let child_id = get_child_external_id(path)?; let child_external_id = Self::validate_external_id(&child_id)?; - C8yTopic::ChildSmartRestResponse(child_external_id.into()).to_topic()? + C8yTopic::ChildSmartRestResponse(child_external_id.into()).to_topic(prefix)? } else { - Topic::new_unchecked(SMARTREST_PUBLISH_TOPIC) + C8yTopic::upstream_topic(prefix) }; Ok(Message::new( @@ -1259,7 +1278,10 @@ impl CumulocityConverter { let needs_cloud_update = self.update_operations(&message.ops_dir)?; if needs_cloud_update { - Ok(Some(self.create_supported_operations(&message.ops_dir)?)) + Ok(Some(self.create_supported_operations( + &message.ops_dir, + &self.config.c8y_prefix, + )?)) } else { Ok(None) } @@ -1282,8 +1304,8 @@ fn get_child_external_id(dir_path: &Path) -> Result { } } -fn create_get_pending_operations_message() -> Result { - let topic = C8yTopic::SmartRestResponse.to_topic()?; +fn create_get_pending_operations_message(prefix: &TopicPrefix) -> Result { + let topic = C8yTopic::SmartRestResponse.to_topic(prefix)?; Ok(Message::new(&topic, request_pending_operations())) } @@ -1328,7 +1350,8 @@ impl CumulocityConverter { let need_cloud_update = self.update_operations(&ops_dir)?; if need_cloud_update { - let device_operations = self.create_supported_operations(&ops_dir)?; + let device_operations = + self.create_supported_operations(&ops_dir, &self.config.c8y_prefix)?; return Ok(vec![device_operations]); } @@ -1391,7 +1414,7 @@ impl CumulocityConverter { let topic = self .entity_store .get(target) - .and_then(C8yTopic::smartrest_response_topic) + .and_then(|entity| C8yTopic::smartrest_response_topic(entity, &self.config.c8y_prefix)) .ok_or_else(|| Error::UnknownEntity(target.to_string()))?; match command.status() { @@ -1486,7 +1509,7 @@ impl CumulocityConverter { let topic = self .entity_store .get(target) - .and_then(C8yTopic::smartrest_response_topic) + .and_then(|entity| C8yTopic::smartrest_response_topic(entity, &self.config.c8y_prefix)) .ok_or_else(|| Error::UnknownEntity(target.to_string()))?; match response.status() { @@ -1603,7 +1626,7 @@ pub(crate) mod tests { use assert_matches::assert_matches; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use c8y_api::smartrest::operations::ResultFormat; - use c8y_api::smartrest::topic::SMARTREST_PUBLISH_TOPIC; + use c8y_api::smartrest::topic::C8yTopic; use c8y_auth_proxy::url::Protocol; use c8y_auth_proxy::url::ProxyUrlGenerator; use c8y_http_proxy::handle::C8YHttpProxy; @@ -1990,7 +2013,7 @@ pub(crate) mod tests { expected_smart_rest_message_child, expected_service_create_msg, expected_smart_rest_message_service, - expected_c8y_json_message.clone() + expected_c8y_json_message.clone(), ] ); @@ -2045,7 +2068,7 @@ pub(crate) mod tests { vec![ expected_create_service_msg, expected_smart_rest_message_service, - expected_c8y_json_message.clone() + expected_c8y_json_message.clone(), ] ); @@ -2121,7 +2144,7 @@ pub(crate) mod tests { out_first_messages, vec![ expected_first_smart_rest_message, - expected_first_c8y_json_message + expected_first_c8y_json_message, ] ); @@ -2146,7 +2169,7 @@ pub(crate) mod tests { out_second_messages, vec![ expected_second_smart_rest_message, - expected_second_c8y_json_message + expected_second_c8y_json_message, ] ); } @@ -2229,7 +2252,7 @@ pub(crate) mod tests { out_messages, vec![ expected_smart_rest_message, - expected_c8y_json_message.clone() + expected_c8y_json_message.clone(), ] ); } @@ -2263,7 +2286,7 @@ pub(crate) mod tests { out_first_messages, vec![ expected_smart_rest_message, - expected_c8y_json_message.clone() + expected_c8y_json_message.clone(), ] ); } @@ -2460,8 +2483,8 @@ pub(crate) mod tests { let payload = result[0].payload_str().unwrap(); assert!(payload.starts_with( - r#"The payload {"temperature0":0,"temperature1":1,"temperature10" received on te/device/main///m/ after translation is"# - )); + r#"The payload {"temperature0":0,"temperature1":1,"temperature10" received on te/device/main///m/ after translation is"# + )); assert!(payload.ends_with("greater than the threshold size of 16184.")); } @@ -2505,8 +2528,8 @@ pub(crate) mod tests { let payload = result[0].payload_str().unwrap(); assert!(payload.starts_with( - r#"The payload {"temperature0":0,"temperature1":1,"temperature10" received on te/device/child1///m/ after translation is"# - )); + r#"The payload {"temperature0":0,"temperature1":1,"temperature10" received on te/device/child1///m/ after translation is"# + )); assert!(payload.ends_with("greater than the threshold size of 16184.")); } @@ -2532,9 +2555,9 @@ pub(crate) mod tests { let payload2 = &result[1].payload_str().unwrap(); assert!(payload1.contains("101,test-device:device:child1,child1,thin-edge.io-child")); - assert!(payload2 .contains( - r#"{"externalSource":{"externalId":"test-device:device:child1","type":"c8y_Serial"},"temperature0":{"temperature0":{"value":0.0}},"# - )); + assert!(payload2.contains( + r#"{"externalSource":{"externalId":"test-device:device:child1","type":"c8y_Serial"},"temperature0":{"temperature0":{"value":0.0}},"# + )); assert!(payload2.contains(r#""type":"ThinEdgeMeasurement""#)); } @@ -2680,7 +2703,7 @@ pub(crate) mod tests { registration, Message::new( &Topic::new_unchecked("te/device/childId//"), - r#"{"@id":"test-device:device:childId","@type":"child-device","name":"childId"}"# + r#"{"@id":"test-device:device:childId","@type":"child-device","name":"childId"}"#, ) .with_retain() ); @@ -2694,7 +2717,7 @@ pub(crate) mod tests { ChannelFilter::Command(OperationType::SoftwareUpdate), ); let mqtt_message = MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_SoftwareUpdate": [ @@ -2767,7 +2790,7 @@ pub(crate) mod tests { CumulocityConverter::validate_external_id(input_id), Err(InvalidExternalIdError { external_id: input_id.into(), - invalid_char + invalid_char, }) ); } @@ -2838,7 +2861,7 @@ pub(crate) mod tests { let output = converter.convert(&service_health_message).await; let service_creation_message = output .into_iter() - .find(|m| m.topic.name == SMARTREST_PUBLISH_TOPIC) + .find(|m| m.topic == C8yTopic::upstream_topic(&"c8y".into())) .expect("service creation message should be present"); let mut smartrest_fields = service_creation_message.payload_str().unwrap().split(','); @@ -2963,6 +2986,7 @@ pub(crate) mod tests { let tmp_dir = TempTedgeDir::new(); let mut config = c8y_converter_config(&tmp_dir); config.enable_auto_register = false; + config.c8y_prefix = "custom-c8y-prefix".into(); let (mut converter, _http_proxy) = create_c8y_converter_from_config(config); @@ -3001,16 +3025,19 @@ pub(crate) mod tests { assert_messages_matching( &messages, [ - ("c8y/s/us", "101,child1,child1,thin-edge.io-child".into()), ( - "c8y/inventory/managedObjects/update/child1", + "custom-c8y-prefix/s/us", + "101,child1,child1,thin-edge.io-child".into(), + ), + ( + "custom-c8y-prefix/inventory/managedObjects/update/child1", json!({ "foo": 5.6789 }) .into(), ), ( - "c8y/measurement/measurements/create", + "custom-c8y-prefix/measurement/measurements/create", json!({ "temperature":{ "temperature":{ @@ -3021,7 +3048,7 @@ pub(crate) mod tests { .into(), ), ( - "c8y/measurement/measurements/create", + "custom-c8y-prefix/measurement/measurements/create", json!({ "temperature":{ "temperature":{ @@ -3032,7 +3059,7 @@ pub(crate) mod tests { .into(), ), ( - "c8y/measurement/measurements/create", + "custom-c8y-prefix/measurement/measurements/create", json!({ "temperature":{ "temperature":{ @@ -3143,7 +3170,8 @@ pub(crate) mod tests { let auth_proxy_port = 8001; let auth_proxy_protocol = Protocol::Http; let topics = - C8yMapperConfig::default_internal_topic_filter(&tmp_dir.to_path_buf()).unwrap(); + C8yMapperConfig::default_internal_topic_filter(&tmp_dir.to_path_buf(), &"c8y".into()) + .unwrap(); C8yMapperConfig::new( tmp_dir.to_path_buf(), @@ -3164,8 +3192,10 @@ pub(crate) mod tests { MqttSchema::default(), true, true, + "c8y".into(), ) } + fn create_c8y_converter_from_config( config: C8yMapperConfig, ) -> ( diff --git a/crates/extensions/c8y_mapper_ext/src/inventory.rs b/crates/extensions/c8y_mapper_ext/src/inventory.rs index d59c6cee556..f05374c9b11 100644 --- a/crates/extensions/c8y_mapper_ext/src/inventory.rs +++ b/crates/extensions/c8y_mapper_ext/src/inventory.rs @@ -11,13 +11,14 @@ use std::path::Path; use tedge_api::entity_store::EntityTwinMessage; use tedge_api::mqtt_topics::Channel; use tedge_api::mqtt_topics::EntityTopicId; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::Message; use tedge_mqtt_ext::Topic; use tracing::info; use tracing::warn; const INVENTORY_FRAGMENTS_FILE_LOCATION: &str = "device/inventory.json"; -const INVENTORY_MANAGED_OBJECTS_TOPIC: &str = "c8y/inventory/managedObjects/update"; +const INVENTORY_MANAGED_OBJECTS_TOPIC: &str = "inventory/managedObjects/update"; impl CumulocityConverter { /// Creates the inventory update message with fragments from inventory.json file @@ -115,7 +116,8 @@ impl CumulocityConverter { ) -> Result { let entity_external_id = self.entity_store.try_get(source)?.external_id.as_ref(); let inventory_update_topic = Topic::new_unchecked(&format!( - "{INVENTORY_MANAGED_OBJECTS_TOPIC}/{entity_external_id}" + "{prefix}/{INVENTORY_MANAGED_OBJECTS_TOPIC}/{entity_external_id}", + prefix = self.config.c8y_prefix, )); Ok(Message::new( diff --git a/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs b/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs index bb83e13350e..55ba717443d 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs @@ -186,8 +186,8 @@ impl CumulocityConverter { Err(err) => { let smartrest_error = fail_operation( - CumulocitySupportedOperations::C8yUploadConfigFile, - &format!("tedge-mapper-c8y failed to download configuration snapshot from file-transfer service: {err}"), + CumulocitySupportedOperations::C8yUploadConfigFile, + &format!("tedge-mapper-c8y failed to download configuration snapshot from file-transfer service: {err}"), ); let c8y_notification = Message::new(&smartrest_topic, smartrest_error); @@ -288,7 +288,7 @@ mod tests { // Simulate c8y_UploadConfigFile operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_UploadConfigFile": { @@ -338,7 +338,7 @@ mod tests { // Simulate c8y_UploadConfigFile operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_UploadConfigFile": { @@ -537,7 +537,7 @@ mod tests { &mut mqtt, [("c8y/s/us", "503,c8y_UploadConfigFile,https://test.c8y.io/event/events/dummy-event-id-1234/binaries")], ) - .await; + .await; } #[tokio::test] @@ -616,6 +616,6 @@ mod tests { "503,c8y_UploadConfigFile,https://test.c8y.io/event/events/dummy-event-id-1234/binaries", )], ) - .await; + .await; } } diff --git a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs index 33086eca750..5d591b839d5 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs @@ -184,7 +184,7 @@ mod tests { // Simulate c8y_DownloadConfigFile operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_DownloadConfigFile": { @@ -235,7 +235,7 @@ mod tests { // Simulate c8y_DownloadConfigFile operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_DownloadConfigFile": { diff --git a/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs b/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs index f8c7cca1671..c6db8f9ce00 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs @@ -172,6 +172,7 @@ mod tests { use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::Topic; use tedge_test_utils::fs::TempTedgeDir; + const TEST_TIMEOUT_MS: Duration = Duration::from_millis(5000); #[tokio::test] @@ -262,7 +263,7 @@ mod tests { // Simulate c8y_Firmware operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_Firmware": { @@ -315,7 +316,7 @@ mod tests { // Simulate c8y_Firmware operation delivered via JSON over MQTT mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_Firmware": { @@ -503,7 +504,7 @@ mod tests { // Simulate log_upload command with "successful" state mqtt.send(MqttMessage::new( - &Topic::new_unchecked("te/device/child1///cmd/firmware_update/c8y-mapper-1234"), + &Topic::new_unchecked("te/device/child1///cmd/firmware_update/c8y-mapper-1234"), json!({ "status": "successful", "name": "myFirmware", diff --git a/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs b/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs index 88a082c33eb..2edb9ffd053 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs @@ -306,7 +306,7 @@ mod tests { // Simulate c8y_LogfileRequest JSON over MQTT request mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_LogfileRequest": { @@ -363,7 +363,7 @@ mod tests { // Simulate c8y_LogfileRequest JSON over MQTT request mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_LogfileRequest": { diff --git a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs index 53b2ef6b517..66a1cdd19b5 100644 --- a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs +++ b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use serde::Serialize; use tedge_api::entity_store::EntityMetadata; use tedge_api::entity_store::EntityType; +use tedge_config::TopicPrefix; use tedge_mqtt_ext::Message; use tracing::error; @@ -20,6 +21,7 @@ pub fn convert_health_status_message( entity: &EntityMetadata, ancestors_external_ids: &[String], message: &Message, + prefix: &TopicPrefix, ) -> Vec { // TODO: introduce type to remove entity type guards if entity.r#type != EntityType::Service { @@ -56,10 +58,10 @@ pub fn convert_health_status_message( let Ok(status_message) = // smartrest::inventory::service_status_update_message(&external_ids, &health_status); - smartrest::inventory::service_creation_message(entity.external_id.as_ref(), display_name, display_type, &health_status, ancestors_external_ids) else { - error!("Can't create 102 for service status update"); - return vec![]; - }; + smartrest::inventory::service_creation_message(entity.external_id.as_ref(), display_name, display_type, &health_status, ancestors_external_ids, prefix) else { + error!("Can't create 102 for service status update"); + return vec![]; + }; vec![status_message] } @@ -72,61 +74,62 @@ mod tests { use tedge_api::mqtt_topics::MqttSchema; use tedge_mqtt_ext::Topic; use test_case::test_case; + #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"pid":"1234","status":"up"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; - "service-monitoring-thin-edge-device" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"pid":"1234","status":"up"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; + "service-monitoring-thin-edge-device" )] #[test_case( - "test_device", - "te/device/child/service/tedge-mapper-c8y/status/health", - r#"{"pid":"1234","status":"up"}"#, - "c8y/s/us/test_device:device:child", - r#"102,test_device:device:child:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; - "service-monitoring-thin-edge-child-device" + "test_device", + "te/device/child/service/tedge-mapper-c8y/status/health", + r#"{"pid":"1234","status":"up"}"#, + "c8y/s/us/test_device:device:child", + r#"102,test_device:device:child:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; + "service-monitoring-thin-edge-child-device" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"pid":"123456"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-thin-edge-no-status" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"pid":"123456"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-thin-edge-no-status" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":""}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-empty-status" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":""}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-empty-status" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - "{}", - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-empty-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + "{}", + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-empty-health-message" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":"up,down"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up,down""#; - "service-monitoring-type-with-comma-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":"up,down"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up,down""#; + "service-monitoring-type-with-comma-health-message" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":"up\"down"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up""down""#; - "service-monitoring-double-quotes-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":"up\"down"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up""down""#; + "service-monitoring-double-quotes-health-message" )] fn translate_health_status_to_c8y_service_monitoring_message( device_name: &str, @@ -179,7 +182,7 @@ mod tests { .ancestors_external_ids(&entity_topic_id) .unwrap(); - let msg = convert_health_status_message(entity, &ancestors_external_ids, &health_message); + let msg = convert_health_status_message(entity, &ancestors_external_ids, &health_message, &"c8y".into()); assert_eq!(msg[0], expected_message); } } diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index e04960fe595..a7fa5a25563 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -293,7 +293,7 @@ async fn mapper_publishes_software_update_request() { // Simulate c8y_SoftwareUpdate JSON over MQTT request mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_SoftwareUpdate": [ @@ -452,7 +452,7 @@ async fn mapper_publishes_software_update_request_with_wrong_action() { // Publish a c8y_SoftwareUpdate via JSON over MQTT that contains a wrong action `remove`, that is not known by c8y. mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_SoftwareUpdate": [ @@ -486,7 +486,7 @@ async fn mapper_publishes_software_update_request_with_wrong_action() { ) ], ) - .await; + .await; } #[tokio::test] @@ -1314,7 +1314,7 @@ async fn mapper_handles_multiple_modules_in_update_list_sm_requests() { // Publish multiple modules software update via JSON over MQTT. mqtt.send(MqttMessage::new( - &C8yDeviceControlTopic::topic(), + &C8yDeviceControlTopic::topic(&"c8y".into()), json!({ "id": "123456", "c8y_SoftwareUpdate": [ @@ -1762,7 +1762,7 @@ async fn custom_operation_without_timeout_successful() { // Simulate c8y_Command SmartREST request mqtt.send(MqttMessage::new( - &C8yTopic::downstream_topic(), + &C8yTopic::downstream_topic(&"c8y".into()), "511,test-device,c8y_Command", )) .await @@ -1824,7 +1824,7 @@ async fn custom_operation_with_timeout_successful() { // Simulate c8y_Command SmartREST request mqtt.send(MqttMessage::new( - &C8yTopic::downstream_topic(), + &C8yTopic::downstream_topic(&"c8y".into()), "511,test-device,c8y_Command", )) .await @@ -1885,7 +1885,7 @@ async fn custom_operation_timeout_sigterm() { // Simulate c8y_Command SmartREST request mqtt.send(MqttMessage::new( - &C8yTopic::downstream_topic(), + &C8yTopic::downstream_topic(&"c8y".into()), "511,test-device,c8y_Command", )) .await @@ -1950,7 +1950,7 @@ async fn custom_operation_timeout_sigkill() { // Simulate c8y_Command SmartREST request mqtt.send(MqttMessage::new( - &C8yTopic::downstream_topic(), + &C8yTopic::downstream_topic(&"c8y".into()), "511,test-device,c8y_Command", )) .await @@ -2074,8 +2074,8 @@ async fn c8y_mapper_nested_child_alarm_mapping_to_smartrest() { &Topic::new_unchecked("te/device/nested_child///a/"), json!({ "severity": "minor", "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), )) - .await - .unwrap(); + .await + .unwrap(); // Expect nested child device creating an minor alarm assert_received_contains_str( @@ -2213,8 +2213,8 @@ async fn c8y_mapper_nested_child_service_alarm_mapping_to_smartrest() { &Topic::new_unchecked("te/device/nested_child/service/nested_service/a/"), json!({ "severity": "minor", "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), )) - .await - .unwrap(); + .await + .unwrap(); mqtt.skip(3).await; @@ -2308,6 +2308,7 @@ fn assert_command_exec_log_content(cfg_dir: TempTedgeDir, expected_contents: &st assert!(contents.contains(expected_contents)); } } + fn create_custom_op_file( cfg_dir: &TempTedgeDir, cmd_file: &Path, @@ -2386,7 +2387,8 @@ pub(crate) async fn spawn_c8y_mapper_actor( let mqtt_schema = MqttSchema::default(); let auth_proxy_addr = "127.0.0.1".into(); let auth_proxy_port = 8001; - let mut topics = C8yMapperConfig::default_internal_topic_filter(config_dir.path()).unwrap(); + let mut topics = + C8yMapperConfig::default_internal_topic_filter(config_dir.path(), &"c8y".into()).unwrap(); topics.add_all(crate::operations::log_upload::log_upload_topic_filter( &mqtt_schema, )); @@ -2415,6 +2417,7 @@ pub(crate) async fn spawn_c8y_mapper_actor( MqttSchema::default(), true, true, + "c8y".into(), ); let mut mqtt_builder: SimpleMessageBoxBuilder = diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index e6722529fa9..38c7615bd04 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -46,11 +46,10 @@ impl MqttBridgeActorBuilder { tedge_config.device.key_path.clone().into(), tedge_config.device.cert_path.clone().into(), ) - .unwrap(); + .unwrap(); let (signal_sender, _signal_receiver) = mpsc::channel(10); - // TODO move this somewhere sensible, and make sure we validate it - let prefix = std::env::var("TEDGE_BRIDGE_PREFIX").unwrap_or_else(|_| "c8y".to_owned()); + let prefix = tedge_config.c8y.bridge.topic_prefix.clone(); let mut local_config = MqttOptions::new( format!("tedge-mapper-bridge-{prefix}"), &tedge_config.mqtt.client.host, @@ -123,10 +122,12 @@ async fn one_way_bridge Fn(&'a str) -> Cow<'a, str>>( let mut last_err = None; loop { let notification = match recv_event_loop.poll().await { + // TODO notify if this is us recovering from an error Ok(notification) => notification, Err(err) => { let err = err.to_string(); if last_err.as_ref() != Some(&err) { + // TODO clarify whether this is cloud/local error!("MQTT bridge connection error: {err}"); last_err = Some(err); } diff --git a/plugins/c8y_configuration_plugin/src/lib.rs b/plugins/c8y_configuration_plugin/src/lib.rs index 78513b10a9b..d8d35dee0b0 100644 --- a/plugins/c8y_configuration_plugin/src/lib.rs +++ b/plugins/c8y_configuration_plugin/src/lib.rs @@ -91,7 +91,8 @@ async fn run_with( // Create actor instances let mqtt_config = mqtt_config(&tedge_config)?; - let mut jwt_actor = C8YJwtRetriever::builder(mqtt_config.clone()); + let mut jwt_actor = + C8YJwtRetriever::builder(mqtt_config.clone(), tedge_config.c8y.bridge.topic_prefix.clone()); let mut http_actor = HttpActor::new().builder(); let c8y_http_config = (&tedge_config).try_into()?; let mut c8y_http_proxy_actor = diff --git a/plugins/c8y_firmware_plugin/src/lib.rs b/plugins/c8y_firmware_plugin/src/lib.rs index e0be16ffe50..ddc50569e26 100644 --- a/plugins/c8y_firmware_plugin/src/lib.rs +++ b/plugins/c8y_firmware_plugin/src/lib.rs @@ -83,7 +83,10 @@ async fn run_with(tedge_config: TEdgeConfig) -> Result<(), anyhow::Error> { // Create actor instances let mqtt_config = tedge_config.mqtt_config()?; - let mut jwt_actor = C8YJwtRetriever::builder(mqtt_config.clone()); + let mut jwt_actor = C8YJwtRetriever::builder( + mqtt_config.clone(), + tedge_config.c8y.bridge.topic_prefix.clone(), + ); let mut timer_actor = TimerActor::builder(); let identity = tedge_config.http.client.auth.identity()?; let mut downloader_actor = DownloaderActor::new(identity).builder(); diff --git a/plugins/c8y_log_plugin/src/main.rs b/plugins/c8y_log_plugin/src/main.rs index d3313f1ab2a..5f911b657ff 100644 --- a/plugins/c8y_log_plugin/src/main.rs +++ b/plugins/c8y_log_plugin/src/main.rs @@ -128,7 +128,10 @@ async fn run(config_dir: impl AsRef, tedge_config: TEdgeConfig) -> Result< &tedge_config.service, ); - let mut jwt_actor = C8YJwtRetriever::builder(base_mqtt_config); + let mut jwt_actor = C8YJwtRetriever::builder( + base_mqtt_config, + tedge_config.c8y.bridge.topic_prefix.clone(), + ); let mut http_actor = HttpActor::new().builder(); let mut c8y_http_proxy_actor = C8YHttpProxyBuilder::new(c8y_http_config, &mut http_actor, &mut jwt_actor); From 556a661f8bd3d447ca3130e503cf9bb301cf55bc Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 7 Mar 2024 10:06:59 +0000 Subject: [PATCH 06/20] Add configuration to enable mapper bridge --- Cargo.lock | 1 + .../src/tedge_config_cli/tedge_config.rs | 16 +++ crates/core/c8y_api/src/utils.rs | 27 ++-- crates/core/tedge/src/bridge/aws.rs | 3 + crates/core/tedge/src/bridge/azure.rs | 3 + crates/core/tedge/src/bridge/c8y.rs | 4 + crates/core/tedge/src/bridge/config.rs | 7 + crates/core/tedge/src/bridge/mod.rs | 1 + .../tedge/src/cli/config/commands/list.rs | 3 +- crates/core/tedge/src/cli/connect/command.rs | 10 ++ crates/core/tedge_mapper/src/c8y/mapper.rs | 53 ++++---- .../c8y_firmware_manager/src/tests.rs | 10 +- crates/extensions/c8y_mapper_ext/src/actor.rs | 12 +- .../extensions/c8y_mapper_ext/src/config.rs | 15 +++ .../c8y_mapper_ext/src/inventory.rs | 1 - .../c8y_mapper_ext/src/service_monitor.rs | 7 +- crates/extensions/c8y_mapper_ext/src/tests.rs | 4 +- .../extensions/tedge_mqtt_bridge/Cargo.toml | 1 + .../extensions/tedge_mqtt_bridge/src/lib.rs | 121 ++++++++++++++---- 19 files changed, 231 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 452a9c47927..f4ff4364312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3873,6 +3873,7 @@ version = "1.0.1" dependencies = [ "assert-json-diff", "async-trait", + "c8y_api", "certificate", "futures", "mqtt_channel", diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index c91557e29f6..dfa114dcb03 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -453,7 +453,14 @@ define_tedge_config! { local_cleansession: AutoFlag, }, + #[tedge_config(default(value = false))] + #[doku(skip)] + in_mapper: bool, + // TODO validation + /// The topic prefix that will be used for the mapper bridge MQTT topic. For instance, + /// if this is set to "c8y", then messages published to `c8y/s/us` will be + /// forwarded by to Cumulocity on the `s/us` topic #[tedge_config(example = "c8y", default(value = "c8y"))] topic_prefix: TopicPrefix, }, @@ -809,6 +816,15 @@ define_tedge_config! { } +impl ReadableKey { + pub fn is_printable_value(self, value: &str) -> bool { + match self { + Self::C8yBridgeInMapper => value != "false", + _ => true, + } + } +} + // TODO doc comment #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, serde::Serialize)] #[serde(from = "String", into = "Arc")] diff --git a/crates/core/c8y_api/src/utils.rs b/crates/core/c8y_api/src/utils.rs index 9be74a4c89f..20e56f7e5da 100644 --- a/crates/core/c8y_api/src/utils.rs +++ b/crates/core/c8y_api/src/utils.rs @@ -2,24 +2,28 @@ pub mod bridge { use mqtt_channel::Message; // FIXME: doesn't account for custom topic root, use MQTT scheme API here - pub const C8Y_BRIDGE_HEALTH_TOPIC: &str = - "te/device/main/service/mosquitto-c8y-bridge/status/health"; pub const C8Y_BRIDGE_UP_PAYLOAD: &str = "1"; - const C8Y_BRIDGE_DOWN_PAYLOAD: &str = "0"; + pub const C8Y_BRIDGE_DOWN_PAYLOAD: &str = "0"; - pub fn is_c8y_bridge_up(message: &Message) -> bool { + pub fn main_device_health_topic(service: &str) -> String { + format!("te/device/main/service/{service}/status/health") + } + + pub fn is_c8y_bridge_up(message: &Message, service: &str) -> bool { + let c8y_bridge_health_topic = main_device_health_topic(service); match message.payload_str() { Ok(payload) => { - message.topic.name == C8Y_BRIDGE_HEALTH_TOPIC && payload == C8Y_BRIDGE_UP_PAYLOAD + message.topic.name == c8y_bridge_health_topic && payload == C8Y_BRIDGE_UP_PAYLOAD } Err(_err) => false, } } - pub fn is_c8y_bridge_established(message: &Message) -> bool { + pub fn is_c8y_bridge_established(message: &Message, service: &str) -> bool { + let c8y_bridge_health_topic = main_device_health_topic(service); match message.payload_str() { Ok(payload) => { - message.topic.name == C8Y_BRIDGE_HEALTH_TOPIC + message.topic.name == c8y_bridge_health_topic && (payload == C8Y_BRIDGE_UP_PAYLOAD || payload == C8Y_BRIDGE_DOWN_PAYLOAD) } Err(_err) => false, @@ -46,9 +50,10 @@ mod tests { use mqtt_channel::Topic; use test_case::test_case; - use crate::utils::bridge::is_c8y_bridge_established; use crate::utils::bridge::is_c8y_bridge_up; - use crate::utils::bridge::C8Y_BRIDGE_HEALTH_TOPIC; + + const C8Y_BRIDGE_HEALTH_TOPIC: &str = + "te/device/main/service/tedge-mapper-bridge-c8y/status/health"; #[test_case(C8Y_BRIDGE_HEALTH_TOPIC, "1", true)] #[test_case(C8Y_BRIDGE_HEALTH_TOPIC, "0", false)] @@ -58,7 +63,7 @@ mod tests { let topic = Topic::new(topic).unwrap(); let message = Message::new(&topic, payload); - let actual = is_c8y_bridge_up(&message); + let actual = is_c8y_bridge_up(&message, "tedge-mapper-bridge-c8y"); assert_eq!(actual, expected); } @@ -71,7 +76,7 @@ mod tests { let topic = Topic::new(topic).unwrap(); let message = Message::new(&topic, payload); - let actual = is_c8y_bridge_established(&message); + let actual = is_c8y_bridge_up(&message, "tedge-mapper-bridge-c8y"); assert_eq!(actual, expected); } } diff --git a/crates/core/tedge/src/bridge/aws.rs b/crates/core/tedge/src/bridge/aws.rs index 84723883b48..e59f43a8ee1 100644 --- a/crates/core/tedge/src/bridge/aws.rs +++ b/crates/core/tedge/src/bridge/aws.rs @@ -1,4 +1,5 @@ use super::BridgeConfig; +use crate::bridge::config::BridgeLocation; use camino::Utf8PathBuf; use tedge_config::ConnectUrl; @@ -74,6 +75,8 @@ impl From for BridgeConfig { connection_check_pub_msg_topic, connection_check_sub_msg_topic, ], + // TODO support configurability + bridge_location: BridgeLocation::Mosquitto, } } } diff --git a/crates/core/tedge/src/bridge/azure.rs b/crates/core/tedge/src/bridge/azure.rs index a842fbe5d44..1da6f7fe868 100644 --- a/crates/core/tedge/src/bridge/azure.rs +++ b/crates/core/tedge/src/bridge/azure.rs @@ -1,4 +1,5 @@ use super::BridgeConfig; +use crate::bridge::config::BridgeLocation; use camino::Utf8PathBuf; use tedge_config::ConnectUrl; @@ -72,6 +73,8 @@ impl From for BridgeConfig { r##"twin/GET/# out 1 az/ $iothub/"##.into(), r##"twin/PATCH/# out 1 az/ $iothub/"##.into(), ], + // TODO support configurability + bridge_location: BridgeLocation::Mosquitto, } } } diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 59aab8f018d..13d95e21fe7 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -1,4 +1,5 @@ use super::BridgeConfig; +use crate::bridge::config::BridgeLocation; use camino::Utf8PathBuf; use std::process::Command; use tedge_config::AutoFlag; @@ -19,6 +20,7 @@ pub struct BridgeConfigC8yParams { pub bridge_keyfile: Utf8PathBuf, pub smartrest_templates: TemplatesSet, pub include_local_clean_session: AutoFlag, + pub bridge_location: BridgeLocation, } impl From for BridgeConfig { @@ -32,6 +34,7 @@ impl From for BridgeConfig { bridge_keyfile, smartrest_templates, include_local_clean_session, + bridge_location, } = params; let mut topics: Vec = vec![ @@ -115,6 +118,7 @@ impl From for BridgeConfig { notification_topic: C8Y_BRIDGE_HEALTH_TOPIC.into(), bridge_attempt_unsubscribe: false, topics, + bridge_location, } } } diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index d6318bae887..d558e58b3fa 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -20,6 +20,7 @@ pub struct BridgeConfig { pub local_clientid: String, pub bridge_certfile: Utf8PathBuf, pub bridge_keyfile: Utf8PathBuf, + pub bridge_location: BridgeLocation, pub use_mapper: bool, pub use_agent: bool, pub try_private: bool, @@ -34,6 +35,12 @@ pub struct BridgeConfig { pub topics: Vec, } +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum BridgeLocation { + Mosquitto, + Mapper, +} + impl BridgeConfig { pub fn serialize(&self, writer: &mut W) -> std::io::Result<()> { writeln!(writer, "### Bridge")?; diff --git a/crates/core/tedge/src/bridge/mod.rs b/crates/core/tedge/src/bridge/mod.rs index 186be8159f3..4c3ddc40b7c 100644 --- a/crates/core/tedge/src/bridge/mod.rs +++ b/crates/core/tedge/src/bridge/mod.rs @@ -9,6 +9,7 @@ pub mod c8y; pub use common_mosquitto_config::*; pub use config::BridgeConfig; +pub use config::BridgeLocation; pub const C8Y_CONFIG_FILENAME: &str = "c8y-bridge.conf"; pub const AZURE_CONFIG_FILENAME: &str = "az-bridge.conf"; diff --git a/crates/core/tedge/src/cli/config/commands/list.rs b/crates/core/tedge/src/cli/config/commands/list.rs index 2b38e50eeb5..255a491ad5e 100644 --- a/crates/core/tedge/src/cli/config/commands/list.rs +++ b/crates/core/tedge/src/cli/config/commands/list.rs @@ -33,9 +33,10 @@ fn print_config_list(config: &TEdgeConfig, all: bool) -> Result<(), ConfigError> let mut keys_without_values = Vec::new(); for config_key in ReadableKey::iter() { match config.read_string(config_key).ok() { - Some(value) => { + Some(value) if config_key.is_printable_value(&value) => { println!("{}={}", config_key, value); } + Some(_) => {} None => { keys_without_values.push(config_key); } diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 2ac0319adce..f79e0e78a91 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -2,6 +2,7 @@ use crate::bridge::aws::BridgeConfigAwsParams; use crate::bridge::azure::BridgeConfigAzureParams; use crate::bridge::c8y::BridgeConfigC8yParams; use crate::bridge::BridgeConfig; +use crate::bridge::BridgeLocation; use crate::bridge::CommonMosquittoConfig; use crate::cli::common::Cloud; use crate::cli::connect::jwt_token::*; @@ -196,6 +197,10 @@ pub fn bridge_config( Ok(BridgeConfig::from(params)) } Cloud::C8y => { + let bridge_location = match config.c8y.bridge.in_mapper { + true => BridgeLocation::Mapper, + false => BridgeLocation::Mosquitto, + }; let params = BridgeConfigC8yParams { mqtt_host: config.c8y.mqtt.or_config_not_set()?.clone(), config_file: C8Y_CONFIG_FILENAME.into(), @@ -205,6 +210,7 @@ pub fn bridge_config( bridge_keyfile: config.device.key_path.clone(), smartrest_templates: config.c8y.smartrest.templates.clone(), include_local_clean_session: config.c8y.bridge.include.local_cleansession.clone(), + bridge_location, }; Ok(BridgeConfig::from(params)) @@ -434,6 +440,10 @@ fn new_bridge( config_location: &TEdgeConfigLocation, device_type: &str, ) -> Result<(), ConnectError> { + if bridge_config.bridge_location == BridgeLocation::Mapper { + clean_up(config_location, bridge_config)?; + return Ok(()); + } println!("Checking if {} is available.\n", service_manager.name()); let service_manager_result = service_manager.check_operational(); diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 7b42520ee55..6783bfff0fc 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -37,27 +37,36 @@ impl TEdgeComponent for CumulocityMapper { start_basic_actors(self.session_name(), &tedge_config).await?; let mqtt_config = tedge_config.mqtt_config()?; - let custom_topics = tedge_config - .c8y - .smartrest - .templates - .0 - .iter() - .map(|id| format!("s/dc/{id}")); - let smartrest_topics: Vec = [ - "s/dt", - "s/dat", - "s/ds", - "s/e", - "s/dc/#", - "devicecontrol/notifications", - "error", - ] - .into_iter() - .map(<_>::to_owned) - .chain(custom_topics) - .collect(); - let bridge_actor = MqttBridgeActorBuilder::new(&tedge_config, &smartrest_topics).await; + let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config)?; + if tedge_config.c8y.bridge.in_mapper { + let custom_topics = tedge_config + .c8y + .smartrest + .templates + .0 + .iter() + .map(|id| format!("s/dc/{id}")); + let smartrest_topics: Vec = [ + "s/dt", + "s/dat", + "s/ds", + "s/e", + "s/dc/#", + "devicecontrol/notifications", + "error", + ] + .into_iter() + .map(<_>::to_owned) + .chain(custom_topics) + .collect(); + let bridge_actor = MqttBridgeActorBuilder::new( + &tedge_config, + c8y_mapper_config.bridge_service_name(), + &smartrest_topics, + ) + .await; + runtime.spawn(bridge_actor).await?; + } let mut jwt_actor = C8YJwtRetriever::builder( mqtt_config.clone(), tedge_config.c8y.bridge.topic_prefix.clone(), @@ -83,7 +92,6 @@ impl TEdgeComponent for CumulocityMapper { let mut service_monitor_actor = MqttActorBuilder::new(service_monitor_client_config(&tedge_config)?); - let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config)?; let c8y_mapper_actor = C8yMapperBuilder::try_new( c8y_mapper_config, &mut mqtt_actor, @@ -111,7 +119,6 @@ impl TEdgeComponent for CumulocityMapper { runtime.spawn(uploader_actor).await?; runtime.spawn(downloader_actor).await?; runtime.spawn(old_to_new_agent_adapter).await?; - runtime.spawn(bridge_actor).await?; runtime.run_to_completion().await?; Ok(()) diff --git a/crates/extensions/c8y_firmware_manager/src/tests.rs b/crates/extensions/c8y_firmware_manager/src/tests.rs index d8ab5727e72..f303d882cf9 100644 --- a/crates/extensions/c8y_firmware_manager/src/tests.rs +++ b/crates/extensions/c8y_firmware_manager/src/tests.rs @@ -603,7 +603,10 @@ async fn required_init_state_recreated_on_startup() -> Result<(), DynError> { // Assert that the startup succeeds and the plugin requests for pending operations mqtt_message_box - .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(&"c8y".into()), "500")]) + .assert_received([MqttMessage::new( + &C8yTopic::upstream_topic(&"c8y".into()), + "500", + )]) .await; for dir in ["cache", "firmware", "file-transfer"] { @@ -630,7 +633,10 @@ async fn required_init_state_recreated_on_startup() -> Result<(), DynError> { // Assert that the startup succeeds again and the mapper requests for pending operations mqtt_message_box - .assert_received([MqttMessage::new(&C8yTopic::upstream_topic(&"c8y".into()), "500")]) + .assert_received([MqttMessage::new( + &C8yTopic::upstream_topic(&"c8y".into()), + "500", + )]) .await; // Assert that all the required directories are recreated diff --git a/crates/extensions/c8y_mapper_ext/src/actor.rs b/crates/extensions/c8y_mapper_ext/src/actor.rs index a851c69f72f..ee60d278938 100644 --- a/crates/extensions/c8y_mapper_ext/src/actor.rs +++ b/crates/extensions/c8y_mapper_ext/src/actor.rs @@ -8,7 +8,7 @@ use c8y_api::smartrest::smartrest_serializer::succeed_static_operation; use c8y_api::smartrest::smartrest_serializer::CumulocitySupportedOperations; use c8y_api::smartrest::smartrest_serializer::SmartRest; use c8y_api::utils::bridge::is_c8y_bridge_established; -use c8y_api::utils::bridge::C8Y_BRIDGE_HEALTH_TOPIC; +use c8y_api::utils::bridge::main_device_health_topic; use c8y_auth_proxy::url::ProxyUrlGenerator; use c8y_http_proxy::handle::C8YHttpProxy; use c8y_http_proxy::messages::C8YRestRequest; @@ -68,6 +68,7 @@ pub struct C8yMapperActor { mqtt_publisher: LoggingSender, timer_sender: LoggingSender, bridge_status_messages: SimpleMessageBox, + c8y_bridge_service_name: String, } #[async_trait] @@ -79,7 +80,7 @@ impl Actor for C8yMapperActor { async fn run(mut self) -> Result<(), RuntimeError> { // Wait till the c8y bridge is established while let Some(message) = self.bridge_status_messages.recv().await { - if is_c8y_bridge_established(&message) { + if is_c8y_bridge_established(&message, &self.c8y_bridge_service_name) { break; } } @@ -124,6 +125,7 @@ impl C8yMapperActor { mqtt_publisher: LoggingSender, timer_sender: LoggingSender, bridge_status_messages: SimpleMessageBox, + c8y_bridge_service_name: String, ) -> Self { Self { converter, @@ -131,6 +133,7 @@ impl C8yMapperActor { mqtt_publisher, timer_sender, bridge_status_messages, + c8y_bridge_service_name, } } @@ -322,8 +325,9 @@ impl C8yMapperBuilder { let bridge_monitor_builder: SimpleMessageBoxBuilder = SimpleMessageBoxBuilder::new("ServiceMonitor", 1); + let bridge_health_topic = main_device_health_topic(&config.bridge_service_name()); service_monitor.connect_consumer( - C8Y_BRIDGE_HEALTH_TOPIC.try_into().unwrap(), + bridge_health_topic.as_str().try_into().unwrap(), bridge_monitor_builder.get_sender(), ); @@ -367,6 +371,7 @@ impl Builder for C8yMapperBuilder { LoggingSender::new("C8yMapper => Uploader".into(), self.upload_sender); let downloader_sender = LoggingSender::new("C8yMapper => Downloader".into(), self.download_sender); + let c8y_bridge_service_name = self.config.bridge_service_name(); let converter = CumulocityConverter::new( self.config, @@ -387,6 +392,7 @@ impl Builder for C8yMapperBuilder { mqtt_publisher, timer_sender, bridge_monitor_box, + c8y_bridge_service_name, )) } } diff --git a/crates/extensions/c8y_mapper_ext/src/config.rs b/crates/extensions/c8y_mapper_ext/src/config.rs index 410f5bd0093..d1b8a8edb06 100644 --- a/crates/extensions/c8y_mapper_ext/src/config.rs +++ b/crates/extensions/c8y_mapper_ext/src/config.rs @@ -53,6 +53,7 @@ pub struct C8yMapperConfig { pub enable_auto_register: bool, pub clean_start: bool, pub c8y_prefix: TopicPrefix, + pub bridge_in_mapper: bool, } impl C8yMapperConfig { @@ -77,6 +78,7 @@ impl C8yMapperConfig { enable_auto_register: bool, clean_start: bool, c8y_prefix: TopicPrefix, + bridge_in_mapper: bool, ) -> Self { let ops_dir = config_dir .join(SUPPORTED_OPERATIONS_DIRECTORY) @@ -105,6 +107,16 @@ impl C8yMapperConfig { enable_auto_register, clean_start, c8y_prefix, + bridge_in_mapper, + } + } + + // TODO don't allocate string + pub fn bridge_service_name(&self) -> String { + if self.bridge_in_mapper { + format!("tedge-mapper-bridge-{}", self.c8y_prefix) + } else { + "mosquitto-c8y-bridge".into() } } @@ -185,6 +197,8 @@ impl C8yMapperConfig { } } + let bridge_in_mapper = tedge_config.c8y.bridge.in_mapper; + Ok(C8yMapperConfig::new( config_dir, logs_path, @@ -205,6 +219,7 @@ impl C8yMapperConfig { enable_auto_register, clean_start, c8y_prefix, + bridge_in_mapper, )) } diff --git a/crates/extensions/c8y_mapper_ext/src/inventory.rs b/crates/extensions/c8y_mapper_ext/src/inventory.rs index f05374c9b11..7a0e6735838 100644 --- a/crates/extensions/c8y_mapper_ext/src/inventory.rs +++ b/crates/extensions/c8y_mapper_ext/src/inventory.rs @@ -11,7 +11,6 @@ use std::path::Path; use tedge_api::entity_store::EntityTwinMessage; use tedge_api::mqtt_topics::Channel; use tedge_api::mqtt_topics::EntityTopicId; -use tedge_config::TopicPrefix; use tedge_mqtt_ext::Message; use tedge_mqtt_ext::Topic; use tracing::info; diff --git a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs index 66a1cdd19b5..ba10ab5d3d2 100644 --- a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs +++ b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs @@ -182,7 +182,12 @@ mod tests { .ancestors_external_ids(&entity_topic_id) .unwrap(); - let msg = convert_health_status_message(entity, &ancestors_external_ids, &health_message, &"c8y".into()); + let msg = convert_health_status_message( + entity, + &ancestors_external_ids, + &health_message, + &"c8y".into(), + ); assert_eq!(msg[0], expected_message); } } diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index dc7d3991160..8be392cf7f3 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -10,7 +10,7 @@ use crate::Capabilities; use assert_json_diff::assert_json_include; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use c8y_api::smartrest::topic::C8yTopic; -use c8y_api::utils::bridge::C8Y_BRIDGE_HEALTH_TOPIC; +use c8y_api::utils::bridge::main_device_health_topic; use c8y_api::utils::bridge::C8Y_BRIDGE_UP_PAYLOAD; use c8y_auth_proxy::url::Protocol; use c8y_http_proxy::messages::C8YRestRequest; @@ -2454,7 +2454,7 @@ pub(crate) async fn spawn_c8y_mapper_actor( let mut service_monitor_box = service_monitor_builder.build(); let bridge_status_msg = MqttMessage::new( - &Topic::new_unchecked(C8Y_BRIDGE_HEALTH_TOPIC), + &Topic::new_unchecked(&main_device_health_topic("tedge-mapper-bridge-c8y")), C8Y_BRIDGE_UP_PAYLOAD, ); service_monitor_box.send(bridge_status_msg).await.unwrap(); diff --git a/crates/extensions/tedge_mqtt_bridge/Cargo.toml b/crates/extensions/tedge_mqtt_bridge/Cargo.toml index d88b68b85be..3a00e000956 100644 --- a/crates/extensions/tedge_mqtt_bridge/Cargo.toml +++ b/crates/extensions/tedge_mqtt_bridge/Cargo.toml @@ -18,6 +18,7 @@ test-helpers = ["dep:assert-json-diff"] assert-json-diff = { workspace = true, optional = true } async-trait = { workspace = true } certificate = { workspace = true } +c8y_api = { workspace = true } futures = { workspace = true } mqtt_channel = { workspace = true } rumqttc = { workspace = true } diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 38c7615bd04..88122ceb325 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -1,4 +1,7 @@ use async_trait::async_trait; +use c8y_api::utils::bridge::main_device_health_topic; +use c8y_api::utils::bridge::C8Y_BRIDGE_DOWN_PAYLOAD; +use c8y_api::utils::bridge::C8Y_BRIDGE_UP_PAYLOAD; use certificate::parse_root_certificate::create_tls_config; use futures::SinkExt; use futures::StreamExt; @@ -6,6 +9,7 @@ use rumqttc::AsyncClient; use rumqttc::Event; use rumqttc::EventLoop; use rumqttc::Incoming; +use rumqttc::LastWill; use rumqttc::MqttOptions; use rumqttc::Outgoing; use rumqttc::PubAck; @@ -23,6 +27,7 @@ use tedge_actors::RuntimeError; use tedge_actors::RuntimeRequest; use tedge_actors::RuntimeRequestSink; use tracing::error; +use tracing::log::info; pub type MqttConfig = mqtt_channel::Config; pub type MqttMessage = Message; @@ -40,7 +45,11 @@ pub struct MqttBridgeActorBuilder { } impl MqttBridgeActorBuilder { - pub async fn new(tedge_config: &TEdgeConfig, cloud_topics: &[impl AsRef]) -> Self { + pub async fn new( + tedge_config: &TEdgeConfig, + service_name: String, + cloud_topics: &[impl AsRef], + ) -> Self { let tls_config = create_tls_config( tedge_config.c8y.root_cert_path.clone().into(), tedge_config.device.key_path.clone().into(), @@ -51,12 +60,20 @@ impl MqttBridgeActorBuilder { let prefix = tedge_config.c8y.bridge.topic_prefix.clone(); let mut local_config = MqttOptions::new( - format!("tedge-mapper-bridge-{prefix}"), + &service_name, &tedge_config.mqtt.client.host, tedge_config.mqtt.client.port.into(), ); + let health_topic = main_device_health_topic(&service_name); // TODO cope with secured mosquitto local_config.set_manual_acks(true); + // TODO const for payload + local_config.set_last_will(LastWill::new( + &health_topic, + C8Y_BRIDGE_DOWN_PAYLOAD, + QoS::AtLeastOnce, + true, + )); let mut cloud_config = MqttOptions::new( tedge_config.device.id.try_read(tedge_config).unwrap(), tedge_config @@ -86,21 +103,20 @@ impl MqttBridgeActorBuilder { .unwrap(); } - let (tx_pubs_from_cloud, rx_pubs_from_cloud) = mpsc::channel(10); - let (tx_pubs_from_local, rx_pubs_from_local) = mpsc::channel(10); - tokio::spawn(one_way_bridge( + let [msgs_local, msgs_cloud] = bidirectional_channel(10); + tokio::spawn(half_bridge( local_event_loop, cloud_client, move |topic| topic.strip_prefix(&topic_prefix).unwrap().into(), - tx_pubs_from_local, - rx_pubs_from_cloud, + msgs_local, + None, )); - tokio::spawn(one_way_bridge( + tokio::spawn(half_bridge( cloud_event_loop, local_client, move |topic| format!("{prefix}/{topic}").into(), - tx_pubs_from_cloud, - rx_pubs_from_local, + msgs_cloud, + Some(health_topic), )); Self { signal_sender } @@ -111,25 +127,82 @@ impl MqttBridgeActorBuilder { } } -async fn one_way_bridge Fn(&'a str) -> Cow<'a, str>>( +fn bidirectional_channel(buffer: usize) -> [BidirectionalChannelHalf; 2] { + let (tx_first, rx_first) = mpsc::channel(buffer); + let (tx_second, rx_second) = mpsc::channel(buffer); + [ + BidirectionalChannelHalf { + tx: tx_first, + rx: rx_second, + }, + BidirectionalChannelHalf { + tx: tx_second, + rx: rx_first, + }, + ] +} + +struct BidirectionalChannelHalf { + tx: mpsc::Sender, + rx: mpsc::Receiver, +} + +impl<'a, T> BidirectionalChannelHalf { + pub fn send(&'a mut self, item: T) -> futures::sink::Send<'a, mpsc::Sender, T> { + self.tx.send(item) + } + + pub fn recv(&mut self) -> futures::stream::Next> { + self.rx.next() + } +} + +async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( mut recv_event_loop: EventLoop, target: AsyncClient, transform_topic: F, - mut tx_pubs: mpsc::Sender, - mut rx_pubs: mpsc::Receiver, + mut corresponding_bridge_half: BidirectionalChannelHalf>, + health_topic: Option, ) { let mut forward_pkid_to_received_msg = HashMap::new(); - let mut last_err = None; + let mut last_err = Some("dummy error".into()); + loop { let notification = match recv_event_loop.poll().await { // TODO notify if this is us recovering from an error - Ok(notification) => notification, + Ok(notification) => { + if last_err.as_ref().is_some() { + // TODO clarify whether this is cloud/local + info!("MQTT bridge connected"); + last_err = None; + if let Some(health_topic) = &health_topic { + target + .publish(health_topic, QoS::AtLeastOnce, true, C8Y_BRIDGE_UP_PAYLOAD) + .await + .unwrap(); + corresponding_bridge_half.send(None).await.unwrap(); + } + } + notification + } Err(err) => { let err = err.to_string(); if last_err.as_ref() != Some(&err) { // TODO clarify whether this is cloud/local error!("MQTT bridge connection error: {err}"); last_err = Some(err); + if let Some(health_topic) = &health_topic { + target + .publish( + health_topic, + QoS::AtLeastOnce, + true, + C8Y_BRIDGE_DOWN_PAYLOAD, + ) + .await + .unwrap(); + corresponding_bridge_half.send(None).await.unwrap(); + } } continue; } @@ -146,23 +219,23 @@ async fn one_way_bridge Fn(&'a str) -> Cow<'a, str>>( ) .await .unwrap(); - tx_pubs.send(publish).await.unwrap(); + corresponding_bridge_half.send(Some(publish)).await.unwrap(); } // Forwarding acks from event loop to target Event::Incoming( Incoming::PubAck(PubAck { pkid: ack_pkid }) | Incoming::PubRec(PubRec { pkid: ack_pkid }), ) => { - target - .ack(&forward_pkid_to_received_msg.remove(&ack_pkid).unwrap()) - .await - .unwrap(); + if let Some(msg) = forward_pkid_to_received_msg.remove(&ack_pkid).unwrap() { + target.ack(&msg).await.unwrap(); + } } Event::Outgoing(Outgoing::Publish(pkid)) => { - if let Some(msg) = rx_pubs.next().await { - forward_pkid_to_received_msg.insert(pkid, msg); - } else { - break; + match corresponding_bridge_half.recv().await { + Some(optional_msg) => { + forward_pkid_to_received_msg.insert(pkid, optional_msg); + } + None => break, } } _ => {} From 392cfcfb07697980240822d5b1e41bad30e76a8a Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 7 Mar 2024 15:46:47 +0000 Subject: [PATCH 07/20] Fix unit tests following mapper bridge changes Signed-off-by: James Rhodes --- .../common/tedge_config/src/tedge_config_cli/tedge_config.rs | 5 ++++- crates/core/c8y_api/src/utils.rs | 3 ++- crates/core/tedge/src/bridge/aws.rs | 1 + crates/core/tedge/src/bridge/azure.rs | 1 + crates/core/tedge/src/bridge/c8y.rs | 2 ++ crates/core/tedge/src/bridge/config.rs | 4 ++++ crates/extensions/c8y_mapper_ext/src/converter.rs | 1 + crates/extensions/c8y_mapper_ext/src/tests.rs | 1 + 8 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index dfa114dcb03..f983369c2b3 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -454,7 +454,7 @@ define_tedge_config! { }, #[tedge_config(default(value = false))] - #[doku(skip)] + #[doku(skip)] // Hide the configuration in `tedge config list --doc` in_mapper: bool, // TODO validation @@ -462,6 +462,7 @@ define_tedge_config! { /// if this is set to "c8y", then messages published to `c8y/s/us` will be /// forwarded by to Cumulocity on the `s/us` topic #[tedge_config(example = "c8y", default(value = "c8y"))] + #[doku(skip)] // Hide the configuration in `tedge config list --doc` topic_prefix: TopicPrefix, }, @@ -817,9 +818,11 @@ define_tedge_config! { } impl ReadableKey { + // This is designed to be simple way of pub fn is_printable_value(self, value: &str) -> bool { match self { Self::C8yBridgeInMapper => value != "false", + Self::C8yBridgeTopicPrefix => value != "c8y", _ => true, } } diff --git a/crates/core/c8y_api/src/utils.rs b/crates/core/c8y_api/src/utils.rs index 20e56f7e5da..b29363d99e0 100644 --- a/crates/core/c8y_api/src/utils.rs +++ b/crates/core/c8y_api/src/utils.rs @@ -50,6 +50,7 @@ mod tests { use mqtt_channel::Topic; use test_case::test_case; + use crate::utils::bridge::is_c8y_bridge_established; use crate::utils::bridge::is_c8y_bridge_up; const C8Y_BRIDGE_HEALTH_TOPIC: &str = @@ -76,7 +77,7 @@ mod tests { let topic = Topic::new(topic).unwrap(); let message = Message::new(&topic, payload); - let actual = is_c8y_bridge_up(&message, "tedge-mapper-bridge-c8y"); + let actual = is_c8y_bridge_established(&message, "tedge-mapper-bridge-c8y"); assert_eq!(actual, expected); } } diff --git a/crates/core/tedge/src/bridge/aws.rs b/crates/core/tedge/src/bridge/aws.rs index e59f43a8ee1..4bc6cc53f97 100644 --- a/crates/core/tedge/src/bridge/aws.rs +++ b/crates/core/tedge/src/bridge/aws.rs @@ -126,6 +126,7 @@ fn test_bridge_config_from_aws_params() -> anyhow::Result<()> { notifications_local_only: true, notification_topic: MOSQUITTO_BRIDGE_TOPIC.into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; assert_eq!(bridge, expected); diff --git a/crates/core/tedge/src/bridge/azure.rs b/crates/core/tedge/src/bridge/azure.rs index 1da6f7fe868..a4db45b5bc6 100644 --- a/crates/core/tedge/src/bridge/azure.rs +++ b/crates/core/tedge/src/bridge/azure.rs @@ -126,6 +126,7 @@ fn test_bridge_config_from_azure_params() -> anyhow::Result<()> { notifications_local_only: true, notification_topic: MOSQUITTO_BRIDGE_TOPIC.into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; assert_eq!(bridge, expected); diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 13d95e21fe7..f4928b53dce 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -167,6 +167,7 @@ mod tests { bridge_keyfile: "./test-private-key.pem".into(), smartrest_templates: TemplatesSet::try_from(vec!["abc", "def"])?, include_local_clean_session: AutoFlag::False, + bridge_location: BridgeLocation::Mosquitto, }; let bridge = BridgeConfig::from(params); @@ -229,6 +230,7 @@ mod tests { notifications_local_only: true, notification_topic: C8Y_BRIDGE_HEALTH_TOPIC.into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; assert_eq!(bridge, expected); diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index d558e58b3fa..f7492a7acad 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -173,6 +173,7 @@ mod test { notifications_local_only: false, notification_topic: "test_topic".into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; let mut serialized_config = Vec::::new(); @@ -238,6 +239,7 @@ bridge_attempt_unsubscribe false notifications_local_only: false, notification_topic: "test_topic".into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; let mut serialized_config = Vec::::new(); bridge.serialize(&mut serialized_config)?; @@ -305,6 +307,7 @@ bridge_attempt_unsubscribe false notifications_local_only: false, notification_topic: "test_topic".into(), bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, }; let mut buffer = Vec::new(); @@ -442,6 +445,7 @@ bridge_attempt_unsubscribe false notification_topic: "test_topic".into(), bridge_attempt_unsubscribe: false, topics: vec![], + bridge_location: BridgeLocation::Mosquitto, } } } diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index a5503642f59..e8c80bb310f 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -3193,6 +3193,7 @@ pub(crate) mod tests { true, true, "c8y".into(), + false, ) } diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index 8be392cf7f3..5e467931d8d 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -2420,6 +2420,7 @@ pub(crate) async fn spawn_c8y_mapper_actor( true, true, "c8y".into(), + false, ); let mut mqtt_builder: SimpleMessageBoxBuilder = From 618a4f8f951190a35f87204aeb460eef300c7cf8 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 7 Mar 2024 16:09:56 +0000 Subject: [PATCH 08/20] Run formatter Signed-off-by: James Rhodes --- crates/extensions/tedge_mqtt_bridge/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/extensions/tedge_mqtt_bridge/Cargo.toml b/crates/extensions/tedge_mqtt_bridge/Cargo.toml index 3a00e000956..5c40d9c0b23 100644 --- a/crates/extensions/tedge_mqtt_bridge/Cargo.toml +++ b/crates/extensions/tedge_mqtt_bridge/Cargo.toml @@ -17,8 +17,8 @@ test-helpers = ["dep:assert-json-diff"] [dependencies] assert-json-diff = { workspace = true, optional = true } async-trait = { workspace = true } -certificate = { workspace = true } c8y_api = { workspace = true } +certificate = { workspace = true } futures = { workspace = true } mqtt_channel = { workspace = true } rumqttc = { workspace = true } From ce44a0342b7de2a627dc598511e4273b7156051a Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 8 Mar 2024 09:53:51 +0000 Subject: [PATCH 09/20] Remove unused deps Signed-off-by: James Rhodes --- Cargo.lock | 2 -- crates/extensions/tedge_mqtt_bridge/Cargo.toml | 5 ----- 2 files changed, 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a6715c08c..1f8277f0c45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3869,13 +3869,11 @@ dependencies = [ name = "tedge_mqtt_bridge" version = "1.0.1" dependencies = [ - "assert-json-diff", "async-trait", "c8y_api", "certificate", "futures", "mqtt_channel", - "mqtt_tests", "rumqttc", "serde_json", "tedge_actors", diff --git a/crates/extensions/tedge_mqtt_bridge/Cargo.toml b/crates/extensions/tedge_mqtt_bridge/Cargo.toml index 5c40d9c0b23..d4667f16be1 100644 --- a/crates/extensions/tedge_mqtt_bridge/Cargo.toml +++ b/crates/extensions/tedge_mqtt_bridge/Cargo.toml @@ -12,10 +12,8 @@ repository = { workspace = true } [features] # No features on by default default = [] -test-helpers = ["dep:assert-json-diff"] [dependencies] -assert-json-diff = { workspace = true, optional = true } async-trait = { workspace = true } c8y_api = { workspace = true } certificate = { workspace = true } @@ -29,8 +27,5 @@ tedge_utils = { workspace = true } tokio = { workspace = true, default_features = false, features = ["macros"] } tracing = { workspace = true } -[dev-dependencies] -mqtt_tests = { path = "../../tests/mqtt_tests" } - [lints] workspace = true From 397dac966fe93c67e3337420d6a19da52fd9e45d Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 8 Mar 2024 12:40:55 +0000 Subject: [PATCH 10/20] Apply review suggestions Signed-off-by: James Rhodes --- crates/core/c8y_api/src/smartrest/topic.rs | 3 +- .../extensions/tedge_mqtt_bridge/src/lib.rs | 62 ++++++++++++++----- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/core/c8y_api/src/smartrest/topic.rs b/crates/core/c8y_api/src/smartrest/topic.rs index 82af739a33a..59d0a7dbb7b 100644 --- a/crates/core/c8y_api/src/smartrest/topic.rs +++ b/crates/core/c8y_api/src/smartrest/topic.rs @@ -14,7 +14,6 @@ pub enum C8yTopic { SmartRestRequest, SmartRestResponse, ChildSmartRestResponse(String), - // OperationTopic(String), } impl C8yTopic { @@ -51,7 +50,7 @@ impl C8yTopic { Self::SmartRestResponse => format!("{prefix}/{SMARTREST_PUBLISH_TOPIC}"), Self::ChildSmartRestResponse(child_id) => { format!("{prefix}/{SMARTREST_PUBLISH_TOPIC}/{child_id}") - } // Self::OperationTopic(name) => name.into(), + } } } diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 88122ceb325..7c64617e1ac 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -23,6 +23,7 @@ use tedge_actors::futures::channel::mpsc; use tedge_actors::Actor; use tedge_actors::Builder; use tedge_actors::DynSender; +use tedge_actors::NullSender; use tedge_actors::RuntimeError; use tedge_actors::RuntimeRequest; use tedge_actors::RuntimeRequestSink; @@ -40,9 +41,7 @@ pub use mqtt_channel::Topic; pub use mqtt_channel::TopicFilter; use tedge_config::TEdgeConfig; -pub struct MqttBridgeActorBuilder { - signal_sender: mpsc::Sender, -} +pub struct MqttBridgeActorBuilder {} impl MqttBridgeActorBuilder { pub async fn new( @@ -56,7 +55,6 @@ impl MqttBridgeActorBuilder { tedge_config.device.cert_path.clone().into(), ) .unwrap(); - let (signal_sender, _signal_receiver) = mpsc::channel(10); let prefix = tedge_config.c8y.bridge.topic_prefix.clone(); let mut local_config = MqttOptions::new( @@ -67,7 +65,6 @@ impl MqttBridgeActorBuilder { let health_topic = main_device_health_topic(&service_name); // TODO cope with secured mosquitto local_config.set_manual_acks(true); - // TODO const for payload local_config.set_last_will(LastWill::new( &health_topic, C8Y_BRIDGE_DOWN_PAYLOAD, @@ -110,6 +107,7 @@ impl MqttBridgeActorBuilder { move |topic| topic.strip_prefix(&topic_prefix).unwrap().into(), msgs_local, None, + "local", )); tokio::spawn(half_bridge( cloud_event_loop, @@ -117,9 +115,10 @@ impl MqttBridgeActorBuilder { move |topic| format!("{prefix}/{topic}").into(), msgs_cloud, Some(health_topic), + "cloud", )); - Self { signal_sender } + Self {} } pub(crate) fn build_actor(self) -> MqttBridgeActor { @@ -157,23 +156,53 @@ impl<'a, T> BidirectionalChannelHalf { } } +/// Forward messages received from `recv_event_loop` to `target` +/// +/// The result of running this function constitutes half the MQTT bridge, hence the name. +/// Each half has two main responsibilities, one is to take messages received on the event +/// loop and forward them to the target client, the other is to communicate with the corresponding +/// half of the bridge to ensure published messages get acknowledged only when they have been +/// fully processed. +/// +/// # Message flow +/// Messages in the bridge go through a few states +/// 1. Received from the sending broker +/// 2. Forwarded to the receiving broker +/// 3. The receiving broker sends an acknowledgement +/// 4. The original forwarded message is acknowledged now we know it is fully processed +/// +/// Since the function is processing one [EventLoop], messages can be sent to the receiving broker, +/// but we cannot receive acknowledgements from that broker, therefore a communication link must +/// be established between the bridge halves. This link is the argument `corresponding_bridge_half`, +/// which can both send and receive messages from the other bridge loop. +/// +/// When a message is forwarded, the [Publish] is forwarded from this loop to the corresponding loop. +/// This allows the loop to store the message along with its packet ID when the forwarded message is +/// published. When an acknowledgement is received for the forwarded message, the packet id is used +/// to retrieve the original [Publish], which is then passed to [AsyncClient::ack] to complete the +/// final step of the message flow. +/// +/// The channel sends [Option] rather than [Publish] to allow the bridge to send entirely +/// novel messages, and not just forwarded ones, as attaching packet IDs relies on pairing every +/// [Outgoing] publish notification with a message sent by the relevant client. This means the +/// +/// # Health topics async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( mut recv_event_loop: EventLoop, target: AsyncClient, transform_topic: F, mut corresponding_bridge_half: BidirectionalChannelHalf>, health_topic: Option, + name: &'static str, ) { let mut forward_pkid_to_received_msg = HashMap::new(); let mut last_err = Some("dummy error".into()); loop { let notification = match recv_event_loop.poll().await { - // TODO notify if this is us recovering from an error Ok(notification) => { if last_err.as_ref().is_some() { - // TODO clarify whether this is cloud/local - info!("MQTT bridge connected"); + info!("MQTT bridge connected to {name} broker"); last_err = None; if let Some(health_topic) = &health_topic { target @@ -188,8 +217,7 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( Err(err) => { let err = err.to_string(); if last_err.as_ref() != Some(&err) { - // TODO clarify whether this is cloud/local - error!("MQTT bridge connection error: {err}"); + error!("MQTT bridge failed to connect to {name} broker: {err}"); last_err = Some(err); if let Some(health_topic) = &health_topic { target @@ -219,22 +247,24 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( ) .await .unwrap(); - corresponding_bridge_half.send(Some(publish)).await.unwrap(); + let publish = (publish.qos > QoS::AtMostOnce).then_some(publish); + corresponding_bridge_half.send(publish).await.unwrap(); } // Forwarding acks from event loop to target Event::Incoming( Incoming::PubAck(PubAck { pkid: ack_pkid }) | Incoming::PubRec(PubRec { pkid: ack_pkid }), ) => { - if let Some(msg) = forward_pkid_to_received_msg.remove(&ack_pkid).unwrap() { + if let Some(msg) = forward_pkid_to_received_msg.remove(&ack_pkid) { target.ack(&msg).await.unwrap(); } } Event::Outgoing(Outgoing::Publish(pkid)) => { match corresponding_bridge_half.recv().await { - Some(optional_msg) => { - forward_pkid_to_received_msg.insert(pkid, optional_msg); + Some(Some(msg)) => { + forward_pkid_to_received_msg.insert(pkid, msg); } + Some(None) => {} None => break, } } @@ -257,7 +287,7 @@ impl Builder for MqttBridgeActorBuilder { impl RuntimeRequestSink for MqttBridgeActorBuilder { fn get_signal_sender(&self) -> DynSender { - Box::new(self.signal_sender.clone()) + NullSender.into() } } From 335ac91a2631e7c05c414af91279e7be48c86493 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 8 Mar 2024 15:17:11 +0000 Subject: [PATCH 11/20] Finish doc comment Signed-off-by: James Rhodes --- .../extensions/tedge_mqtt_bridge/src/lib.rs | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 7c64617e1ac..ef7d7761ca7 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -159,10 +159,9 @@ impl<'a, T> BidirectionalChannelHalf { /// Forward messages received from `recv_event_loop` to `target` /// /// The result of running this function constitutes half the MQTT bridge, hence the name. -/// Each half has two main responsibilities, one is to take messages received on the event -/// loop and forward them to the target client, the other is to communicate with the corresponding -/// half of the bridge to ensure published messages get acknowledged only when they have been -/// fully processed. +/// Each half has two main responsibilities, one is to take messages received on the event loop and +/// forward them to the target client, the other is to communicate with the companion half of the +/// bridge to ensure published messages get acknowledged only when they have been fully processed. /// /// # Message flow /// Messages in the bridge go through a few states @@ -184,14 +183,25 @@ impl<'a, T> BidirectionalChannelHalf { /// /// The channel sends [Option] rather than [Publish] to allow the bridge to send entirely /// novel messages, and not just forwarded ones, as attaching packet IDs relies on pairing every -/// [Outgoing] publish notification with a message sent by the relevant client. This means the +/// [Outgoing] publish notification with a message sent by the relevant client. So, when a QoS 1 +/// message is forwarded, this will be accompanied by sending `Some(message)` to the channel, +/// allowing the original message to be acknowledged once an acknowledgement is received for the +/// forwarded message. When publishing a health message, this will be accompanied by sending `None` +/// to the channel, telling the bridge to ignore the associated packet ID as this didn't arise from +/// a forwarded message that itself requires acknowledgement. /// /// # Health topics +/// The bridge will publish health information to `health_topic` (if supplied) on `target` to enable +/// other components to establish bridge health. This is intended to be used the half with cloud +/// event loop, so the status of this connection will be relayed to a relevant `te` topic like its +/// mosquitto-based predecessor. The payload is either `1` (healthy) or `0` (unhealthy). When the +/// connection is created, the last-will message is set to send the `0` payload when the connection +/// is dropped. async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( mut recv_event_loop: EventLoop, target: AsyncClient, transform_topic: F, - mut corresponding_bridge_half: BidirectionalChannelHalf>, + mut companion_bridge_half: BidirectionalChannelHalf>, health_topic: Option, name: &'static str, ) { @@ -209,7 +219,7 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( .publish(health_topic, QoS::AtLeastOnce, true, C8Y_BRIDGE_UP_PAYLOAD) .await .unwrap(); - corresponding_bridge_half.send(None).await.unwrap(); + companion_bridge_half.send(None).await.unwrap(); } } notification @@ -229,7 +239,7 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( ) .await .unwrap(); - corresponding_bridge_half.send(None).await.unwrap(); + companion_bridge_half.send(None).await.unwrap(); } } continue; @@ -248,7 +258,7 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( .await .unwrap(); let publish = (publish.qos > QoS::AtMostOnce).then_some(publish); - corresponding_bridge_half.send(publish).await.unwrap(); + companion_bridge_half.send(publish).await.unwrap(); } // Forwarding acks from event loop to target Event::Incoming( @@ -259,15 +269,13 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( target.ack(&msg).await.unwrap(); } } - Event::Outgoing(Outgoing::Publish(pkid)) => { - match corresponding_bridge_half.recv().await { - Some(Some(msg)) => { - forward_pkid_to_received_msg.insert(pkid, msg); - } - Some(None) => {} - None => break, + Event::Outgoing(Outgoing::Publish(pkid)) => match companion_bridge_half.recv().await { + Some(Some(msg)) => { + forward_pkid_to_received_msg.insert(pkid, msg); } - } + Some(None) => {} + None => break, + }, _ => {} } } From a6db6bce83a8532e1e117507b3ddae2bda5dc363 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Mon, 11 Mar 2024 17:07:42 +0000 Subject: [PATCH 12/20] Apply more review suggestions Signed-off-by: James Rhodes --- Cargo.lock | 4 +- .../src/tedge_config_cli/tedge_config.rs | 11 +- crates/core/c8y_api/src/utils.rs | 15 +- crates/core/tedge/src/bridge/config.rs | 2 +- crates/core/tedge/src/cli/connect/command.rs | 6 +- crates/core/tedge_api/src/health.rs | 8 + crates/core/tedge_api/src/lib.rs | 1 + crates/core/tedge_mapper/src/c8y/mapper.rs | 2 +- crates/extensions/c8y_mapper_ext/src/actor.rs | 2 +- .../extensions/c8y_mapper_ext/src/config.rs | 2 +- crates/extensions/c8y_mapper_ext/src/tests.rs | 6 +- .../extensions/tedge_mqtt_bridge/Cargo.toml | 4 +- .../extensions/tedge_mqtt_bridge/src/lib.rs | 180 +++++++++++++----- 13 files changed, 169 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f8277f0c45..7c67aa5dc2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3870,15 +3870,13 @@ name = "tedge_mqtt_bridge" version = "1.0.1" dependencies = [ "async-trait", - "c8y_api", "certificate", "futures", "mqtt_channel", "rumqttc", - "serde_json", "tedge_actors", + "tedge_api", "tedge_config", - "tedge_utils", "tokio", "tracing", ] diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index f983369c2b3..e84c620144e 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -455,7 +455,7 @@ define_tedge_config! { #[tedge_config(default(value = false))] #[doku(skip)] // Hide the configuration in `tedge config list --doc` - in_mapper: bool, + built_in: bool, // TODO validation /// The topic prefix that will be used for the mapper bridge MQTT topic. For instance, @@ -818,10 +818,15 @@ define_tedge_config! { } impl ReadableKey { - // This is designed to be simple way of + // This is designed to be a simple way of controlling whether values appear in the output of + // `tedge config list`. Ideally this would be integrated into [define_tedge_config], see + // https://github.com/thin-edge/thin-edge.io/issues/2767 for more detail on that. + // Currently this accompanies `#[doku(skip)]` on the relevant configurations, which hides + // them in `tedge config list --doc`. The configurations are hidden to avoid unfinished + // features from being discovered. pub fn is_printable_value(self, value: &str) -> bool { match self { - Self::C8yBridgeInMapper => value != "false", + Self::C8yBridgeBuiltIn => value != "false", Self::C8yBridgeTopicPrefix => value != "c8y", _ => true, } diff --git a/crates/core/c8y_api/src/utils.rs b/crates/core/c8y_api/src/utils.rs index b29363d99e0..b91ba0835cd 100644 --- a/crates/core/c8y_api/src/utils.rs +++ b/crates/core/c8y_api/src/utils.rs @@ -1,19 +1,14 @@ pub mod bridge { use mqtt_channel::Message; - - // FIXME: doesn't account for custom topic root, use MQTT scheme API here - pub const C8Y_BRIDGE_UP_PAYLOAD: &str = "1"; - pub const C8Y_BRIDGE_DOWN_PAYLOAD: &str = "0"; - - pub fn main_device_health_topic(service: &str) -> String { - format!("te/device/main/service/{service}/status/health") - } + use tedge_api::main_device_health_topic; + use tedge_api::MQTT_BRIDGE_DOWN_PAYLOAD; + use tedge_api::MQTT_BRIDGE_UP_PAYLOAD; pub fn is_c8y_bridge_up(message: &Message, service: &str) -> bool { let c8y_bridge_health_topic = main_device_health_topic(service); match message.payload_str() { Ok(payload) => { - message.topic.name == c8y_bridge_health_topic && payload == C8Y_BRIDGE_UP_PAYLOAD + message.topic.name == c8y_bridge_health_topic && payload == MQTT_BRIDGE_UP_PAYLOAD } Err(_err) => false, } @@ -24,7 +19,7 @@ pub mod bridge { match message.payload_str() { Ok(payload) => { message.topic.name == c8y_bridge_health_topic - && (payload == C8Y_BRIDGE_UP_PAYLOAD || payload == C8Y_BRIDGE_DOWN_PAYLOAD) + && (payload == MQTT_BRIDGE_UP_PAYLOAD || payload == MQTT_BRIDGE_DOWN_PAYLOAD) } Err(_err) => false, } diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index f7492a7acad..7bcf039490f 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -38,7 +38,7 @@ pub struct BridgeConfig { #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum BridgeLocation { Mosquitto, - Mapper, + BuiltIn, } impl BridgeConfig { diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index bc0a208fdcb..8076ee30b1f 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -197,8 +197,8 @@ pub fn bridge_config( Ok(BridgeConfig::from(params)) } Cloud::C8y => { - let bridge_location = match config.c8y.bridge.in_mapper { - true => BridgeLocation::Mapper, + let bridge_location = match config.c8y.bridge.built_in { + true => BridgeLocation::BuiltIn, false => BridgeLocation::Mosquitto, }; let params = BridgeConfigC8yParams { @@ -440,7 +440,7 @@ fn new_bridge( config_location: &TEdgeConfigLocation, device_type: &str, ) -> Result<(), ConnectError> { - if bridge_config.bridge_location == BridgeLocation::Mapper { + if bridge_config.bridge_location == BridgeLocation::BuiltIn { clean_up(config_location, bridge_config)?; return Ok(()); } diff --git a/crates/core/tedge_api/src/health.rs b/crates/core/tedge_api/src/health.rs index 29fb1bcd819..a37c9968530 100644 --- a/crates/core/tedge_api/src/health.rs +++ b/crates/core/tedge_api/src/health.rs @@ -11,6 +11,14 @@ use std::process; use std::sync::Arc; use tedge_utils::timestamp::TimeFormat; +pub const MQTT_BRIDGE_UP_PAYLOAD: &str = "1"; +pub const MQTT_BRIDGE_DOWN_PAYLOAD: &str = "0"; + +// FIXME: doesn't account for custom topic root, use MQTT scheme API here +pub fn main_device_health_topic(service: &str) -> String { + format!("te/device/main/service/{service}/status/health") +} + /// Encodes a valid health topic. /// /// Health topics are topics on which messages about health status of services are published. To be diff --git a/crates/core/tedge_api/src/lib.rs b/crates/core/tedge_api/src/lib.rs index 71788d40d17..58df85a200e 100644 --- a/crates/core/tedge_api/src/lib.rs +++ b/crates/core/tedge_api/src/lib.rs @@ -23,6 +23,7 @@ pub mod workflow; pub use download::*; pub use entity_store::EntityStore; pub use error::*; +pub use health::*; pub use messages::CommandStatus; pub use messages::Jsonify; pub use messages::OperationStatus; diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 6783bfff0fc..d1d0bf663df 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -38,7 +38,7 @@ impl TEdgeComponent for CumulocityMapper { let mqtt_config = tedge_config.mqtt_config()?; let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config)?; - if tedge_config.c8y.bridge.in_mapper { + if tedge_config.c8y.bridge.built_in { let custom_topics = tedge_config .c8y .smartrest diff --git a/crates/extensions/c8y_mapper_ext/src/actor.rs b/crates/extensions/c8y_mapper_ext/src/actor.rs index 64f86257824..62452a357ca 100644 --- a/crates/extensions/c8y_mapper_ext/src/actor.rs +++ b/crates/extensions/c8y_mapper_ext/src/actor.rs @@ -8,7 +8,6 @@ use c8y_api::smartrest::smartrest_serializer::succeed_static_operation; use c8y_api::smartrest::smartrest_serializer::CumulocitySupportedOperations; use c8y_api::smartrest::smartrest_serializer::SmartRest; use c8y_api::utils::bridge::is_c8y_bridge_established; -use c8y_api::utils::bridge::main_device_health_topic; use c8y_auth_proxy::url::ProxyUrlGenerator; use c8y_http_proxy::handle::C8YHttpProxy; use c8y_http_proxy::messages::C8YRestRequest; @@ -33,6 +32,7 @@ use tedge_actors::Service; use tedge_actors::ServiceProvider; use tedge_actors::SimpleMessageBox; use tedge_actors::SimpleMessageBoxBuilder; +use tedge_api::main_device_health_topic; use tedge_downloader_ext::DownloadRequest; use tedge_downloader_ext::DownloadResult; use tedge_file_system_ext::FsWatchEvent; diff --git a/crates/extensions/c8y_mapper_ext/src/config.rs b/crates/extensions/c8y_mapper_ext/src/config.rs index d1b8a8edb06..3a4673cb27b 100644 --- a/crates/extensions/c8y_mapper_ext/src/config.rs +++ b/crates/extensions/c8y_mapper_ext/src/config.rs @@ -197,7 +197,7 @@ impl C8yMapperConfig { } } - let bridge_in_mapper = tedge_config.c8y.bridge.in_mapper; + let bridge_in_mapper = tedge_config.c8y.bridge.built_in; Ok(C8yMapperConfig::new( config_dir, diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index 84f78209ba9..39d8adfddee 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -10,8 +10,6 @@ use crate::Capabilities; use assert_json_diff::assert_json_include; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use c8y_api::smartrest::topic::C8yTopic; -use c8y_api::utils::bridge::main_device_health_topic; -use c8y_api::utils::bridge::C8Y_BRIDGE_UP_PAYLOAD; use c8y_auth_proxy::url::Protocol; use c8y_http_proxy::messages::C8YRestRequest; use c8y_http_proxy::messages::C8YRestResult; @@ -33,10 +31,12 @@ use tedge_actors::Sender; use tedge_actors::SimpleMessageBox; use tedge_actors::SimpleMessageBoxBuilder; use tedge_actors::WrappedInput; +use tedge_api::main_device_health_topic; use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::mqtt_topics::MqttSchema; use tedge_api::CommandStatus; use tedge_api::SoftwareUpdateCommand; +use tedge_api::MQTT_BRIDGE_UP_PAYLOAD; use tedge_config::TEdgeConfig; use tedge_file_system_ext::FsWatchEvent; use tedge_mqtt_ext::test_helpers::assert_received_contains_str; @@ -2456,7 +2456,7 @@ pub(crate) async fn spawn_c8y_mapper_actor( let mut service_monitor_box = service_monitor_builder.build(); let bridge_status_msg = MqttMessage::new( &Topic::new_unchecked(&main_device_health_topic("tedge-mapper-bridge-c8y")), - C8Y_BRIDGE_UP_PAYLOAD, + MQTT_BRIDGE_UP_PAYLOAD, ); service_monitor_box.send(bridge_status_msg).await.unwrap(); diff --git a/crates/extensions/tedge_mqtt_bridge/Cargo.toml b/crates/extensions/tedge_mqtt_bridge/Cargo.toml index d4667f16be1..b05d6679571 100644 --- a/crates/extensions/tedge_mqtt_bridge/Cargo.toml +++ b/crates/extensions/tedge_mqtt_bridge/Cargo.toml @@ -15,15 +15,13 @@ default = [] [dependencies] async-trait = { workspace = true } -c8y_api = { workspace = true } certificate = { workspace = true } futures = { workspace = true } mqtt_channel = { workspace = true } rumqttc = { workspace = true } -serde_json = { workspace = true } tedge_actors = { workspace = true } +tedge_api = { workspace = true } tedge_config = { workspace = true } -tedge_utils = { workspace = true } tokio = { workspace = true, default_features = false, features = ["macros"] } tracing = { workspace = true } diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index ef7d7761ca7..4fecae45fd9 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -1,11 +1,9 @@ use async_trait::async_trait; -use c8y_api::utils::bridge::main_device_health_topic; -use c8y_api::utils::bridge::C8Y_BRIDGE_DOWN_PAYLOAD; -use c8y_api::utils::bridge::C8Y_BRIDGE_UP_PAYLOAD; use certificate::parse_root_certificate::create_tls_config; use futures::SinkExt; use futures::StreamExt; use rumqttc::AsyncClient; +use rumqttc::ConnectionError; use rumqttc::Event; use rumqttc::EventLoop; use rumqttc::Incoming; @@ -39,6 +37,9 @@ pub use mqtt_channel::MqttError; pub use mqtt_channel::QoS; pub use mqtt_channel::Topic; pub use mqtt_channel::TopicFilter; +use tedge_api::main_device_health_topic; +use tedge_api::MQTT_BRIDGE_DOWN_PAYLOAD; +use tedge_api::MQTT_BRIDGE_UP_PAYLOAD; use tedge_config::TEdgeConfig; pub struct MqttBridgeActorBuilder {} @@ -67,7 +68,7 @@ impl MqttBridgeActorBuilder { local_config.set_manual_acks(true); local_config.set_last_will(LastWill::new( &health_topic, - C8Y_BRIDGE_DOWN_PAYLOAD, + MQTT_BRIDGE_DOWN_PAYLOAD, QoS::AtLeastOnce, true, )); @@ -172,16 +173,16 @@ impl<'a, T> BidirectionalChannelHalf { /// /// Since the function is processing one [EventLoop], messages can be sent to the receiving broker, /// but we cannot receive acknowledgements from that broker, therefore a communication link must -/// be established between the bridge halves. This link is the argument `corresponding_bridge_half`, +/// be established between the bridge halves. This link is the argument `companion_bridge_half`, /// which can both send and receive messages from the other bridge loop. /// -/// When a message is forwarded, the [Publish] is forwarded from this loop to the corresponding loop. +/// When a message is forwarded, the [Publish] is forwarded from this loop to the companion loop. /// This allows the loop to store the message along with its packet ID when the forwarded message is /// published. When an acknowledgement is received for the forwarded message, the packet id is used /// to retrieve the original [Publish], which is then passed to [AsyncClient::ack] to complete the /// final step of the message flow. /// -/// The channel sends [Option] rather than [Publish] to allow the bridge to send entirely +/// The channel sends [`Option`] rather than [`Publish`] to allow the bridge to send entirely /// novel messages, and not just forwarded ones, as attaching packet IDs relies on pairing every /// [Outgoing] publish notification with a message sent by the relevant client. So, when a QoS 1 /// message is forwarded, this will be accompanied by sending `Some(message)` to the channel, @@ -190,6 +191,66 @@ impl<'a, T> BidirectionalChannelHalf { /// to the channel, telling the bridge to ignore the associated packet ID as this didn't arise from /// a forwarded message that itself requires acknowledgement. /// +/// ## Bridging local messages to the cloud +/// +/// The two `half-bridge` instances cooperate: +/// +/// - The `half_bridge(local_event_loop,cloud_client)` receives local messages and publishes these message on the cloud. +/// - The `half_bridge(cloud_event_loop,local_client)` handles the acknowledgements: waiting for messages be acknowledged by the cloud, before sending acks for the original messages. +/// +/// ```text +/// ┌───────────────┐ ┌───────────────┐ +/// │ (EventLoop) │ │ (client) │ +/// Incoming::PubAck │ ┌──┐ │ │ ┌──┐ │ client.ack +/// ─────────────────┼────►│6.├──────┼───────────────────┬───────────────┼────►│7.├──────┼──────────────► +/// │ └──┘ │ │ │ └──┘ │ +/// │ │ │ │ │ +/// │ │ ┌─────┼────────┐ │ │ +/// │ │ │ │ │ │ │ +/// Outgoing::Publish │ ┌──┐ │ │ ┌┴─┐ │ │ │ +/// ─────────────────┼────►│4.├──────┼─────────────┼───►│5.│ │ │ │ +/// │ └─▲┘ │ │ └▲─┘ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ half_bridge(cloud_event_loop,local_client) +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// xxxxxxxxxxxxxxxxxxxxxxxxxx│xxxxxxxxxxxxxxxxxxxxx│xxxxx│xxxxxxxx│xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ half_bridge(local_event_loop,cloud_client) +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// │ │ │ │ │ │ │ │ +/// client.publish │ ┌┴─┐ │ │ ┌┴─┐ │ │ ┌──┐ │ Incoming::Publish +/// ◄────────────────┼──────┤3.◄─────┼─────────────┼────┤2.│◄─────┼──────┼─────┤1.│◄─────┼─────────────── +/// │ └──┘ │ │ └──┘ │ │ └──┘ │ +/// │ │ │ │ │ │ +/// │ MQTT │ │ │ │ MQTT │ +/// │ cloud │ └──────────────┘ │ local │ +/// │ connection │ │ connection │ +/// │ │ │ │ +/// │ (client) │ │ (EventLoop) │ +/// └───────────────┘ └───────────────┘ +/// ``` +/// +/// 1. A message is received via the local_event_loop. +/// 2. This message is sent unchanged to the second half_bridge which owns the local_client: so the latter will be able to acknowledge it when fully processed. +/// 3. A copy of this message is published by the cloud_client on the cloud topic derived from the local topic. +/// 4. The cloud_event_loop is notified that the message has been published by the cloud client. The notification event provides the `pkid` of the message used on the cloud connection. +/// 5. The message cloud `pkid` is joined with the original local message sent step 2. The pair (cloud `pkid`, local message) is cached +/// 6. The cloud MQTT end-point acknowledges the message, providing its cloud `pkid`. +/// 7. The pair (cloud `pkid`, local message) is extracted from the cache and the local message is finaly acknowledged. +/// +/// ## Bridging cloud messages to the local broker +/// +/// The very same two `half-bridge` instances ensure the reverse flow. Their roles are simply swapped: +/// +/// - The `half_bridge(cloud_event_loop,local_client)` receives cloud messages and publishes these message locally. +/// - The `half_bridge(local_event_loop,cloud_client)` handles the acknowledgements: waiting for messages be acknowledged locally, before sending acks for the original messages. +/// /// # Health topics /// The bridge will publish health information to `health_topic` (if supplied) on `target` to enable /// other components to establish bridge health. This is intended to be used the half with cloud @@ -206,47 +267,19 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( name: &'static str, ) { let mut forward_pkid_to_received_msg = HashMap::new(); - let mut last_err = Some("dummy error".into()); + let mut bridge_health = BridgeHealth::new(name, health_topic, &target); loop { - let notification = match recv_event_loop.poll().await { - Ok(notification) => { - if last_err.as_ref().is_some() { - info!("MQTT bridge connected to {name} broker"); - last_err = None; - if let Some(health_topic) = &health_topic { - target - .publish(health_topic, QoS::AtLeastOnce, true, C8Y_BRIDGE_UP_PAYLOAD) - .await - .unwrap(); - companion_bridge_half.send(None).await.unwrap(); - } - } - notification - } - Err(err) => { - let err = err.to_string(); - if last_err.as_ref() != Some(&err) { - error!("MQTT bridge failed to connect to {name} broker: {err}"); - last_err = Some(err); - if let Some(health_topic) = &health_topic { - target - .publish( - health_topic, - QoS::AtLeastOnce, - true, - C8Y_BRIDGE_DOWN_PAYLOAD, - ) - .await - .unwrap(); - companion_bridge_half.send(None).await.unwrap(); - } - } - continue; - } + let res = recv_event_loop.poll().await; + bridge_health.update(&res, &mut companion_bridge_half).await; + + let notification = match res { + Ok(notification) => notification, + Err(_) => continue, }; + match notification { - // Forwarding messages from event loop to target + // Forward messages from event loop to target Event::Incoming(Incoming::Publish(publish)) => { target .publish( @@ -260,7 +293,8 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( let publish = (publish.qos > QoS::AtMostOnce).then_some(publish); companion_bridge_half.send(publish).await.unwrap(); } - // Forwarding acks from event loop to target + + // Forward acks from event loop to target Event::Incoming( Incoming::PubAck(PubAck { pkid: ack_pkid }) | Incoming::PubRec(PubRec { pkid: ack_pkid }), @@ -269,11 +303,18 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( target.ack(&msg).await.unwrap(); } } + + // Keep track of packet IDs so we can acknowledge messages Event::Outgoing(Outgoing::Publish(pkid)) => match companion_bridge_half.recv().await { + // A message was forwarded by the other bridge half, note the packet id Some(Some(msg)) => { forward_pkid_to_received_msg.insert(pkid, msg); } + + // A healthcheck message was published, ignore this packet id Some(None) => {} + + // The other bridge half has disconnected, break the loop and shut down the bridge None => break, }, _ => {} @@ -281,6 +322,55 @@ async fn half_bridge Fn(&'a str) -> Cow<'a, str>>( } } +type NotificationRes = Result; + +struct BridgeHealth<'a> { + name: &'static str, + health_topic: Option, + target: &'a AsyncClient, + last_err: Option, +} + +impl<'a> BridgeHealth<'a> { + fn new(name: &'static str, health_topic: Option, target: &'a AsyncClient) -> Self { + Self { + name, + health_topic, + target, + last_err: Some("dummy error".into()), + } + } + async fn update( + &mut self, + result: &NotificationRes, + companion_bridge_half: &mut BidirectionalChannelHalf>, + ) { + let name = self.name; + let (err, health_payload) = match result { + Ok(_) => { + info!("MQTT bridge connected to {name} broker"); + (None, MQTT_BRIDGE_UP_PAYLOAD) + } + Err(err) => { + error!("MQTT bridge failed to connect to {name} broker: {err}"); + (Some(err.to_string()), MQTT_BRIDGE_DOWN_PAYLOAD) + } + }; + + if self.last_err != err { + self.last_err = err; + if let Some(health_topic) = &self.health_topic { + self.target + .publish(health_topic, QoS::AtLeastOnce, true, health_payload) + .await + .unwrap(); + // Send a note that a message has been published to maintain synchronisation + // between the two bridge halves + companion_bridge_half.send(None).await.unwrap(); + } + } + } +} impl Builder for MqttBridgeActorBuilder { type Error = Infallible; From 30e243470df1e0500574e54035ff97fb591f6041 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 12 Mar 2024 14:29:15 +0000 Subject: [PATCH 13/20] Only log error when message changes Signed-off-by: James Rhodes --- crates/extensions/tedge_mqtt_bridge/src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index 4fecae45fd9..c0668741a86 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -347,18 +347,17 @@ impl<'a> BridgeHealth<'a> { ) { let name = self.name; let (err, health_payload) = match result { - Ok(_) => { - info!("MQTT bridge connected to {name} broker"); - (None, MQTT_BRIDGE_UP_PAYLOAD) - } - Err(err) => { - error!("MQTT bridge failed to connect to {name} broker: {err}"); - (Some(err.to_string()), MQTT_BRIDGE_DOWN_PAYLOAD) - } + Ok(_) => (None, MQTT_BRIDGE_UP_PAYLOAD), + Err(err) => (Some(err.to_string()), MQTT_BRIDGE_DOWN_PAYLOAD), }; if self.last_err != err { + match &err { + None => info!("MQTT bridge connected to {name} broker"), + Some(err) => error!("MQTT bridge failed to connect to {name} broker: {err}"), + } self.last_err = err; + if let Some(health_topic) = &self.health_topic { self.target .publish(health_topic, QoS::AtLeastOnce, true, health_payload) From 172caab76b557a0abfb162ba3d8ab221605b3135 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 12 Mar 2024 14:34:31 +0000 Subject: [PATCH 14/20] Allow `tedge connect c8y --test` to work with internal bridge Signed-off-by: James Rhodes --- crates/core/tedge/src/cli/connect/command.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 8076ee30b1f..0395f20a173 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -65,7 +65,11 @@ impl Command for ConnectCommand { let updated_mosquitto_config = CommonMosquittoConfig::from_tedge_config(config); if self.is_test_connection { - if self.check_if_bridge_exists(&bridge_config) { + // 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 + if bridge_config.bridge_location == BridgeLocation::BuiltIn + || self.check_if_bridge_exists(&bridge_config) + { return match self.check_connection(config) { Ok(DeviceStatus::AlreadyExists) => { let cloud = bridge_config.cloud_name; From a36f0b250d625a94eb02b5123539206034d677ea Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 12 Mar 2024 14:47:44 +0000 Subject: [PATCH 15/20] Log connected only on connack Signed-off-by: James Rhodes --- crates/extensions/tedge_mqtt_bridge/src/lib.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index c0668741a86..acde8a1416a 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -340,6 +340,7 @@ impl<'a> BridgeHealth<'a> { last_err: Some("dummy error".into()), } } + async fn update( &mut self, result: &NotificationRes, @@ -347,14 +348,18 @@ impl<'a> BridgeHealth<'a> { ) { let name = self.name; let (err, health_payload) = match result { - Ok(_) => (None, MQTT_BRIDGE_UP_PAYLOAD), + Ok(event) => { + if let Event::Incoming(Incoming::ConnAck(_)) = event { + info!("MQTT bridge connected to {name} broker") + } + (None, MQTT_BRIDGE_UP_PAYLOAD) + } Err(err) => (Some(err.to_string()), MQTT_BRIDGE_DOWN_PAYLOAD), }; if self.last_err != err { - match &err { - None => info!("MQTT bridge connected to {name} broker"), - Some(err) => error!("MQTT bridge failed to connect to {name} broker: {err}"), + if let Some(err) = &err { + error!("MQTT bridge failed to connect to {name} broker: {err}") } self.last_err = err; From 3c91d10fbcc095cb6c32b7813c60b2c1b7327ad3 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 15 Mar 2024 10:07:21 +0000 Subject: [PATCH 16/20] Add (some) mapper-bridge support for authenticated MQTT connections to local broker Signed-off-by: James Rhodes --- .../extensions/tedge_mqtt_bridge/src/lib.rs | 34 ++- ...dge_device_telemetry_built-in_bridge.robot | 278 ++++++++++++++++++ 2 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot diff --git a/crates/extensions/tedge_mqtt_bridge/src/lib.rs b/crates/extensions/tedge_mqtt_bridge/src/lib.rs index acde8a1416a..049dda60b56 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/lib.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/lib.rs @@ -40,6 +40,7 @@ pub use mqtt_channel::TopicFilter; use tedge_api::main_device_health_topic; use tedge_api::MQTT_BRIDGE_DOWN_PAYLOAD; use tedge_api::MQTT_BRIDGE_UP_PAYLOAD; +use tedge_config::MqttAuthConfig; use tedge_config::TEdgeConfig; pub struct MqttBridgeActorBuilder {} @@ -64,7 +65,38 @@ impl MqttBridgeActorBuilder { tedge_config.mqtt.client.port.into(), ); let health_topic = main_device_health_topic(&service_name); - // TODO cope with secured mosquitto + // TODO cope with certs but not ca_dir, or handle that case with an explicit error message? + let auth_config = tedge_config.mqtt_client_auth_config(); + let local_tls_config = match auth_config { + MqttAuthConfig { + ca_dir: Some(ca_dir), + client: Some(client), + .. + } => Some( + create_tls_config( + ca_dir.into(), + client.key_file.into(), + client.cert_file.into(), + ) + .unwrap(), + ), + MqttAuthConfig { + ca_file: Some(ca_file), + client: Some(client), + .. + } => Some( + create_tls_config( + ca_file.into(), + client.key_file.into(), + client.cert_file.into(), + ) + .unwrap(), + ), + _ => None, + }; + if let Some(tls_config) = local_tls_config { + local_config.set_transport(Transport::tls_with_config(tls_config.into())); + } local_config.set_manual_acks(true); local_config.set_last_will(LastWill::new( &health_topic, diff --git a/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot new file mode 100644 index 00000000000..0dc67570603 --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot @@ -0,0 +1,278 @@ +*** Settings *** +Resource ../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Tags theme:c8y theme:telemetry +Suite Setup Custom Setup +Test Teardown Get Logs + +*** Variables *** +${C8Y_TOPIC_PREFIX} custom-c8y-prefix + +*** Test Cases *** +Thin-edge devices support sending simple measurements + Execute Command tedge mqtt pub te/device/main///m/ '{ "temperature": 25 }' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=ThinEdgeMeasurement value=temperature series=temperature + Log ${measurements} + +Bridge stops if mapper stops running + Execute Command tedge mqtt pub ${C8Y_TOPIC_PREFIX}/s/us '200,CustomMeasurement,temperature,25' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomMeasurement series=temperature + Log ${measurements} + Execute Command systemctl stop tedge-mapper-c8y + Execute Command tedge mqtt pub ${C8Y_TOPIC_PREFIX}/s/us '200,CustomMeasurement,temperature,25' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomMeasurement series=temperature + Log ${measurements} + Execute Command systemctl start tedge-mapper-c8y + +Thin-edge devices support sending simple measurements with custom type + Execute Command tedge mqtt pub te/device/main///m/ '{ "type":"CustomType", "temperature": 25 }' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomType value=temperature series=temperature + Log ${measurements} + +Thin-edge devices support sending simple measurements with custom type in topic + Execute Command tedge mqtt pub te/device/main///m/CustomType_topic '{ "temperature": 25 }' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomType_topic value=temperature series=temperature + Log ${measurements} + + +Thin-edge devices support sending simple measurements with custom type in payload + Execute Command tedge mqtt pub te/device/main///m/CustomType_topic '{ "type":"CustomType_payload","temperature": 25 }' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomType_payload value=temperature series=temperature + Log ${measurements} + +Thin-edge devices support sending custom measurements + Execute Command tedge mqtt pub te/device/main///m/ '{ "current": {"L1": 9.5, "L2": 1.3} }' + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=ThinEdgeMeasurement value=current series=L1 + Log ${measurements} + + +Thin-edge devices support sending custom events + Execute Command tedge mqtt pub te/device/main///e/myCustomType1 '{ "text": "Some test event", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${events}= Device Should Have Event/s expected_text=Some test event with_attachment=False minimum=1 maximum=1 type=myCustomType1 fragment=someOtherCustomFragment + Log ${events} + + +Thin-edge devices support sending large events + Execute Command tedge mqtt pub te/device/main///e/largeEvent "$(printf '{"text":"Large event","large_text_field":"%s"}' "$(yes "x" | head -n 100000 | tr -d '\n')")" + ${events}= Device Should Have Event/s expected_text=Large event with_attachment=False minimum=1 maximum=1 type=largeEvent fragment=large_text_field + Length Should Be ${events[0]["large_text_field"]} 100000 + Log ${events} + + +Thin-edge devices support sending large events using legacy api + [Tags] legacy + Execute Command tedge mqtt pub tedge/events/largeEvent2 "$(printf '{"text":"Large event","large_text_field":"%s"}' "$(yes "x" | head -n 100000 | tr -d '\n')")" + ${events}= Device Should Have Event/s expected_text=Large event with_attachment=False minimum=1 maximum=1 type=largeEvent2 fragment=large_text_field + Length Should Be ${events[0]["large_text_field"]} 100000 + Log ${events} + + +Thin-edge devices support sending custom events overriding the type + Execute Command tedge mqtt pub te/device/main///e/myCustomType '{"type": "otherType", "text": "Some test event", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${events}= Device Should Have Event/s expected_text=Some test event with_attachment=False minimum=1 maximum=1 type=otherType fragment=someOtherCustomFragment + Log ${events} + + +Thin-edge devices support sending custom events without type in topic + Execute Command tedge mqtt pub te/device/main///e/ '{"text": "Some test event", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${events}= Device Should Have Event/s expected_text=Some test event with_attachment=False minimum=1 maximum=1 type=ThinEdgeEvent fragment=someOtherCustomFragment + Log ${events} + + +Thin-edge devices support sending custom alarms #1699 + [Tags] \#1699 + Execute Command tedge mqtt pub te/device/main///a/myCustomAlarmType '{ "severity": "critical", "text": "Some test alarm", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=CRITICAL minimum=1 maximum=1 type=myCustomAlarmType + Should Be Equal ${alarms[0]["someOtherCustomFragment"]["nested"]["value"]} extra info + Log ${alarms} + + +Thin-edge devices support sending custom alarms overriding the type + Execute Command tedge mqtt pub te/device/main///a/myCustomAlarmType '{ "severity": "critical", "text": "Some test alarm", "type": "otherType", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=CRITICAL minimum=1 maximum=1 type=otherType + Log ${alarms} + + +Thin-edge devices support sending custom alarms without type in topic + Execute Command tedge mqtt pub te/device/main///a/ '{ "severity": "critical", "text": "Some test alarm", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=CRITICAL minimum=1 maximum=1 type=ThinEdgeAlarm + Log ${alarms} + + +Thin-edge devices support sending custom alarms without severity in payload + Execute Command tedge mqtt pub te/device/main///a/myCustomAlarmType2 '{ "text": "Some test alarm" }' + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=MINOR minimum=1 maximum=1 type=myCustomAlarmType2 + Log ${alarms} + + +Thin-edge devices support sending custom alarms with unknown severity in payload + Execute Command tedge mqtt pub te/device/main///a/myCustomAlarmType3 '{ "severity": "invalid", "text": "Some test alarm", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=MINOR minimum=1 maximum=1 type=myCustomAlarmType3 + Log ${alarms} + + + +Thin-edge devices support sending custom alarms without text in payload + Execute Command tedge mqtt pub te/device/main///a/myCustomAlarmType4 '{ "severity": "major", "someOtherCustomFragment": {"nested":{"value": "extra info"}} }' + ${alarms}= Device Should Have Alarm/s expected_text=myCustomAlarmType4 severity=MAJOR minimum=1 maximum=1 type=myCustomAlarmType4 + Log ${alarms} + + +Thin-edge devices support sending alarms using text fragment + Execute Command tedge mqtt pub te/device/main///a/parentAlarmType1 '{ "severity": "minor", "text": "Some test alarm" }' + Cumulocity.Set Device ${DEVICE_SN} + ${alarms}= Device Should Have Alarm/s expected_text=Some test alarm severity=MINOR minimum=1 maximum=1 type=parentAlarmType1 + Log ${alarms} + + +Thin-edge device supports sending custom Thin-edge device measurements directly to c8y + Execute Command tedge mqtt pub "${C8Y_TOPIC_PREFIX}/measurement/measurements/create" '{"time":"2023-03-20T08:03:56.940907Z","environment":{"temperature":{"value":29.9,"unit":"°C"}},"type":"10min_average","meta":{"sensorLocation":"Brisbane, Australia"}}' + Cumulocity.Set Device ${DEVICE_SN} + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 value=environment series=temperature type=10min_average + Should Be Equal As Numbers ${measurements[0]["environment"]["temperature"]["value"]} 29.9 + Should Be Equal ${measurements[0]["meta"]["sensorLocation"]} Brisbane, Australia + Should Be Equal ${measurements[0]["type"]} 10min_average + + +Thin-edge device support sending inventory data via c8y topic + Execute Command tedge mqtt pub "${C8Y_TOPIC_PREFIX}/inventory/managedObjects/update/${DEVICE_SN}" '{"parentInfo":{"nested":{"name":"complex"}},"subType":"customType"}' + Cumulocity.Set Device ${DEVICE_SN} + ${mo}= Device Should Have Fragments parentInfo subType + Should Be Equal ${mo["parentInfo"]["nested"]["name"]} complex + Should Be Equal ${mo["subType"]} customType + + +Thin-edge device support sending inventory data via tedge topic + Execute Command tedge mqtt pub --retain "te/device/main///twin/device_OS" '{"family":"Debian","version":11,"complex":[1,"2",3],"object":{"foo":"bar"}}' + Cumulocity.Set Device ${DEVICE_SN} + ${mo}= Device Should Have Fragments device_OS + Should Be Equal ${mo["device_OS"]["family"]} Debian + Should Be Equal As Integers ${mo["device_OS"]["version"]} 11 + + Should Be Equal As Integers ${mo["device_OS"]["complex"][0]} 1 + Should Be Equal As Strings ${mo["device_OS"]["complex"][1]} 2 + Should Be Equal As Integers ${mo["device_OS"]["complex"][2]} 3 + Should Be Equal ${mo["device_OS"]["object"]["foo"]} bar + + # Validate clearing of fragments + Execute Command tedge mqtt pub --retain "te/device/main///twin/device_OS" '' + Managed Object Should Not Have Fragments device_OS + + +Thin-edge device supports sending inventory data via tedge topic to root fragments + Execute Command tedge mqtt pub --retain "te/device/main///twin/subtype" '"LinuxDeviceA"' + Execute Command tedge mqtt pub --retain "te/device/main///twin/type" '"ShouldBeIgnored"' + Execute Command tedge mqtt pub --retain "te/device/main///twin/name" '"ShouldBeIgnored"' + Cumulocity.Set Device ${DEVICE_SN} + ${mo}= Device Should Have Fragments subtype + Should Be Equal ${mo["subtype"]} LinuxDeviceA + Should Be Equal ${mo["type"]} thin-edge.io + Should Be Equal ${mo["name"]} ${DEVICE_SN} + + # Validate clearing of fragments + Execute Command tedge mqtt pub --retain "te/device/main///twin/subtype" '' + Managed Object Should Not Have Fragments subtype + + # Validate `name` and `type` can't be cleared + Execute Command tedge mqtt pub --retain "te/device/main///twin/type" '' + Execute Command tedge mqtt pub --retain "te/device/main///twin/name" '' + Sleep 5s reason=Wait a minimum period before checking that the fragment has not changed (as it was previously set) + ${mo}= Device Should Have Fragments type + Should Be Equal ${mo["type"]} thin-edge.io + Should Be Equal ${mo["name"]} ${DEVICE_SN} + + +Previously cleared property should be sent to cloud when set again #2365 + [Tags] \#2365 + Cumulocity.Set Device ${DEVICE_SN} + + # set initial value + Execute Command tedge mqtt pub --retain "te/device/main///twin/subtype" '"LinuxDeviceA"' + Device Should Have Fragment Values subtype\=LinuxDeviceA + + # Clear + Execute Command tedge mqtt pub --retain "te/device/main///twin/subtype" '' + Managed Object Should Not Have Fragments subtype + + # Set to same value prior to clearing it + Execute Command tedge mqtt pub --retain "te/device/main///twin/subtype" '"LinuxDeviceA"' + Device Should Have Fragment Values subtype\=LinuxDeviceA + +# +# Services +# +# measurements +Send measurements to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app1/m/service_type001 '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app1 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=service_type001 + Should Be Equal ${measurements[0]["type"]} service_type001 + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +Send measurements to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app2 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app2 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/main/service/app2/m/service_type002 '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app2 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=service_type002 + Should Be Equal ${measurements[0]["type"]} service_type002 + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +# alarms +Send alarms to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app3/a/alarm_001 '{"text": "test alarm","severity":"major"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app3 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app3 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_001 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_001 + Should Be Equal ${alarms[0]["severity"]} MAJOR + +Send alarms to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app4 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app4 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/main/service/app4/a/alarm_002 '{"text": "test alarm"}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app4 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_002 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_002 + +# events +Send events to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app5/e/event_001 '{"text": "test event"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app5 min_count=1 max_count=1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app5 + Device Should Have Event/s expected_text=test event type=event_001 minimum=1 maximum=1 + +Send events to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app6 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app6 min_count=1 max_count=1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app6 + Execute Command tedge mqtt pub te/device/main/service/app6/e/event_002 '{"text": "test event"}' + Device Should Have Event/s expected_text=test event type=event_002 minimum=1 maximum=1 + +*** Keywords *** + +Custom Setup + ${DEVICE_SN}= Setup + Set Suite Variable $DEVICE_SN + Device Should Exist ${DEVICE_SN} + ThinEdgeIO.Execute Command tedge config set c8y.bridge.built_in true + ThinEdgeIO.Execute Command tedge config set c8y.bridge.topic_prefix custom-c8y-prefix + File Should Exist /etc/tedge/mosquitto-conf/c8y-bridge.conf + ThinEdgeIO.Execute Command tedge reconnect c8y + File Should Not Exist /etc/tedge/mosquitto-conf/c8y-bridge.conf + Service Health Status Should Be Up tedge-mapper-c8y From c471aa8cdf8d34936c6dffd403be69a329b7a9f3 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 15 Mar 2024 10:22:39 +0000 Subject: [PATCH 17/20] Ensure `tedge reconnect c8y` migrates configuration successfully for built-in bridge Signed-off-by: James Rhodes --- .../core/tedge/src/cli/certificate/create.rs | 1 + crates/core/tedge/src/cli/connect/command.rs | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/crates/core/tedge/src/cli/certificate/create.rs b/crates/core/tedge/src/cli/certificate/create.rs index f027942b9e5..03f5a190375 100644 --- a/crates/core/tedge/src/cli/certificate/create.rs +++ b/crates/core/tedge/src/cli/certificate/create.rs @@ -57,6 +57,7 @@ impl CreateCertCmd { let cert = KeyCertPair::new_selfsigned_certificate(config, &self.id, key_kind)?; + // TODO cope with broker user being tedge // Creating files with permission 644 owned by the MQTT broker let mut cert_file = create_new_file(&self.cert_path, crate::BROKER_USER, crate::BROKER_GROUP) diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 0395f20a173..b6090833f35 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -67,9 +67,7 @@ impl Command for ConnectCommand { 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 - if bridge_config.bridge_location == BridgeLocation::BuiltIn - || self.check_if_bridge_exists(&bridge_config) - { + if self.check_if_bridge_exists(&bridge_config) { return match self.check_connection(config) { Ok(DeviceStatus::AlreadyExists) => { let cloud = bridge_config.cloud_name; @@ -103,6 +101,12 @@ impl Command for ConnectCommand { Err(err) => return Err(err.into()), } + if bridge_config.use_mapper && bridge_config.bridge_location == BridgeLocation::BuiltIn { + // If the bridge is built in, the mapper needs to be running with the new configuration + // to be connected + self.start_mapper(); + } + match self.check_connection(config) { Ok(DeviceStatus::AlreadyExists) => { println!("Connection check is successful.\n"); @@ -115,16 +119,10 @@ impl Command for ConnectCommand { } } - if bridge_config.use_mapper { - println!("Checking if tedge-mapper is installed.\n"); - - if which("tedge-mapper").is_err() { - println!("Warning: tedge-mapper is not installed.\n"); - } else { - self.service_manager - .as_ref() - .start_and_enable_service(self.cloud.mapper_service(), std::io::stdout()); - } + if bridge_config.use_mapper && bridge_config.bridge_location == BridgeLocation::Mosquitto { + // If the bridge is in mosquitto, the mapper should only start once the cloud connection + // is verified + self.start_mapper(); } if let Cloud::C8y = self.cloud { @@ -165,7 +163,20 @@ impl ConnectCommand { .join(TEDGE_BRIDGE_CONF_DIR_PATH) .join(br_config.config_file.clone()); - Path::new(&bridge_conf_path).exists() + br_config.bridge_location == BridgeLocation::BuiltIn + || Path::new(&bridge_conf_path).exists() + } + + fn start_mapper(&self) { + println!("Checking if tedge-mapper is installed.\n"); + + if which("tedge-mapper").is_err() { + println!("Warning: tedge-mapper is not installed.\n"); + } else { + self.service_manager + .as_ref() + .start_and_enable_service(self.cloud.mapper_service(), std::io::stdout()); + } } } @@ -445,7 +456,9 @@ fn new_bridge( device_type: &str, ) -> Result<(), ConnectError> { if bridge_config.bridge_location == BridgeLocation::BuiltIn { + println!("Deleting mosquitto bridge configuration in favour of built-in bridge"); clean_up(config_location, bridge_config)?; + restart_mosquitto(bridge_config, service_manager, config_location)?; return Ok(()); } println!("Checking if {} is available.\n", service_manager.name()); @@ -512,6 +525,27 @@ fn restart_mosquitto( config_location: &TEdgeConfigLocation, ) -> Result<(), ConnectError> { println!("Restarting mosquitto service.\n"); + + if let Err(err) = service_manager.stop_service(SystemService::Mosquitto) { + clean_up(config_location, bridge_config)?; + return Err(err.into()); + } + + let (user, group) = match bridge_config.bridge_location { + BridgeLocation::BuiltIn => ("tedge", "tedge"), + BridgeLocation::Mosquitto => (crate::BROKER_USER, crate::BROKER_GROUP), + }; + // Ignore errors - This was the behavior with the now deprecated user manager. + // - When `tedge cert create` is not run as root, a certificate is created but owned by the user running the command. + // - A better approach could be to remove this `chown` and run the command as mosquitto. + for path in [ + &bridge_config.bridge_certfile, + &bridge_config.bridge_keyfile, + ] { + // TODO maybe ignore errors here + tedge_utils::file::change_user_and_group(dbg!(path.as_ref()), user, group).unwrap(); + } + if let Err(err) = service_manager.restart_service(SystemService::Mosquitto) { clean_up(config_location, bridge_config)?; return Err(err.into()); From 8c6a13e7d62564f600a77bc61705a86dd83b0719 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 15 Mar 2024 10:24:13 +0000 Subject: [PATCH 18/20] Allow events topic to vary based on c8y prefix Signed-off-by: James Rhodes --- .../c8y_mapper_ext/src/converter.rs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index cf633e42794..85da0fb1323 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -100,7 +100,7 @@ use tracing::trace; use tracing::warn; const INTERNAL_ALARMS_TOPIC: &str = "c8y-internal/alarms/"; -const C8Y_JSON_MQTT_EVENTS_TOPIC: &str = "c8y/event/events/create"; +const C8Y_JSON_MQTT_EVENTS_TOPIC: &str = "event/events/create"; const TEDGE_AGENT_LOG_DIR: &str = "agent"; const CREATE_EVENT_SMARTREST_CODE: u16 = 400; const DEFAULT_EVENT_TYPE: &str = "ThinEdgeEvent"; @@ -499,7 +499,10 @@ impl CumulocityConverter { } else { // If the message contains extra fields other than `text` and `time`, convert to Cumulocity JSON let cumulocity_event_json = serde_json::to_string(&c8y_event)?; - let json_mqtt_topic = Topic::new_unchecked(C8Y_JSON_MQTT_EVENTS_TOPIC); + let json_mqtt_topic = Topic::new_unchecked(&format!( + "{}/{C8Y_JSON_MQTT_EVENTS_TOPIC}", + self.config.c8y_prefix + )); Message::new(&json_mqtt_topic, cumulocity_event_json) }; @@ -2396,6 +2399,30 @@ pub(crate) mod tests { ); } + #[tokio::test] + async fn convert_event_with_custom_c8y_topic_prefix() { + let tmp_dir = TempTedgeDir::new(); + let mut config = c8y_converter_config(&tmp_dir); + let tedge_config = TEdgeConfig::load_toml_str("service.ty = \"\""); + config.service = tedge_config.service.clone(); + config.c8y_prefix = "custom-topic".into(); + + let (mut converter, _) = create_c8y_converter_from_config(config); + let event_topic = "te/device/main///e/click_event"; + let event_payload = r#"{ "text": "Someone clicked", "time": "2020-02-02T01:02:03+05:30" }"#; + let event_message = Message::new(&Topic::new_unchecked(event_topic), event_payload); + + let converted_events = converter.convert(&event_message).await; + assert_eq!(converted_events.len(), 1); + let converted_event = converted_events.get(0).unwrap(); + assert_eq!(converted_event.topic.name, "custom-topic/s/us"); + + assert_eq!( + converted_event.payload_str().unwrap(), + r#"400,click_event,"Someone clicked",2020-02-02T01:02:03+05:30"# + ); + } + #[tokio::test] async fn convert_event_with_extra_fields_to_c8y_json() { let tmp_dir = TempTedgeDir::new(); From eaad4bd1bb745592d98e49131c4ff4ac814fdf95 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 15 Mar 2024 10:36:18 +0000 Subject: [PATCH 19/20] Restore formatting of test case following accidental clobbering in 466e17 Signed-off-by: James Rhodes --- .../c8y_mapper_ext/src/service_monitor.rs | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs index ba10ab5d3d2..b1b6b051bac 100644 --- a/crates/extensions/c8y_mapper_ext/src/service_monitor.rs +++ b/crates/extensions/c8y_mapper_ext/src/service_monitor.rs @@ -76,60 +76,60 @@ mod tests { use test_case::test_case; #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"pid":"1234","status":"up"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; - "service-monitoring-thin-edge-device" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"pid":"1234","status":"up"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; + "service-monitoring-thin-edge-device" )] #[test_case( - "test_device", - "te/device/child/service/tedge-mapper-c8y/status/health", - r#"{"pid":"1234","status":"up"}"#, - "c8y/s/us/test_device:device:child", - r#"102,test_device:device:child:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; - "service-monitoring-thin-edge-child-device" + "test_device", + "te/device/child/service/tedge-mapper-c8y/status/health", + r#"{"pid":"1234","status":"up"}"#, + "c8y/s/us/test_device:device:child", + r#"102,test_device:device:child:service:tedge-mapper-c8y,service,tedge-mapper-c8y,up"#; + "service-monitoring-thin-edge-child-device" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"pid":"123456"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-thin-edge-no-status" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"pid":"123456"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-thin-edge-no-status" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":""}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-empty-status" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":""}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-empty-status" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - "{}", - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; - "service-monitoring-empty-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + "{}", + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,unknown"#; + "service-monitoring-empty-health-message" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":"up,down"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up,down""#; - "service-monitoring-type-with-comma-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":"up,down"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up,down""#; + "service-monitoring-type-with-comma-health-message" )] #[test_case( - "test_device", - "te/device/main/service/tedge-mapper-c8y/status/health", - r#"{"status":"up\"down"}"#, - "c8y/s/us", - r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up""down""#; - "service-monitoring-double-quotes-health-message" + "test_device", + "te/device/main/service/tedge-mapper-c8y/status/health", + r#"{"status":"up\"down"}"#, + "c8y/s/us", + r#"102,test_device:device:main:service:tedge-mapper-c8y,service,tedge-mapper-c8y,"up""down""#; + "service-monitoring-double-quotes-health-message" )] fn translate_health_status_to_c8y_service_monitoring_message( device_name: &str, From dcd4329da79e734d02ba918b00a0d012c1b5d8ad Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 15 Mar 2024 14:29:14 +0000 Subject: [PATCH 20/20] Add operation test and connectivity check in telemetry test Signed-off-by: James Rhodes --- .../shell_operation_built-in_bridge.robot | 43 +++++++++++++++++++ ...dge_device_telemetry_built-in_bridge.robot | 2 + 2 files changed, 45 insertions(+) create mode 100644 tests/RobotFramework/tests/cumulocity/shell/shell_operation_built-in_bridge.robot diff --git a/tests/RobotFramework/tests/cumulocity/shell/shell_operation_built-in_bridge.robot b/tests/RobotFramework/tests/cumulocity/shell/shell_operation_built-in_bridge.robot new file mode 100644 index 00000000000..48ca3be0d6e --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/shell/shell_operation_built-in_bridge.robot @@ -0,0 +1,43 @@ +*** Settings *** +Resource ../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Tags theme:c8y theme:troubleshooting theme:plugins +Suite Setup Custom Setup +Test Teardown Get Logs + +*** Test Cases *** + +Successful shell command with output + ${operation}= Cumulocity.Execute Shell Command echo helloworld + Operation Should Be SUCCESSFUL ${operation} + Should Be Equal ${operation.to_json()["c8y_Command"]["result"]} helloworld\n + +Check Successful shell command with literal double quotes output + ${operation}= Cumulocity.Execute Shell Command echo \\"helloworld\\" + Operation Should Be SUCCESSFUL ${operation} + Should Be Equal ${operation.to_json()["c8y_Command"]["result"]} "helloworld"\n + +Execute multiline shell command + ${operation}= Cumulocity.Execute Shell Command echo "hello"${\n}echo "world" + Operation Should Be SUCCESSFUL ${operation} + Should Be Equal ${operation.to_json()["c8y_Command"]["result"]} hello\nworld\n + +Failed shell command + ${operation}= Cumulocity.Execute Shell Command exit 1 + Operation Should Be FAILED ${operation} + + +*** Keywords *** + +Custom Setup + ${DEVICE_SN}= Setup + Set Suite Variable $DEVICE_SN + Device Should Exist ${DEVICE_SN} + ThinEdgeIO.Execute Command tedge config set c8y.bridge.built_in true + ThinEdgeIO.Execute Command tedge config set c8y.bridge.topic_prefix custom-c8y + ThinEdgeIO.Transfer To Device ${CURDIR}/command_handler.* /etc/tedge/operations/command + ThinEdgeIO.Transfer To Device ${CURDIR}/c8y_Command* /etc/tedge/operations/c8y/ + ThinEdgeIO.Restart Service tedge-agent + ThinEdgeIO.Execute Command tedge reconnect c8y diff --git a/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot index 0dc67570603..371b9f20741 100644 --- a/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot +++ b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry_built-in_bridge.robot @@ -276,3 +276,5 @@ Custom Setup ThinEdgeIO.Execute Command tedge reconnect c8y File Should Not Exist /etc/tedge/mosquitto-conf/c8y-bridge.conf Service Health Status Should Be Up tedge-mapper-c8y + ${output}= Execute Command sudo tedge connect c8y --test + Should Contain ${output} Connection check to c8y cloud is successful.