diff options
author | Rina Fujino <18257209+rina23q@users.noreply.github.com> | 2022-04-08 15:30:43 +0200 |
---|---|---|
committer | Rina Fujino <18257209+rina23q@users.noreply.github.com> | 2022-04-08 15:30:43 +0200 |
commit | 5fcf8b57d45b2d98175bf010c8bbd92ef50550f5 (patch) | |
tree | 128cea7b6e124bcbda9beccf6b542f6136e6f24a | |
parent | 45ea96aca1e148118d2f1726a5c7ec3669582b4c (diff) |
Initial skeleton of c8y_configuration_plugin
- The daemon reads the plugin config file, and reports the supported config types to c8y
- Issue tracked by #1030, Specs is by #1028
Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
-rw-r--r-- | Cargo.lock | 17 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/Cargo.toml | 23 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/config.rs | 150 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/main.rs | 56 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/smartrest.rs | 51 |
6 files changed, 298 insertions, 0 deletions
@@ -388,6 +388,23 @@ dependencies = [ ] [[package]] +name = "c8y_configuration_plugin" +version = "0.6.1" +dependencies = [ + "anyhow", + "c8y_smartrest", + "mqtt_channel", + "serde", + "tedge_config", + "tempfile", + "test-case", + "thiserror", + "tokio", + "toml", + "tracing", +] + +[[package]] name = "c8y_smartrest" version = "0.6.1" dependencies = [ @@ -3,6 +3,7 @@ members = [ "crates/common/*", "crates/core/*", "crates/tests/*", + "plugins/c8y_configuration_plugin", "plugins/tedge_apt_plugin", "plugins/tedge_dummy_plugin", "plugins/tedge_apama_plugin", diff --git a/plugins/c8y_configuration_plugin/Cargo.toml b/plugins/c8y_configuration_plugin/Cargo.toml new file mode 100644 index 00000000..81dd3b97 --- /dev/null +++ b/plugins/c8y_configuration_plugin/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "c8y_configuration_plugin" +version = "0.6.1" +authors = ["thin-edge.io team <info@thin-edge.io>"] +edition = "2021" +rust-version = "1.58.1" +license = "Apache-2.0" +description = "Thin.edge.io operation plugin for Cumulocity configuration management request" + +[dependencies] +anyhow = "1.0" +c8y_smartrest = { path = "../../crates/core/c8y_smartrest" } +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +tokio = { version = "1.9", default_features = false, features = [ "fs", "io-util", "macros", "rt-multi-thread","signal"] } +toml = "0.5" +tracing = { version = "0.1", features = ["attributes", "log"] } +tedge_config = { path = "../../crates/common/tedge_config" } +mqtt_channel = { path = "../../crates/common/mqtt_channel" } + +[dev-dependencies] +tempfile = "3.3" +test-case = "2.0" diff --git a/plugins/c8y_configuration_plugin/src/config.rs b/plugins/c8y_configuration_plugin/src/config.rs new file mode 100644 index 00000000..ecd382ab --- /dev/null +++ b/plugins/c8y_configuration_plugin/src/config.rs @@ -0,0 +1,150 @@ +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +pub const PLUGIN_CONFIG_FILE: &str = "c8y_configuration_plugin.toml"; + +#[derive(Deserialize, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PluginConfig { + pub files: Vec<String>, +} + +impl Default for PluginConfig { + fn default() -> Self { + Self { files: vec![] } + } +} + +impl PluginConfig { + pub fn new(config_root: PathBuf) -> Self { + let config_path = config_root.join(PLUGIN_CONFIG_FILE); + let config_path_str = config_path.to_str().unwrap_or(PLUGIN_CONFIG_FILE); + Self::read_config(config_path.clone()).add_file(config_path_str.into()) + } + + fn read_config(path: PathBuf) -> Self { + match fs::read_to_string(path) { + Ok(contents) => match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => Self::default(), // The config file is ill-formed + }, + Err(_) => Self::default(), // The config file does not exist or is not readable + } + } + + fn add_file(&self, file: String) -> Self { + let mut files = self.files.clone(); + let () = files.push(file); + Self { files } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + use test_case::test_case; + + #[test] + fn deserialize_plugin_config() { + let config: PluginConfig = toml::from_str( + r#" + files = [ + '/etc/tedge/tedge.toml', + '/etc/tedge/mosquitto-conf/c8y-bridge.conf', + '/etc/tedge/mosquitto-conf/tedge-mosquitto.conf', + '/etc/mosquitto/mosquitto.conf' + ]"#, + ) + .unwrap(); + + assert_eq!( + config.files, + vec![ + "/etc/tedge/tedge.toml".to_string(), + "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), + "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), + "/etc/mosquitto/mosquitto.conf".to_string(), + ] + ); + } + + #[test_case( + r#"files = [ + '/etc/tedge/tedge.toml', + '/etc/tedge/mosquitto-conf/c8y-bridge.conf', + '/etc/tedge/mosquitto-conf/tedge-mosquitto.conf', + '/etc/mosquitto/mosquitto.conf' + ]"#, + PluginConfig { + files: vec![ + "/etc/tedge/tedge.toml".to_string(), + "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), + "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), + "/etc/mosquitto/mosquitto.conf".to_string(), + ] + } + )] + #[test_case( + r#"files = []"#, + PluginConfig { + files: vec![] + } + ;"empty case" + )] + #[test_case( + r#"test"#, + PluginConfig { + files: vec![] + } + ;"not toml" + )] + #[test_case( + r#"files = [ + '/etc/tedge/tedge.toml', + '/etc/tedge/mosquitto-conf/c8y-bridge.conf', + '/etc/tedge/mosquitto-conf/tedge-mosquitto.conf', + '/etc/mosquitto/mosquitto.conf' + ] + test = false + "#, + PluginConfig { + files: vec![] + } + ;"unexpected field" + )] + fn read_plugin_config_file(file_content: &str, raw_config: PluginConfig) -> anyhow::Result<()> { + let (_dir, config_root_path) = create_temp_plugin_config(file_content)?; + let tmp_path_to_plugin_config = config_root_path.join(PLUGIN_CONFIG_FILE); + let tmp_path_to_plugin_config_str = tmp_path_to_plugin_config + .to_str() + .unwrap_or(PLUGIN_CONFIG_FILE); + + let config = PluginConfig::new(config_root_path.clone()); + + // The expected output should contain /tmp/<random>/c8y_configuration_plugin.toml + let expected_config = raw_config.add_file(tmp_path_to_plugin_config_str.into()); + + assert_eq!(config, expected_config); + + Ok(()) + } + + // Need to return TempDir, otherwise the dir will be deleted when this function ends. + fn create_temp_plugin_config(content: &str) -> std::io::Result<(TempDir, PathBuf)> { + let temp_dir = TempDir::new()?; + let config_root = temp_dir.path().to_path_buf(); + let config_file_path = config_root.join(PLUGIN_CONFIG_FILE); + let mut file = std::fs::File::create(config_file_path.as_path())?; + file.write_all(content.as_bytes())?; + Ok((temp_dir, config_root)) + } + + #[test] + fn add_file_to_plugin_config() { + let config = PluginConfig::default().add_file("/test/path/file".into()); + assert_eq!(config.files, vec!["/test/path/file".to_string()]) + } +} diff --git a/plugins/c8y_configuration_plugin/src/main.rs b/plugins/c8y_configuration_plugin/src/main.rs new file mode 100644 index 00000000..c82100fd --- /dev/null +++ b/plugins/c8y_configuration_plugin/src/main.rs @@ -0,0 +1,56 @@ +mod config; +mod smartrest; + +use crate::config::PluginConfig; +use c8y_smartrest::topic::C8yTopic; +use mqtt_channel::{SinkExt, StreamExt}; +use std::path::PathBuf; +use tedge_config::{get_tedge_config, ConfigSettingAccessor, MqttPortSetting}; +use tracing::{debug, error, info, instrument, warn}; + +const CONFIG_ROOT_PATH: &str = "/etc/tedge/c8y"; + +async fn create_mqtt_client() -> Result<mqtt_channel::Connection, anyhow::Error> { + let tedge_config = get_tedge_config()?; + let mqtt_port = tedge_config.query(MqttPortSetting)?.into(); + let mqtt_config = mqtt_channel::Config::default() + .with_port(mqtt_port) + .with_subscriptions(mqtt_channel::TopicFilter::new_unchecked( + C8yTopic::SmartRestRequest.as_str(), + )); + + let mqtt_client = mqtt_channel::Connection::new(&mqtt_config).await?; + Ok(mqtt_client) +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Create required clients + let mut mqtt_client = create_mqtt_client().await?; + + let plugin_config = PluginConfig::new(PathBuf::from(CONFIG_ROOT_PATH)); + + // Publish supported configuration types + let msg = plugin_config.to_message()?; + let () = mqtt_client.published.send(msg).await?; + + // mqtt loop + while let Some(message) = mqtt_client.received.next().await { + debug!("Received {:?}", message); + match message.payload_str()?.split_at(3).0 { + "524" => { + debug!("{}", message.payload_str()?); + todo!() // c8y_DownloadConfigFile + } + "526" => { + debug!("{}", message.payload_str()?); + todo!() // c8y_UploadConfigFile + } + _ => {} + } + } + + mqtt_client.close().await; + + Ok(()) +} diff --git a/plugins/c8y_configuration_plugin/src/smartrest.rs b/plugins/c8y_configuration_plugin/src/smartrest.rs new file mode 100644 index 00000000..6822f5a7 --- /dev/null +++ b/plugins/c8y_configuration_plugin/src/smartrest.rs @@ -0,0 +1,51 @@ +use crate::config::PluginConfig; +use c8y_smartrest::{ + smartrest_serializer::{ + SmartRestSerializer, SmartRestSetOperationToExecuting, SmartRestSetOperationToSuccessful, + }, + topic::C8yTopic, +}; +use mqtt_channel::Message; + +impl PluginConfig { + pub fn to_message(&self) -> Result<Message, anyhow::Error> { + let topic = C8yTopic::SmartRestResponse.to_topic()?; + Ok(Message::new(&topic, self.to_smartrest_payload())) + } + + // 119,typeA,typeB,... + fn to_smartrest_payload(&self) -> String { + let config_types = self + .files + .iter() + .map(|x| x.to_string()) + .collect::<Vec<_>>() + .join(","); + format!("119,{config_types}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case( + PluginConfig { + files: vec!["typeA".to_string()] + }, + "119,typeA".to_string() + ;"single file" + )] + #[test_case( + PluginConfig { + files: vec!["typeA".to_string(), "typeB".to_string(), "typeC".to_string()] + }, + "119,typeA,typeB,typeC".to_string() + ;"multiple files" + )] + fn get_smartrest(input: PluginConfig, expected_output: String) { + let output = input.to_smartrest_payload(); + assert_eq!(output, expected_output); + } +} |