From dfca3329302d6ac7ed52f7573c0d4dbffa786a80 Mon Sep 17 00:00:00 2001 From: Marcel Guzik Date: Thu, 7 Dec 2023 08:51:13 +0000 Subject: [PATCH 1/2] Add operation file cache to agent Signed-off-by: Marcel Guzik --- Cargo.lock | 1 + .../src/tedge_config_cli/tedge_config.rs | 2 +- crates/core/c8y_api/src/http_proxy.rs | 2 +- crates/core/tedge_agent/Cargo.toml | 1 + crates/core/tedge_agent/src/agent.rs | 50 ++- crates/core/tedge_agent/src/lib.rs | 1 + .../src/operation_file_cache/mod.rs | 360 ++++++++++++++++++ crates/core/tedge_api/src/messages.rs | 3 +- crates/extensions/c8y_auth_proxy/src/url.rs | 1 + crates/extensions/c8y_mapper_ext/src/actor.rs | 54 +-- .../c8y_mapper_ext/src/converter.rs | 10 +- .../src/operations/config_update.rs | 287 ++------------ .../tedge_config_manager/src/actor.rs | 11 +- 13 files changed, 455 insertions(+), 328 deletions(-) create mode 100644 crates/core/tedge_agent/src/operation_file_cache/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7f3f1a8a405..7bb9c3e69b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3658,6 +3658,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "sha256", "tedge_actors", "tedge_api", "tedge_config", 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 4fa1c84c824..cfba6ce0478 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 @@ -645,7 +645,7 @@ define_tedge_config! { #[doku(as = "PathBuf")] key_path: Utf8PathBuf, - /// Path to a file containing the PEM encoded CA certificates that are + /// Path to a directory containing the PEM encoded CA certificates that are /// trusted when checking incoming client certificates for the File Transfer Service #[tedge_config(example = "/etc/ssl/certs")] #[doku(as = "PathBuf")] diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index 0251d307420..fe7aeed9153 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -20,7 +20,7 @@ pub enum C8yEndPointError { } /// Define a C8y endpoint -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct C8yEndPoint { c8y_host: String, pub device_id: String, diff --git a/crates/core/tedge_agent/Cargo.toml b/crates/core/tedge_agent/Cargo.toml index aea2acbd424..463cc9c02b4 100644 --- a/crates/core/tedge_agent/Cargo.toml +++ b/crates/core/tedge_agent/Cargo.toml @@ -29,6 +29,7 @@ reqwest = { workspace = true } rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +sha256 = { workspace = true } tedge_actors = { workspace = true } tedge_api = { workspace = true } tedge_config = { workspace = true } diff --git a/crates/core/tedge_agent/src/agent.rs b/crates/core/tedge_agent/src/agent.rs index 1136d3b44cb..bf8553a9f1d 100644 --- a/crates/core/tedge_agent/src/agent.rs +++ b/crates/core/tedge_agent/src/agent.rs @@ -1,5 +1,6 @@ use crate::file_transfer_server::actor::FileTransferServerBuilder; use crate::file_transfer_server::actor::FileTransferServerConfig; +use crate::operation_file_cache::FileCacheActorBuilder; use crate::restart_manager::builder::RestartManagerBuilder; use crate::restart_manager::config::RestartManagerConfig; use crate::software_manager::builder::SoftwareManagerBuilder; @@ -75,6 +76,7 @@ pub(crate) struct AgentConfig { pub mqtt_topic_root: Arc, pub service_type: String, pub identity: Option, + pub fts_url: Arc, pub is_sudo_enabled: bool, pub capabilities: Capabilities, } @@ -145,6 +147,11 @@ impl AgentConfig { config_snapshot: tedge_config.agent.enable.config_snapshot, log_upload: tedge_config.agent.enable.log_upload, }; + let fts_url = format!( + "{}:{}", + tedge_config.http.client.host, tedge_config.http.client.port + ) + .into(); Ok(Self { mqtt_config, @@ -162,6 +169,7 @@ impl AgentConfig { mqtt_device_topic_id, service_type: tedge_config.service.ty.clone(), identity, + fts_url, is_sudo_enabled, capabilities, }) @@ -295,7 +303,7 @@ impl Agent { let log_manager_config = LogManagerConfig::from_options(LogManagerOptions { config_dir: self.config.config_dir.clone().into(), tmp_dir: self.config.config_dir.into(), - mqtt_schema, + mqtt_schema: mqtt_schema.clone(), mqtt_device_topic_id: self.config.mqtt_device_topic_id.clone(), })?; Some( @@ -311,6 +319,30 @@ impl Agent { None }; + // TODO: replace with a call to entity store when we stop assuming default MQTT schema + let is_main_device = + self.config.mqtt_device_topic_id == EntityTopicId::default_main_device(); + if is_main_device { + info!("Running as a main device, starting tedge_to_te_converter and File Transfer Service"); + + runtime.spawn(tedge_to_te_converter).await?; + + let file_transfer_server_builder = + FileTransferServerBuilder::try_bind(self.config.http_config).await?; + runtime.spawn(file_transfer_server_builder).await?; + + let operation_file_cache_builder = FileCacheActorBuilder::new( + mqtt_schema, + self.config.fts_url.clone(), + self.config.data_dir, + &mut downloader_actor_builder, + &mut mqtt_actor_builder, + ); + runtime.spawn(operation_file_cache_builder).await?; + } else { + info!("Running as a child device, tedge_to_te_converter and File Transfer Service disabled"); + } + // Spawn all runtime.spawn(signal_actor_builder).await?; runtime.spawn(mqtt_actor_builder).await?; @@ -329,22 +361,6 @@ impl Agent { runtime.spawn(converter_actor_builder).await?; runtime.spawn(health_actor).await?; - // TODO: replace with a call to entity store when we stop assuming default MQTT schema - let is_main_device = - self.config.mqtt_device_topic_id == EntityTopicId::default_main_device(); - if is_main_device { - info!( - "Running as a main device, starting tedge_to_te_converter and file transfer actors" - ); - - let file_transfer_server_builder = - FileTransferServerBuilder::try_bind(self.config.http_config).await?; - runtime.spawn(tedge_to_te_converter).await?; - runtime.spawn(file_transfer_server_builder).await?; - } else { - info!("Running as a child device, tedge_to_te_converter and file transfer actors disabled"); - } - runtime.run_to_completion().await?; Ok(()) diff --git a/crates/core/tedge_agent/src/lib.rs b/crates/core/tedge_agent/src/lib.rs index 8526aa5e306..8911f220c77 100644 --- a/crates/core/tedge_agent/src/lib.rs +++ b/crates/core/tedge_agent/src/lib.rs @@ -22,6 +22,7 @@ use tracing::log::warn; mod agent; mod file_transfer_server; +mod operation_file_cache; mod restart_manager; mod software_manager; mod state_repository; diff --git a/crates/core/tedge_agent/src/operation_file_cache/mod.rs b/crates/core/tedge_agent/src/operation_file_cache/mod.rs new file mode 100644 index 00000000000..d7f8922f2f1 --- /dev/null +++ b/crates/core/tedge_agent/src/operation_file_cache/mod.rs @@ -0,0 +1,360 @@ +//! Inspects incoming operation requests for URLs to files and downloads them into the File Transfer +//! Service, so that they can be downloaded more quickly by the child devices. +//! +//! Payloads of some operations contain `remoteUrl` property which contains a URL from which we +//! need to download a file. However, child devices may have reduced speed or even may not be able +//! to reach the remote URL at all. Additionally, multiple child devices may require the same file, +//! so it makes sense to download it once and place it inside the File Transfer Service, so that +//! child devices may download the files quickly on the local network. +//! +//! This actor, for all child devices, for operations that have `remoteUrl` property, tries to +//! download the file from this URL, places it in the File Transfer Service, and inserts the URL to +//! download the file from the FTS in the `tedgeUrl` property. + +use async_trait::async_trait; +use camino::Utf8PathBuf; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use tedge_actors::adapt; +use tedge_actors::fan_in_message_type; +use tedge_actors::Actor; +use tedge_actors::Builder; +use tedge_actors::DynSender; +use tedge_actors::LinkError; +use tedge_actors::LoggingReceiver; +use tedge_actors::MessageReceiver; +use tedge_actors::MessageSink; +use tedge_actors::NoConfig; +use tedge_actors::RuntimeError; +use tedge_actors::RuntimeRequest; +use tedge_actors::RuntimeRequestSink; +use tedge_actors::Sender; +use tedge_actors::ServiceProvider; +use tedge_actors::SimpleMessageBoxBuilder; +use tedge_api::messages::ConfigUpdateCmdPayload; +use tedge_api::mqtt_topics::Channel; +use tedge_api::mqtt_topics::ChannelFilter; +use tedge_api::mqtt_topics::EntityFilter; +use tedge_api::mqtt_topics::EntityTopicId; +use tedge_api::mqtt_topics::MqttSchema; +use tedge_api::mqtt_topics::OperationType; +use tedge_api::path::DataDir; +use tedge_api::CommandStatus; +use tedge_downloader_ext::DownloadRequest; +use tedge_downloader_ext::DownloadResult; +use tedge_mqtt_ext::MqttMessage; +use tedge_mqtt_ext::Topic; +use tedge_mqtt_ext::TopicFilter; +use tracing::error; +use tracing::info; +use tracing::warn; + +type IdDownloadRequest = (String, DownloadRequest); +type IdDownloadResult = (String, DownloadResult); + +fan_in_message_type!(FileCacheInput[MqttMessage, IdDownloadResult]: Debug); + +pub struct FileCacheActor { + input_receiver: LoggingReceiver, + mqtt_sender: DynSender, + downloader_sender: DynSender, + + tedge_http_host: Arc, + mqtt_schema: MqttSchema, + data_dir: DataDir, + + pending_operations: HashMap, +} + +#[async_trait] +impl Actor for FileCacheActor { + fn name(&self) -> &str { + "FileCacheActor" + } + + async fn run(mut self) -> Result<(), RuntimeError> { + while let Some(message) = self.input_receiver.recv().await { + match message { + FileCacheInput::MqttMessage(message) => self.process_mqtt_message(message).await?, + FileCacheInput::IdDownloadResult(message) => self.process_download(message).await?, + } + } + + Ok(()) + } +} + +impl FileCacheActor { + async fn process_mqtt_message( + &mut self, + mqtt_message: MqttMessage, + ) -> Result<(), RuntimeError> { + let Ok((entity, channel)) = self.mqtt_schema.entity_channel_of(&mqtt_message.topic) else { + return Ok(()); + }; + + let Channel::Command { + operation: OperationType::ConfigUpdate, + cmd_id, + } = channel + else { + return Ok(()); + }; + + let update_payload = + match serde_json::from_slice::(mqtt_message.payload.as_bytes()) + { + Ok(payload) => payload, + Err(err) => { + warn!("Received config update, but payload is malformed: {err}"); + return Ok(()); + } + }; + + if update_payload.remote_url.is_empty() || update_payload.tedge_url.is_some() { + return Ok(()); + } + + match &update_payload.status { + CommandStatus::Executing => { + self.download_config_file_to_cache(&mqtt_message.topic, &update_payload) + .await?; + } + CommandStatus::Successful => self.delete_symlink_for_config_update( + &entity, + &update_payload.config_type, + &cmd_id, + )?, + CommandStatus::Failed { .. } => self.delete_symlink_for_config_update( + &entity, + &update_payload.config_type, + &cmd_id, + )?, + _ => {} + } + + Ok(()) + } + + async fn process_download(&mut self, download: IdDownloadResult) -> Result<(), RuntimeError> { + let (topic, result) = download; + + let Some(mut operation) = self.pending_operations.remove(&topic) else { + return Ok(()); + }; + + let (entity, Channel::Command { cmd_id, .. }) = self + .mqtt_schema + .entity_channel_of(&Topic::new(&topic).unwrap()) + .expect("only topics targeting config update command should be inserted") + else { + return Ok(()); + }; + + let download = match result { + // if cant download file, operation failed + Err(err) => { + let error_message = format!("tedge-agent failed downloading a file: {err}"); + operation.failed(&error_message); + error!("{}", error_message); + let message = MqttMessage::new( + &Topic::new_unchecked(&topic), + serde_json::to_string(&operation).unwrap(), + ); + self.mqtt_sender.send(message).await?; + return Ok(()); + } + Ok(download) => download, + }; + + self.create_symlink_for_config_update( + &entity, + &operation.config_type, + &cmd_id, + download.file_path, + )?; + + let url_symlink_path = symlink_path(&entity, &operation.config_type, &cmd_id); + + let tedge_url = format!( + "http://{}/tedge/file-transfer/{}", + &self.tedge_http_host, url_symlink_path + ); + + operation.tedge_url = Some(tedge_url); + + let mqtt_message = MqttMessage::new( + &Topic::new(&topic).unwrap(), + serde_json::to_string(&operation).unwrap(), + ); + self.mqtt_sender.send(mqtt_message).await.unwrap(); + + Ok(()) + } + + async fn download_config_file_to_cache( + &mut self, + config_update_topic: &Topic, + config_update_payload: &ConfigUpdateCmdPayload, + ) -> Result<(), RuntimeError> { + let remote_url = &config_update_payload.remote_url; + + let file_cache_key = sha256::digest(remote_url); + let dest_path = self.data_dir.cache_dir().join(file_cache_key); + let topic = config_update_topic.name.clone(); + + info!("Downloading config file from `{remote_url}` to cache"); + + let download_request = DownloadRequest::new(remote_url, dest_path.as_std_path()); + + self.pending_operations.insert( + config_update_topic.name.clone(), + config_update_payload.clone(), + ); + + self.downloader_sender + .send((topic, download_request)) + .await?; + + Ok(()) + } + + fn create_symlink_for_config_update( + &self, + entity_topic_id: &EntityTopicId, + config_type: &str, + cmd_id: &str, + original: impl AsRef, + ) -> Result<(), RuntimeError> { + let symlink_path = self.fs_file_transfer_symlink_path(entity_topic_id, config_type, cmd_id); + + if !symlink_path.is_symlink() { + std::fs::create_dir_all(symlink_path.parent().unwrap()) + .and_then(|_| std::os::unix::fs::symlink(original, &symlink_path)) + .map_err(|e| RuntimeError::ActorError(e.into()))?; + } + + Ok(()) + } + + fn delete_symlink_for_config_update( + &self, + entity_topic_id: &EntityTopicId, + config_type: &str, + cmd_id: &str, + ) -> Result<(), RuntimeError> { + let symlink_path = self.fs_file_transfer_symlink_path(entity_topic_id, config_type, cmd_id); + + if let Err(e) = std::fs::remove_file(symlink_path) { + if e.kind() != std::io::ErrorKind::NotFound { + // we're missing permissions or trying to delete a directory + return Err(RuntimeError::ActorError(e.into()))?; + } + } + + Ok(()) + } + + fn fs_file_transfer_symlink_path( + &self, + entity_topic_id: &EntityTopicId, + config_type: &str, + cmd_id: &str, + ) -> Utf8PathBuf { + let symlink_dir_path = self.data_dir.file_transfer_dir(); + + symlink_dir_path.join(symlink_path(entity_topic_id, config_type, cmd_id)) + } +} + +fn symlink_path(entity_topic_id: &EntityTopicId, config_type: &str, cmd_id: &str) -> Utf8PathBuf { + Utf8PathBuf::from(entity_topic_id.as_str().replace('/', "_")) + .join("config_update") + .join(format!("{}-{cmd_id}", config_type.replace('/', ":"))) +} + +pub struct FileCacheActorBuilder { + message_box: SimpleMessageBoxBuilder, + mqtt_sender: DynSender, + download_sender: DynSender, + + mqtt_schema: MqttSchema, + tedge_http_host: Arc, + data_dir: DataDir, +} + +impl FileCacheActorBuilder { + pub fn new( + mqtt_schema: MqttSchema, + tedge_http_host: Arc, + data_dir: DataDir, + downloader_actor: &mut impl ServiceProvider, + mqtt_actor: &mut impl ServiceProvider, + ) -> Self { + let message_box = SimpleMessageBoxBuilder::new("RestartManager", 10); + + let download_sender = + downloader_actor.connect_consumer(NoConfig, adapt(&message_box.get_sender())); + + let mqtt_sender = mqtt_actor.connect_consumer( + Self::subscriptions(&mqtt_schema), + adapt(&message_box.get_sender()), + ); + + Self { + message_box, + mqtt_sender, + download_sender, + mqtt_schema, + tedge_http_host, + data_dir, + } + } + + fn subscriptions(mqtt_schema: &MqttSchema) -> TopicFilter { + mqtt_schema.topics( + EntityFilter::AnyEntity, + ChannelFilter::Command(OperationType::ConfigUpdate), + ) + } +} + +impl MessageSink for FileCacheActorBuilder { + fn get_config(&self) -> NoConfig { + NoConfig + } + + fn get_sender(&self) -> DynSender { + self.message_box.get_sender() + } +} + +impl RuntimeRequestSink for FileCacheActorBuilder { + fn get_signal_sender(&self) -> DynSender { + self.message_box.get_signal_sender() + } +} + +impl Builder for FileCacheActorBuilder { + type Error = LinkError; + + fn try_build(self) -> Result { + Ok(self.build()) + } + + fn build(self) -> FileCacheActor { + let (_, rx) = self.message_box.build().into_split(); + FileCacheActor { + mqtt_sender: self.mqtt_sender, + downloader_sender: self.download_sender, + input_receiver: rx, + + tedge_http_host: self.tedge_http_host, + mqtt_schema: self.mqtt_schema, + data_dir: self.data_dir, + + pending_operations: HashMap::new(), + } + } +} diff --git a/crates/core/tedge_api/src/messages.rs b/crates/core/tedge_api/src/messages.rs index 92be1d86f77..4c3712a6930 100644 --- a/crates/core/tedge_api/src/messages.rs +++ b/crates/core/tedge_api/src/messages.rs @@ -680,7 +680,8 @@ impl ConfigSnapshotCmdPayload { pub struct ConfigUpdateCmdPayload { #[serde(flatten)] pub status: CommandStatus, - pub tedge_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tedge_url: Option, pub remote_url: String, #[serde(rename = "type")] pub config_type: String, diff --git a/crates/extensions/c8y_auth_proxy/src/url.rs b/crates/extensions/c8y_auth_proxy/src/url.rs index e32f982a502..63197b8f0ab 100644 --- a/crates/extensions/c8y_auth_proxy/src/url.rs +++ b/crates/extensions/c8y_auth_proxy/src/url.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use tedge_config::TEdgeConfig; +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ProxyUrlGenerator { host: Arc, port: u16, diff --git a/crates/extensions/c8y_mapper_ext/src/actor.rs b/crates/extensions/c8y_mapper_ext/src/actor.rs index 7d4db8d6a23..dfbb573889c 100644 --- a/crates/extensions/c8y_mapper_ext/src/actor.rs +++ b/crates/extensions/c8y_mapper_ext/src/actor.rs @@ -3,7 +3,6 @@ use super::converter::CumulocityConverter; use super::dynamic_discovery::process_inotify_events; use crate::operations::FtsDownloadOperationType; use async_trait::async_trait; -use c8y_api::smartrest::smartrest_deserializer::SmartRestOperationVariant; use c8y_api::smartrest::smartrest_serializer::fail_operation; use c8y_api::smartrest::smartrest_serializer::succeed_static_operation; use c8y_api::smartrest::smartrest_serializer::CumulocitySupportedOperations; @@ -284,45 +283,28 @@ impl C8yMapperActor { cmd_id: CmdId, result: DownloadResult, ) -> Result<(), RuntimeError> { - let operation_result = match self.converter.pending_download_operations.remove(&cmd_id) { - None => { - // download not from c8y_proxy, check if it was from FTS - if let Some(fts_download) = self - .converter - .pending_fts_download_operations - .remove(&cmd_id) - { - let cmd_id = cmd_id.clone(); - match fts_download.download_type { - FtsDownloadOperationType::ConfigDownload => { - self.converter - .handle_fts_config_download_result(cmd_id, result, fts_download) - .await - } - FtsDownloadOperationType::LogDownload => { - self.converter - .handle_fts_log_download_result(cmd_id, result, fts_download) - .await - } - } - } else { - error!("Received a download result for the unknown command ID: {cmd_id}"); - return Ok(()); + // download not from c8y_proxy, check if it was from FTS + let operation_result = if let Some(fts_download) = self + .converter + .pending_fts_download_operations + .remove(&cmd_id) + { + let cmd_id = cmd_id.clone(); + match fts_download.download_type { + FtsDownloadOperationType::ConfigDownload => { + self.converter + .handle_fts_config_download_result(cmd_id, result, fts_download) + .await } - } - - Some(operation) => match operation { - SmartRestOperationVariant::DownloadConfigFile(smartrest) => { + FtsDownloadOperationType::LogDownload => { self.converter - .process_download_result_for_config_update( - cmd_id.into(), - &smartrest, - result, - ) + .handle_fts_log_download_result(cmd_id, result, fts_download) .await } - _other_types => return Ok(()), // unsupported - }, + } + } else { + error!("Received a download result for the unknown command ID: {cmd_id}"); + return Ok(()); }; match operation_result { diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index 0a8ff74ff22..501250c2245 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -30,7 +30,6 @@ use c8y_api::smartrest::operations::get_operations; use c8y_api::smartrest::operations::Operations; use c8y_api::smartrest::operations::ResultFormat; use c8y_api::smartrest::smartrest_deserializer::AvailableChildDevices; -use c8y_api::smartrest::smartrest_deserializer::SmartRestOperationVariant; use c8y_api::smartrest::smartrest_deserializer::SmartRestRequestGeneric; use c8y_api::smartrest::smartrest_deserializer::SmartRestRestartRequest; use c8y_api::smartrest::smartrest_deserializer::SmartRestUpdateSoftware; @@ -202,7 +201,6 @@ pub struct CumulocityConverter { pub uploader_sender: LoggingSender, pub downloader_sender: LoggingSender, pub pending_upload_operations: HashMap, - pub pending_download_operations: HashMap, /// Used to store pending downloads from the FTS. // Using a separate field to not mix downloads from FTS and HTTP proxy @@ -293,7 +291,6 @@ impl CumulocityConverter { uploader_sender, downloader_sender, pending_upload_operations: HashMap::new(), - pending_download_operations: HashMap::new(), pending_fts_download_operations: HashMap::new(), command_id, }) @@ -3293,16 +3290,11 @@ pub(crate) mod tests { let service_type = "service".into(); let c8y_host = "test.c8y.io".into(); let tedge_http_host = "localhost".into(); - let mqtt_schema = MqttSchema::default(); let auth_proxy_addr = "127.0.0.1".into(); let auth_proxy_port = 8001; let auth_proxy_protocol = Protocol::Http; - let mut topics = + let topics = C8yMapperConfig::default_internal_topic_filter(&tmp_dir.to_path_buf()).unwrap(); - topics.add_all(crate::operations::log_upload::log_upload_topic_filter( - &mqtt_schema, - )); - topics.add_all(C8yMapperConfig::default_external_topic_filter()); C8yMapperConfig::new( tmp_dir.to_path_buf(), 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 245b058b19d..84f1ddba3ea 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs @@ -2,18 +2,12 @@ use crate::converter::CumulocityConverter; use crate::error::ConversionError; use crate::error::CumulocityMapperError; use c8y_api::smartrest::smartrest_deserializer::SmartRestConfigDownloadRequest; -use c8y_api::smartrest::smartrest_deserializer::SmartRestOperationVariant; use c8y_api::smartrest::smartrest_deserializer::SmartRestRequestGeneric; use c8y_api::smartrest::smartrest_serializer::fail_operation; use c8y_api::smartrest::smartrest_serializer::set_operation_executing; use c8y_api::smartrest::smartrest_serializer::succeed_operation_no_payload; use c8y_api::smartrest::smartrest_serializer::CumulocitySupportedOperations; -use std::fs; -use std::io; -use std::os::unix::fs as unix_fs; -use std::path::Path; use std::sync::Arc; -use tedge_actors::Sender; use tedge_api::entity_store::EntityMetadata; use tedge_api::messages::CommandStatus; use tedge_api::messages::ConfigUpdateCmdPayload; @@ -24,12 +18,9 @@ use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::mqtt_topics::MqttSchema; use tedge_api::mqtt_topics::OperationType; use tedge_api::Jsonify; -use tedge_downloader_ext::DownloadRequest; -use tedge_downloader_ext::DownloadResult; use tedge_mqtt_ext::Message; use tedge_mqtt_ext::QoS; use tedge_mqtt_ext::TopicFilter; -use tracing::info; use tracing::log::warn; pub fn topic_filter(mqtt_schema: &MqttSchema) -> TopicFilter { @@ -48,60 +39,14 @@ pub fn topic_filter(mqtt_schema: &MqttSchema) -> TopicFilter { } impl CumulocityConverter { - /// This function is called after DownloaderActor returns the result. - /// If the result is - /// - Ok, create a new config_update command. - /// - Err, create SmartREST Executing and Failed messages to move the operation to end state. - pub async fn process_download_result_for_config_update( - &mut self, - cmd_id: Arc, - smartrest: &SmartRestConfigDownloadRequest, - download_result: DownloadResult, - ) -> Result, ConversionError> { - let target = self - .entity_store - .try_get_by_external_id(&smartrest.device.clone().into())?; - - match download_result { - Ok(download_response) => { - self.create_symlink_for_config_update( - target, - &smartrest.config_type, - &cmd_id, - download_response.file_path, - )?; - let message = self.create_config_update_cmd(cmd_id, smartrest, target); - Ok(message) - } - Err(download_err) => { - let sm_topic = self.smartrest_publish_topic_for_entity(&target.topic_id)?; - let smartrest_executing = - set_operation_executing(CumulocitySupportedOperations::C8yDownloadConfigFile); - let smartrest_failed = fail_operation( - CumulocitySupportedOperations::C8yDownloadConfigFile, - &format!( - "Download from {} failed with {}", - smartrest.url, download_err - ), - ); - - Ok(vec![ - Message::new(&sm_topic, smartrest_executing), - Message::new(&sm_topic, smartrest_failed), - ]) - } - } - } - /// Address a received ThinEdge config_update command. If its status is /// - "executing", it converts the message to SmartREST "Executing". /// - "successful", it converts the message to SmartREST "Successful". /// - "failed", it converts the message to SmartREST "Failed". - /// Remove the symlink when the status is either successful or failed. pub async fn handle_config_update_state_change( &mut self, topic_id: &EntityTopicId, - cmd_id: &str, + _cmd_id: &str, message: &Message, ) -> Result, ConversionError> { if !self.config.capabilities.config_update { @@ -109,7 +54,6 @@ impl CumulocityConverter { return Ok(vec![]); } - let target = self.entity_store.try_get(topic_id)?; let sm_topic = self.smartrest_publish_topic_for_entity(topic_id)?; let payload = message.payload_str()?; let response = &ConfigUpdateCmdPayload::from_json(payload)?; @@ -130,8 +74,6 @@ impl CumulocityConverter { .with_retain() .with_qos(QoS::AtLeastOnce); - self.delete_symlink_for_config_update(target, &response.config_type, cmd_id)?; - vec![c8y_notification, clear_local_cmd] } CommandStatus::Failed { reason } => { @@ -142,8 +84,6 @@ impl CumulocityConverter { .with_retain() .with_qos(QoS::AtLeastOnce); - self.delete_symlink_for_config_update(target, &response.config_type, cmd_id)?; - vec![c8y_notification, clear_local_cmd] } _ => { @@ -154,10 +94,8 @@ impl CumulocityConverter { Ok(messages) } - /// Upon receiving a SmartREST c8y_DownloadConfigFile request, - /// - Create a download request if the target file is not available in cache. - /// - If the file is already available, proceed to create a new ThinEdge config_update command. - /// Command ID is generated here, but it should be replaced by c8y's operation ID in the future. + /// Upon receiving a SmartREST c8y_DownloadConfigFile request, convert it to a message on the + /// command channel. pub async fn convert_config_update_request( &mut self, smartrest: &str, @@ -168,47 +106,9 @@ impl CumulocityConverter { .try_get_by_external_id(&smartrest.device.clone().into())?; let cmd_id = self.command_id.new_id(); - let remote_url = smartrest.url.as_str(); - let file_cache_key = sha256::digest(remote_url); - let file_cache_path = self.config.data_dir.cache_dir().join(file_cache_key); - - if file_cache_path.is_file() { - // No download. Create a symlink and config_update command. - info!("Hit the file cache={file_cache_path}. Create a symlink to the file"); - self.create_symlink_for_config_update( - target, - &smartrest.config_type, - &cmd_id, - file_cache_path, - )?; - - let message = self.create_config_update_cmd(cmd_id.into(), &smartrest, target); - Ok(message) - } else { - // Require file download - // Send a request to the Downloader to download the file asynchronously. - let download_request = - if let Some(cumulocity_url) = self.c8y_endpoint.maybe_tenant_url(remote_url) { - DownloadRequest::new( - self.auth_proxy.proxy_url(cumulocity_url).as_ref(), - file_cache_path.as_std_path(), - ) - } else { - DownloadRequest::new(remote_url, file_cache_path.as_std_path()) - }; - - self.downloader_sender - .send((cmd_id.clone(), download_request)) - .await?; - info!("Awaiting config download for cmd_id: {cmd_id} from url: {remote_url}"); - - self.pending_download_operations.insert( - cmd_id, - SmartRestOperationVariant::DownloadConfigFile(smartrest), - ); - - Ok(vec![]) - } + + let message = self.create_config_update_cmd(cmd_id.into(), &smartrest, target); + Ok(message) } /// Converts a config_update metadata message to @@ -237,21 +137,18 @@ impl CumulocityConverter { cmd_id: cmd_id.to_string(), }; let topic = self.mqtt_schema.topic_for(&target.topic_id, &channel); - let external_id: String = target.external_id.clone().into(); - // Replace '/' with ':' to avoid creating unexpected directories in file transfer repo - let tedge_url = format!( - "http://{}/tedge/file-transfer/{}/config_update/{}-{}", - &self.config.tedge_http_host, - external_id, - smartrest.config_type.replace('/', ":"), - cmd_id - ); + let proxy_url = self + .c8y_endpoint + .maybe_tenant_url(&smartrest.url) + .map(|cumulocity_url| self.auth_proxy.proxy_url(cumulocity_url).into()); + + let remote_url = proxy_url.unwrap_or(smartrest.url.to_string()); let request = ConfigUpdateCmdPayload { status: CommandStatus::Init, - tedge_url, - remote_url: smartrest.url.clone(), + tedge_url: None, + remote_url, config_type: smartrest.config_type.clone(), path: None, }; @@ -259,70 +156,22 @@ impl CumulocityConverter { // Command messages must be retained vec![Message::new(&topic, request.to_json()).with_retain()] } - - fn create_symlink_for_config_update( - &self, - target: &EntityMetadata, - config_type: &str, - cmd_id: &str, - original: impl AsRef, - ) -> Result<(), io::Error> { - let symlink_dir_path = self - .config - .data_dir - .file_transfer_dir() - .join(target.external_id.as_ref()) - .join("config_update"); - let symlink_path = - symlink_dir_path.join(format!("{}-{cmd_id}", config_type.replace('/', ":"))); - - if !symlink_path.is_symlink() { - fs::create_dir_all(symlink_dir_path)?; - unix_fs::symlink(original, &symlink_path)?; - } - - Ok(()) - } - - fn delete_symlink_for_config_update( - &self, - target: &EntityMetadata, - config_type: &str, - cmd_id: &str, - ) -> Result<(), io::Error> { - let symlink_dir_path = self - .config - .data_dir - .file_transfer_dir() - .join(target.external_id.as_ref()) - .join("config_update"); - let symlink_path = - symlink_dir_path.join(format!("{}-{cmd_id}", config_type.replace('/', ":"))); - - if symlink_path.exists() { - fs::remove_file(symlink_path)? - } - - Ok(()) - } } #[cfg(test)] mod tests { - use std::time::Duration; - use crate::tests::skip_init_messages; use crate::tests::spawn_c8y_mapper_actor; use c8y_api::smartrest::topic::C8yTopic; use serde_json::json; use sha256::digest; + use std::time::Duration; use tedge_actors::test_helpers::MessageReceiverExt; use tedge_actors::MessageReceiver; use tedge_actors::Sender; use tedge_api::mqtt_topics::Channel; use tedge_api::mqtt_topics::MqttSchema; use tedge_api::mqtt_topics::OperationType; - use tedge_downloader_ext::DownloadResponse; use tedge_mqtt_ext::test_helpers::assert_received_contains_str; use tedge_mqtt_ext::MqttMessage; use tedge_mqtt_ext::Topic; @@ -331,80 +180,6 @@ mod tests { const TEST_TIMEOUT_MS: Duration = Duration::from_millis(5000); #[tokio::test] - #[ignore] - async fn mapper_converts_smartrest_config_download_req_with_new_download_for_main_device() { - let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, mut dl) = spawn_c8y_mapper_actor(&ttd, true).await; - let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); - - skip_init_messages(&mut mqtt).await; - - // Simulate c8y_DownloadConfigFile SmartREST request - mqtt.send(MqttMessage::new( - &C8yTopic::downstream_topic(), - "524,test-device,http://www.my.url,path/config/A", - )) - .await - .expect("Send failed"); - - // Assert download request - let download_path = ttd.path().join("cache").join(digest("http://www.my.url")); - let (cmd_id, download_request) = dl.recv().await.unwrap(); - assert_eq!(download_request.url, "http://www.my.url"); - assert_eq!(download_request.file_path, download_path); - assert_eq!(download_request.auth, None); - - // Simulate downloading a file is completed - ttd.dir("cache").file(&digest("http://www.my.url")); - let download_response = DownloadResponse::new("http://www.my.url", &download_path); - dl.send((cmd_id.clone(), Ok(download_response))) - .await - .unwrap(); - - // New config_update command should be published - let (topic, received_json) = mqtt - .recv() - .await - .map(|msg| { - ( - msg.topic, - serde_json::from_str::(msg.payload.as_str().expect("UTF8")) - .expect("JSON"), - ) - }) - .unwrap(); - - let mqtt_schema = MqttSchema::default(); - let (entity, channel) = mqtt_schema.entity_channel_of(&topic).unwrap(); - assert_eq!(entity, "device/main//"); - - if let Channel::Command { - operation: OperationType::ConfigUpdate, - cmd_id, - } = channel - { - // Assert symlink is created - assert!(ttd - .path() - .join(format!( - "file-transfer/test-device/config_update/path:config:A-{cmd_id}" - )) - .is_symlink()); - - // Validate the payload JSON - let expected_json = json!({ - "status": "init", - "tedgeUrl": format!("http://localhost:8888/tedge/file-transfer/test-device/config_update/path:config:A-{cmd_id}"), - "type": "path/config/A", - }); - assert_json_diff::assert_json_include!(actual: received_json, expected: expected_json); - } else { - panic!("Unexpected response on channel: {:?}", topic) - } - } - - #[tokio::test] - #[ignore] async fn mapper_converts_smartrest_config_download_req_without_new_download_for_child_device() { let ttd = TempTedgeDir::new(); let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; @@ -450,29 +225,21 @@ mod tests { let (entity, channel) = mqtt_schema.entity_channel_of(&topic).unwrap(); assert_eq!(entity, "device/child1//"); - if let Channel::Command { + let Channel::Command { operation: OperationType::ConfigUpdate, - cmd_id, + .. } = channel - { - // Assert symlink is created - assert!(ttd - .path() - .join(format!( - "file-transfer/child1/config_update/configA-{cmd_id}" - )) - .is_symlink()); - - // Validate the payload JSON - let expected_json = json!({ - "status": "init", - "tedgeUrl": format!("http://localhost:8888/tedge/file-transfer/child1/config_update/configA-{cmd_id}"), - "type": "configA", - }); - assert_json_diff::assert_json_include!(actual: received_json, expected: expected_json); - } else { + else { panic!("Unexpected response on channel: {:?}", topic) - } + }; + + // Validate the payload JSON + let expected_json = json!({ + "status": "init", + "remoteUrl": "http://www.my.url", + "type": "configA", + }); + assert_json_diff::assert_json_include!(actual: received_json, expected: expected_json); } #[tokio::test] diff --git a/crates/extensions/tedge_config_manager/src/actor.rs b/crates/extensions/tedge_config_manager/src/actor.rs index b250d0834a0..6fd429efc56 100644 --- a/crates/extensions/tedge_config_manager/src/actor.rs +++ b/crates/extensions/tedge_config_manager/src/actor.rs @@ -129,7 +129,7 @@ impl ConfigManagerActor { }, Ok(Some(ConfigOperation::Update(request))) => match request.status { CommandStatus::Init => { - info!("Config Snapshot received: {request:?}"); + info!("Config Update received: {request:?}"); self.start_executing_config_request( &message.topic, ConfigOperation::Update(request), @@ -294,12 +294,17 @@ impl ConfigManagerActor { // move to destination later let temp_path = &self.config.tmp_path.join(&file_entry.config_type); - let download_request = DownloadRequest::new(&request.tedge_url, temp_path.as_std_path()) + let Some(tedge_url) = &request.tedge_url else { + debug!("tedge_url not present in config update payload, ignoring"); + return Ok(()); + }; + + let download_request = DownloadRequest::new(tedge_url, temp_path.as_std_path()) .with_permission(file_entry.file_permissions.to_owned()); info!( "Awaiting download for config type: {} from url: {}", - request.config_type, request.tedge_url + request.config_type, tedge_url ); self.download_sender From 0cce0b390a4bb7c84fe0343a213eba13f8a820a8 Mon Sep 17 00:00:00 2001 From: Marcel Guzik Date: Tue, 12 Dec 2023 07:50:00 +0000 Subject: [PATCH 2/2] file transfer https test use FTS on separate container Signed-off-by: Marcel Guzik --- .../src/operations/config_update.rs | 84 +++++---- ...nfiguration_with_file_transfer_https.robot | 159 ++++++++++++++---- .../configuration/generate_certificates.sh | 24 ++- 3 files changed, 193 insertions(+), 74 deletions(-) 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 84f1ddba3ea..dde2e9c7bcb 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs @@ -180,7 +180,57 @@ mod tests { const TEST_TIMEOUT_MS: Duration = Duration::from_millis(5000); #[tokio::test] - async fn mapper_converts_smartrest_config_download_req_without_new_download_for_child_device() { + async fn mapper_converts_smartrest_config_download_req_with_new_download_for_main_device() { + let ttd = TempTedgeDir::new(); + let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + + skip_init_messages(&mut mqtt).await; + + // Simulate c8y_DownloadConfigFile SmartREST request + mqtt.send(MqttMessage::new( + &C8yTopic::downstream_topic(), + "524,test-device,http://www.my.url,path/config/A", + )) + .await + .expect("Send failed"); + + // New config_update command should be published + let (topic, received_json) = mqtt + .recv() + .await + .map(|msg| { + ( + msg.topic, + serde_json::from_str::(msg.payload.as_str().expect("UTF8")) + .expect("JSON"), + ) + }) + .unwrap(); + + let mqtt_schema = MqttSchema::default(); + let (entity, channel) = mqtt_schema.entity_channel_of(&topic).unwrap(); + assert_eq!(entity, "device/main//"); + + let Channel::Command { + operation: OperationType::ConfigUpdate, + .. + } = channel + else { + panic!("Unexpected response on channel: {:?}", topic) + }; + + // Validate the payload JSON + let expected_json = json!({ + "status": "init", + "remoteUrl": "http://www.my.url", + "type": "path/config/A", + }); + assert_json_diff::assert_json_include!(actual: received_json, expected: expected_json); + } + + #[tokio::test] + async fn mapper_converts_smartrest_config_download_req_for_child_device() { let ttd = TempTedgeDir::new(); let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -250,16 +300,6 @@ mod tests { skip_init_messages(&mut mqtt).await; - // Simulate a symlink exists - ttd.dir("file-transfer") - .dir("test-device") - .dir("config_update") - .file("typeA-c8y-mapper-1234"); - assert!(ttd - .path() - .join("file-transfer/test-device/config_update/typeA-c8y-mapper-1234") - .exists()); - // Simulate config_snapshot command with "executing" state mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/main///cmd/config_update/c8y-mapper-1234"), @@ -301,12 +341,6 @@ mod tests { )], ) .await; - - // Assert symlink is removed - assert!(!ttd - .path() - .join("file-transfer/test-device/config_update/typeA-c8y-mapper-1234") - .exists()); } #[tokio::test] @@ -382,16 +416,6 @@ mod tests { skip_init_messages(&mut mqtt).await; - // Simulate a symlink exists - ttd.dir("file-transfer") - .dir("test-device") - .dir("config_update") - .file("path:type:A-c8y-mapper-1234"); - assert!(ttd - .path() - .join("file-transfer/test-device/config_update/path:type:A-c8y-mapper-1234") - .exists()); - // Simulate config_update command with "executing" state mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/main///cmd/config_update/c8y-mapper-1234"), @@ -408,12 +432,6 @@ mod tests { // Expect `503` smartrest message on `c8y/s/us`. assert_received_contains_str(&mut mqtt, [("c8y/s/us", "503,c8y_DownloadConfigFile")]).await; - - // Assert symlink is removed - assert!(!ttd - .path() - .join("file-transfer/test-device/config_update/path:type:A-1234") - .exists()); } #[tokio::test] diff --git a/tests/RobotFramework/tests/cumulocity/configuration/configuration_with_file_transfer_https.robot b/tests/RobotFramework/tests/cumulocity/configuration/configuration_with_file_transfer_https.robot index 904447feb5f..fe37b24cba4 100644 --- a/tests/RobotFramework/tests/cumulocity/configuration/configuration_with_file_transfer_https.robot +++ b/tests/RobotFramework/tests/cumulocity/configuration/configuration_with_file_transfer_https.robot @@ -10,6 +10,11 @@ Test Setup Test Setup Test Tags theme:configuration theme:childdevices +Documentation This suite aims to test the configuration update and snapshot operations when +... the File Transfer Service is located in another container of the main device, +... and operations are triggered for a separate child device, which makes 3 +... containers in total. + *** Variables *** ${PARENT_SN} ${CHILD_SN} @@ -18,21 +23,21 @@ ${CHILD_SN} *** Test Cases *** File Transfer Service has HTTPS enabled ThinEdgeIO.Set Device Context ${PARENT_SN} - ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} https://localhost:8000/tedge/file-transfer/non-existent-file timeout=0 + ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} https://${FTS_IP}:8000/tedge/file-transfer/non-existent-file timeout=0 Should Be Equal ${code} 404 File Transfer Service redirects HTTP to HTTPS ThinEdgeIO.Set Device Context ${PARENT_SN} - ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} http://localhost:8000/tedge/file-transfer/non-existent-file timeout=0 + ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} http://${FTS_IP}:8000/tedge/file-transfer/non-existent-file timeout=0 Should Be Equal ${code} 307 - ${GET_url_effective}= Execute Command curl --output /dev/null --write-out \%\{url_effective\} -L http://localhost:8000/tedge/file-transfer/non-existent-file timeout=0 - Should Be Equal ${GET_url_effective} https://localhost:8000/tedge/file-transfer/non-existent-file - ${HEAD_url_effective}= Execute Command curl --head --output /dev/null --write-out \%\{url_effective\} -L http://localhost:8000/tedge/file-transfer/non-existent-file timeout=0 - Should Be Equal ${HEAD_url_effective} https://localhost:8000/tedge/file-transfer/non-existent-file + ${GET_url_effective}= Execute Command curl --output /dev/null --write-out \%\{url_effective\} -L http://${FTS_IP}:8000/tedge/file-transfer/non-existent-file timeout=0 + Should Be Equal ${GET_url_effective} https://${FTS_IP}:8000/tedge/file-transfer/non-existent-file + ${HEAD_url_effective}= Execute Command curl --head --output /dev/null --write-out \%\{url_effective\} -L http://${FTS_IP}:8000/tedge/file-transfer/non-existent-file timeout=0 + Should Be Equal ${HEAD_url_effective} https://${FTS_IP}:8000/tedge/file-transfer/non-existent-file File Transfer Service is accessible over HTTPS from child device ThinEdgeIO.Set Device Context ${CHILD_SN} - ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} https://${parent_ip}:8000/tedge/file-transfer/non-existent-file timeout=0 + ${code}= Execute Command curl --output /dev/null --write-out \%\{http_code\} https://${FTS_IP}:8000/tedge/file-transfer/non-existent-file timeout=0 Should Be Equal ${code} 404 Configuration snapshots are uploaded to File Transfer Service via HTTPS @@ -44,28 +49,30 @@ Configuration snapshots are uploaded to File Transfer Service via HTTPS with cli Configuration operation fails when configuration-plugin does not supply client certificate Enable Certificate Authentication for File Transfer Service - Disable HTTP Client Certificate for Child Device + Disable HTTP Client Certificate for FTS client Get Configuration Should Fail ... device=${CHILD_SN} - ... failure_reason=config-manager failed uploading configuration snapshot:.+https://${parent_ip}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired + ... failure_reason=config-manager failed uploading configuration snapshot:.+https://${FTS_IP}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired ... external_id=${PARENT_SN}:device:${CHILD_SN} Update Configuration Should Fail ... device=${CHILD_SN} - ... failure_reason=config-manager failed downloading a file:.+https://${parent_ip}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired + ... failure_reason=config-manager failed downloading a file:.+https://${FTS_IP}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired ... external_id=${PARENT_SN}:device:${CHILD_SN} Configuration snapshot fails when mapper does not supply client certificate Enable Certificate Authentication for File Transfer Service Disable HTTP Client Certificate for Mapper + Enable HTTP Client Certificate for FTS client Get Configuration Should Fail ... device=${CHILD_SN} - ... failure_reason=tedge-mapper-c8y failed to download configuration snapshot from file-transfer service:.+https://${parent_ip}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired + ... failure_reason=tedge-mapper-c8y failed to download configuration snapshot from file-transfer service:.+https://${FTS_IP}:8000/tedge/file-transfer/.+received fatal alert: CertificateRequired ... external_id=${PARENT_SN}:device:${CHILD_SN} [Teardown] Re-enable HTTP Client Certificate for Mapper Configuration update succeeds despite mapper not supplying client certificate Enable Certificate Authentication for File Transfer Service Disable HTTP Client Certificate for Mapper + Enable HTTP Client Certificate for FTS client Update Configuration Should Succeed ... external_id=${PARENT_SN}:device:${CHILD_SN} [Teardown] Re-enable HTTP Client Certificate for Mapper @@ -108,17 +115,24 @@ Update Configuration Should Succeed Cumulocity.Should Support Configurations tedge-configuration-plugin /etc/tedge/tedge.toml system.toml CONFIG1 Config@2.0.0 Enable Certificate Authentication for File Transfer Service - Set Device Context ${PARENT_SN} + Set Device Context ${FTS_SN} Execute Command sudo tedge config set http.ca_path /etc/tedge/device-local-certs/roots Execute Command sudo systemctl restart tedge-agent ThinEdgeIO.Service Health Status Should Be Up tedge-agent -Disable HTTP Client Certificate for Child Device +Disable HTTP Client Certificate for FTS client Set Device Context ${CHILD_SN} Execute Command tedge config unset http.client.auth.cert_file Execute Command tedge config unset http.client.auth.key_file Execute Command sudo systemctl restart tedge-agent - ThinEdgeIO.Service Health Status Should Be Up tedge-agent device=${CHILD_SN} + ThinEdgeIO.Service Health Status Should Be Up tedge-agent device=${CHILD_SN} + +Enable HTTP Client Certificate for FTS client + Set Device Context ${CHILD_SN} + Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt + Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key + Execute Command sudo systemctl restart tedge-agent + ThinEdgeIO.Service Health Status Should Be Up tedge-agent device=${CHILD_SN} Disable HTTP Client Certificate for Mapper Set Device Context ${PARENT_SN} @@ -130,8 +144,8 @@ Disable HTTP Client Certificate for Mapper Re-enable HTTP Client Certificate for Mapper Set Device Context ${PARENT_SN} - Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/main-agent.crt - Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/main-agent.key + Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt + Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key ThinEdgeIO.Service Health Status Should Be Up tedge-mapper-c8y Execute Command sudo systemctl restart tedge-mapper-c8y ThinEdgeIO.Service Health Status Should Be Up tedge-mapper-c8y @@ -142,53 +156,99 @@ Re-enable HTTP Client Certificate for Mapper Suite Setup # Parent ${parent_sn}= Setup skip_bootstrap=${False} + Execute Command apt-get -y remove tedge-agent Set Suite Variable $PARENT_SN ${parent_sn} ${parent_ip}= Get IP Address Set Suite Variable $PARENT_IP ${parent_ip} + + # Main device agent + ${FTS_SN}= Setup skip_bootstrap=${True} + Set Suite Variable $FTS_SN ${FTS_SN} + + ${FTS_IP}= Get IP Address + Set Suite Variable $FTS_IP ${FTS_IP} + + # Child device + ${child_sn}= Setup skip_bootstrap=${True} + Set Suite Variable $CHILD_SN ${child_sn} + + + Set Device Context ${PARENT_SN} + + Execute Command sudo tedge config set http.client.host ${FTS_IP} + Execute Command sudo tedge config set mqtt.external.bind.address ${parent_ip} Execute Command sudo tedge config set mqtt.external.bind.port 1883 - Execute Command sudo tedge config set http.bind.address 0.0.0.0 - Execute Command sudo tedge config set http.client.host ${parent_ip} + + Execute Command sudo tedge config set c8y.proxy.bind.address ${parent_ip} + Execute Command sudo tedge config set c8y.proxy.client.host ${parent_ip} ThinEdgeIO.Transfer To Device ${CURDIR}/generate_certificates.sh /etc/tedge/ Execute Command /etc/tedge/generate_certificates.sh timeout=0 ${root_certificate}= Execute Command cat /etc/tedge/device-local-certs/roots/tedge-local-ca.crt + ${client_certificate}= Execute Command cat /etc/tedge/device-local-certs/tedge-client.crt ${client_key}= Execute Command cat /etc/tedge/device-local-certs/tedge-client.key - Restart Service tedge-agent + ${agent_certificate}= Execute Command cat /etc/tedge/device-local-certs/main-agent.crt + ${agent_key}= Execute Command cat /etc/tedge/device-local-certs/main-agent.key + + Execute Command echo "${root_certificate}" > /usr/local/share/ca-certificates/tedge-ca.crt + Execute Command sudo update-ca-certificates + + Execute Command tedge config set c8y.proxy.ca_path /etc/tedge/device-local-certs/roots + Execute Command tedge config set c8y.proxy.cert_path /etc/tedge/device-local-certs/c8y-mapper.crt + Execute Command tedge config set c8y.proxy.key_path /etc/tedge/device-local-certs/c8y-mapper.key + + Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt + Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key + ThinEdgeIO.Disconnect Then Connect Mapper c8y ThinEdgeIO.Service Health Status Should Be Up tedge-mapper-c8y # Child - Setup Child Device parent_ip=${parent_ip} install_package=tedge-agent root_certificate=${root_certificate} certificate=${client_certificate} private_key=${client_key} + Setup Child Device ${child_sn} parent_ip=${parent_ip} install_package=tedge-agent + ... root_certificate=${root_certificate} + ... agent_certificate=${agent_certificate} agent_private_key=${agent_key} + ... client_certificate=${client_certificate} client_key=${client_key} + + Setup Main Device Agent ${root_certificate} ${agent_certificate} ${agent_key} + ... ${client_certificate} ${client_key} + + Set Device Context ${PARENT_SN} + Suite Teardown Get Logs name=${PARENT_SN} + Get Logs name=${FTS_SN} Get Logs name=${CHILD_SN} Setup Child Device - [Arguments] ${parent_ip} ${install_package} ${root_certificate} ${certificate} ${private_key} - ${child_sn}= Setup skip_bootstrap=${True} - Set Suite Variable $CHILD_SN ${child_sn} + [Arguments] ${child_sn} ${parent_ip} ${install_package} ${root_certificate} + ... ${agent_certificate} ${agent_private_key} + ... ${client_certificate} ${client_key} Set Device Context ${CHILD_SN} + Execute Command sudo dpkg -i packages/tedge_*.deb + Execute Command sudo tedge config set mqtt.device_topic_id device/${CHILD_SN}// + + Execute Command sudo tedge config set http.client.host ${FTS_IP} Execute Command sudo tedge config set mqtt.client.host ${parent_ip} - Execute Command sudo tedge config set mqtt.client.port 1883 - Execute Command sudo tedge config set http.client.host ${parent_ip} - Execute Command sudo tedge config set mqtt.topic_root te - Execute Command sudo tedge config set mqtt.device_topic_id device/${child_sn}// - Execute Command mkdir -p /etc/tedge/device-local-certs + Execute Command mkdir -p /etc/tedge/device-local-certs/roots Execute Command echo "${root_certificate}" > /usr/local/share/ca-certificates/tedge-ca.crt + Execute Command echo "${root_certificate}" > /etc/tedge/device-local-certs/roots/tedge-local-ca.crt Execute Command sudo update-ca-certificates + Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key - Execute Command echo "${certificate}" | sudo tee "$(tedge config get http.client.auth.cert_file)" - Execute Command echo "${private_key}" | sudo tee "$(tedge config get http.client.auth.key_file)" + + Execute Command echo "${client_certificate}" | tee "$(tedge config get http.client.auth.cert_file)" + Execute Command echo "${client_key}" | tee "$(tedge config get http.client.auth.key_file)" + Execute Command chown -R tedge:tedge /etc/tedge/device-local-certs # Install plugin after the default settings have been updated to prevent it from starting up as the main plugin @@ -200,14 +260,47 @@ Setup Child Device RETURN ${child_sn} +Setup Main Device Agent + [Arguments] ${root_certificate} ${agent_certificate} ${agent_key} + ... ${client_certificate} ${client_key} + Set Device Context ${FTS_SN} + + Execute Command sudo dpkg -i packages/tedge_*.deb + + Execute Command sudo tedge config set http.client.host ${FTS_IP} + Execute Command sudo tedge config set mqtt.client.host ${PARENT_IP} + + Execute Command sudo tedge config set http.bind.address 0.0.0.0 + + Execute Command mkdir -p /etc/tedge/device-local-certs/roots + Execute Command echo "${root_certificate}" > /usr/local/share/ca-certificates/tedge-ca.crt + Execute Command echo "${root_certificate}" > /etc/tedge/device-local-certs/roots/tedge-local-ca.crt + Execute Command sudo update-ca-certificates + + Execute Command tedge config set http.cert_path /etc/tedge/device-local-certs/main-agent.crt + Execute Command tedge config set http.key_path /etc/tedge/device-local-certs/main-agent.key + + Execute Command echo "${agent_certificate}" | tee "$(tedge config get http.cert_path)" + Execute Command echo "${agent_key}" | tee "$(tedge config get http.key_path)" + + Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt + Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key + + Execute Command echo "${client_certificate}" | tee "$(tedge config get http.client.auth.cert_file)" + Execute Command echo "${client_key}" | tee "$(tedge config get http.client.auth.key_file)" + + Execute Command chown -R tedge:tedge /etc/tedge/device-local-certs + + Execute Command sudo dpkg -i packages/tedge-agent*.deb + Execute Command sudo systemctl enable tedge-agent + Execute Command sudo systemctl start tedge-agent + Test Setup Copy Configuration Files ${PARENT_SN} Copy Configuration Files ${CHILD_SN} ThinEdgeIO.Set Device Context ${CHILD_SN} - Execute Command tedge config set http.client.auth.cert_file /etc/tedge/device-local-certs/tedge-client.crt - Execute Command tedge config set http.client.auth.key_file /etc/tedge/device-local-certs/tedge-client.key Execute Command sudo systemctl restart tedge-agent - ThinEdgeIO.Service Health Status Should Be Up tedge-agent device=${CHILD_SN} + ThinEdgeIO.Service Health Status Should Be Up tedge-agent Copy Configuration Files [Arguments] ${device} diff --git a/tests/RobotFramework/tests/cumulocity/configuration/generate_certificates.sh b/tests/RobotFramework/tests/cumulocity/configuration/generate_certificates.sh index de9e1838c91..e24e047a9ff 100755 --- a/tests/RobotFramework/tests/cumulocity/configuration/generate_certificates.sh +++ b/tests/RobotFramework/tests/cumulocity/configuration/generate_certificates.sh @@ -3,8 +3,8 @@ set -e DEVICE=$(tedge config get device.id) -COMMON_NAME=$(tedge config get http.client.host) -echo "Generating certificate with SAN $COMMON_NAME" +C8Y_PROXY_COMMON_NAME=$(tedge config get c8y.proxy.client.host) +FTS_COMMON_NAME=$(tedge config get http.client.host) ## Signing certificate openssl req \ @@ -22,40 +22,48 @@ openssl req \ openssl genrsa -out c8y-mapper.key 2048 openssl req -out c8y-mapper.csr -key c8y-mapper.key \ - -subj "/O=thin-edge/OU=$DEVICE/SN=c8y-mapper/CN=localhost" \ + -subj "/O=thin-edge/OU=$DEVICE/SN=c8y-mapper/CN=$C8Y_PROXY_COMMON_NAME" \ -new -cat > v3.ext << EOF +cat > v3.c8yproxy.ext << EOF authorityKeyIdentifier=keyid basicConstraints=CA:FALSE keyUsage = digitalSignature, keyAgreement extendedKeyUsage = serverAuth, clientAuth -subjectAltName=DNS:localhost,IP:$COMMON_NAME +subjectAltName=DNS:localhost,IP:$C8Y_PROXY_COMMON_NAME EOF openssl x509 -req \ -in c8y-mapper.csr \ -CA tedge-local-ca.crt \ -CAkey tedge-local-ca.key \ - -extfile v3.ext \ + -extfile v3.c8yproxy.ext \ -CAcreateserial \ -out c8y-mapper.crt \ -days 100 ## main agent certificate +cat > v3.agent.ext << EOF +authorityKeyIdentifier=keyid +basicConstraints=CA:FALSE +keyUsage = digitalSignature, keyAgreement +extendedKeyUsage = serverAuth, clientAuth +subjectAltName=DNS:localhost,IP:$FTS_COMMON_NAME +EOF + openssl genrsa -out main-agent.key 2048 openssl req -out main-agent.csr \ -key main-agent.key \ - -subj "/O=thin-edge/OU=$DEVICE/SN=main-agent/CN=$COMMON_NAME" \ + -subj "/O=thin-edge/OU=$DEVICE/SN=main-agent/CN=$FTS_COMMON_NAME" \ -new openssl x509 -req \ -in main-agent.csr \ -CA tedge-local-ca.crt \ -CAkey tedge-local-ca.key \ - -extfile v3.ext \ + -extfile v3.agent.ext \ -CAcreateserial \ -out main-agent.crt \ -days 100