Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

c8y-mapper sends software list to advanced software management endpoint #2771

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::str::FromStr;
use strum::Display;

/// A flag that switches legacy or advanced software management API.
/// Can be set to auto in the future, see #2778.
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, doku::Document, Display,
)]
#[strum(serialize_all = "camelCase")]
pub enum SoftwareManagementApiFlag {
Legacy,
Advanced,
}

#[derive(thiserror::Error, Debug)]
#[error("Failed to parse flag: {input}. Supported values are: legacy, advanced")]
pub struct InvalidSoftwareManagementApiFlag {
input: String,
}

impl FromStr for SoftwareManagementApiFlag {
didier-wenzek marked this conversation as resolved.
Show resolved Hide resolved
type Err = InvalidSoftwareManagementApiFlag;

fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"legacy" => Ok(SoftwareManagementApiFlag::Legacy),
"advanced" => Ok(SoftwareManagementApiFlag::Advanced),
_ => Err(InvalidSoftwareManagementApiFlag {
input: input.to_string(),
}),
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod apt_config;
pub mod auto;
pub mod c8y_software_management;
pub mod connect_url;
pub mod flag;
pub mod host_port;
Expand All @@ -15,6 +16,7 @@ pub const MQTT_TLS_PORT: u16 = 8883;

pub use self::apt_config::*;
pub use self::auto::*;
pub use self::c8y_software_management::*;
pub use self::connect_url::*;
pub use self::flag::*;
#[doc(inline)]
Expand Down
11 changes: 11 additions & 0 deletions crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::AutoFlag;
use crate::ConnectUrl;
use crate::HostPort;
use crate::Seconds;
use crate::SoftwareManagementApiFlag;
use crate::TEdgeConfigLocation;
use crate::TemplatesSet;
use crate::HTTPS_PORT;
Expand Down Expand Up @@ -475,6 +476,16 @@ define_tedge_config! {
#[tedge_config(example = "true", default(value = true))]
clean_start: bool,
},

software_management: {
/// Switch legacy or advanced software management API to use. Value: legacy or advanced
#[tedge_config(example = "advanced", default(variable = "SoftwareManagementApiFlag::Legacy"))]
api: SoftwareManagementApiFlag,

/// Enable publishing c8y_SupportedSoftwareTypes fragment to the c8y inventory API
#[tedge_config(example = "true", default(value = false))]
with_types: bool,
},
},

#[tedge_config(deprecated_name = "azure")] // for 0.1.0 compatibility
Expand Down
168 changes: 163 additions & 5 deletions crates/core/c8y_api/src/smartrest/smartrest_serializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use crate::smartrest::topic::C8yTopic;
use csv::StringRecord;
use mqtt_channel::Message;
use serde::ser::SerializeSeq;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use tedge_api::SoftwareListCommand;
use tedge_api::SoftwareModule;
use tedge_config::TopicPrefix;
use tracing::warn;

Expand Down Expand Up @@ -105,11 +106,93 @@ pub fn declare_supported_operations(ops: &[&str]) -> String {
format!("114,{}", fields_to_csv_string(ops))
}

#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct SmartRestSoftwareModuleItem {
pub software: String,
pub version: Option<String>,
pub url: Option<String>,
pub name: String,
pub version: String,
pub software_type: String,
pub url: String,
}

impl From<SoftwareModule> for SmartRestSoftwareModuleItem {
fn from(module: SoftwareModule) -> Self {
let url = match module.url {
None => "".to_string(),
Some(download_info) => download_info.url,
};

Self {
name: module.name,
version: module.version.unwrap_or_default(),
software_type: module.module_type.unwrap_or(SoftwareModule::default_type()),
url,
}
}
}

pub enum AdvancedSoftwareList {
Set(Vec<SmartRestSoftwareModuleItem>),
Append(Vec<SmartRestSoftwareModuleItem>),
}

impl AdvancedSoftwareList {
fn smartrest_payload(self) -> String {
let vec = match self {
AdvancedSoftwareList::Set(items) => Self::create_software_list("140", items),
AdvancedSoftwareList::Append(items) => Self::create_software_list("141", items),
};
let list: Vec<&str> = vec.iter().map(std::ops::Deref::deref).collect();

fields_to_csv_string(list.as_slice())
}

fn create_software_list(id: &str, items: Vec<SmartRestSoftwareModuleItem>) -> Vec<String> {
if items.is_empty() {
vec![id.into(), "".into(), "".into(), "".into(), "".into()]
} else {
let mut vec = vec![id.to_string()];
for item in items {
vec.push(item.name);
vec.push(item.version);
vec.push(item.software_type);
vec.push(item.url);
}
vec
}
}
}

pub fn get_advanced_software_list_payloads(
software_list_cmd: &SoftwareListCommand,
chunk_size: usize,
) -> Vec<String> {
let mut messages: Vec<String> = Vec::new();

if software_list_cmd.modules().is_empty() {
messages.push(AdvancedSoftwareList::Set(vec![]).smartrest_payload());
return messages;
}

let mut items: Vec<SmartRestSoftwareModuleItem> = Vec::new();
software_list_cmd
.modules()
.into_iter()
.for_each(|software_module| {
let c8y_software_module: SmartRestSoftwareModuleItem = software_module.into();
items.push(c8y_software_module);
});

let mut first = true;
for chunk in items.chunks(chunk_size) {
if first {
messages.push(AdvancedSoftwareList::Set(chunk.to_vec()).smartrest_payload());
first = false;
} else {
messages.push(AdvancedSoftwareList::Append(chunk.to_vec()).smartrest_payload());
}
}

messages
}

/// A supported operation of the thin-edge device, used in status updates via SmartREST
Expand Down Expand Up @@ -230,6 +313,9 @@ pub trait OperationStatusMessage {
#[cfg(test)]
mod tests {
use super::*;
use tedge_api::messages::SoftwareListCommandPayload;
use tedge_api::mqtt_topics::EntityTopicId;
use tedge_api::Jsonify;

#[test]
fn serialize_smartrest_supported_operations() {
Expand Down Expand Up @@ -351,4 +437,76 @@ mod tests {
let smartrest = fail_operation(CumulocitySupportedOperations::C8ySoftwareUpdate, "");
assert_eq!(smartrest, "502,c8y_SoftwareUpdate,");
}

#[test]
fn from_software_module_to_smartrest_software_module_item() {
let software_module = SoftwareModule {
module_type: Some("a".into()),
name: "b".into(),
version: Some("c".into()),
url: Some("".into()),
file_path: None,
};

let expected_c8y_item = SmartRestSoftwareModuleItem {
name: "b".into(),
version: "c".into(),
software_type: "a".to_string(),
url: "".into(),
};

let converted: SmartRestSoftwareModuleItem = software_module.into();
assert_eq!(converted, expected_c8y_item);
}

#[test]
fn from_thin_edge_json_to_advanced_software_list() {
let input_json = r#"{
"id":"1",
"status":"successful",
"currentSoftwareList":[
{"type":"debian", "modules":[
{"name":"a"},
{"name":"b","version":"1.0"},
{"name":"c","url":"https://foobar.io/c.deb"},
{"name":"d","version":"beta","url":"https://foobar.io/d.deb"}
]},
{"type":"apama","modules":[
{"name":"m","url":"https://foobar.io/m.epl"}
]}
]}"#;

let command = SoftwareListCommand {
target: EntityTopicId::default_main_device(),
cmd_id: "1".to_string(),
payload: SoftwareListCommandPayload::from_json(input_json).unwrap(),
};

let advanced_sw_list = get_advanced_software_list_payloads(&command, 2);

assert_eq!(advanced_sw_list[0], "140,a,,debian,,b,1.0,debian,");
assert_eq!(
advanced_sw_list[1],
"141,c,,debian,https://foobar.io/c.deb,d,beta,debian,https://foobar.io/d.deb"
);
assert_eq!(advanced_sw_list[2], "141,m,,apama,https://foobar.io/m.epl");
}

#[test]
fn empty_to_advanced_list() {
let input_json = r#"{
"id":"1",
"status":"successful",
"currentSoftwareList":[]
}"#;

let command = &SoftwareListCommand {
target: EntityTopicId::default_main_device(),
cmd_id: "1".to_string(),
payload: SoftwareListCommandPayload::from_json(input_json).unwrap(),
};

let advanced_sw_list = get_advanced_software_list_payloads(command, 2);
assert_eq!(advanced_sw_list[0], "140,,,,");
}
}
12 changes: 12 additions & 0 deletions crates/extensions/c8y_mapper_ext/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use tedge_api::mqtt_topics::TopicIdError;
use tedge_api::path::DataDir;
use tedge_config::ConfigNotSet;
use tedge_config::ReadError;
use tedge_config::SoftwareManagementApiFlag;
use tedge_config::TEdgeConfig;
use tedge_config::TEdgeConfigReaderService;
use tedge_config::TopicPrefix;
Expand Down Expand Up @@ -54,6 +55,8 @@ pub struct C8yMapperConfig {
pub clean_start: bool,
pub c8y_prefix: TopicPrefix,
pub bridge_in_mapper: bool,
pub software_management_api: SoftwareManagementApiFlag,
pub software_management_with_types: bool,
}

impl C8yMapperConfig {
Expand All @@ -79,6 +82,8 @@ impl C8yMapperConfig {
clean_start: bool,
c8y_prefix: TopicPrefix,
bridge_in_mapper: bool,
software_management_api: SoftwareManagementApiFlag,
software_management_with_types: bool,
) -> Self {
let ops_dir = config_dir
.join(SUPPORTED_OPERATIONS_DIRECTORY)
Expand Down Expand Up @@ -108,6 +113,8 @@ impl C8yMapperConfig {
clean_start,
c8y_prefix,
bridge_in_mapper,
software_management_api,
software_management_with_types,
}
}

Expand Down Expand Up @@ -161,6 +168,9 @@ impl C8yMapperConfig {
let enable_auto_register = tedge_config.c8y.entity_store.auto_register;
let clean_start = tedge_config.c8y.entity_store.clean_start;

let software_management_api = tedge_config.c8y.software_management.api.clone();
let software_management_with_types = tedge_config.c8y.software_management.with_types;

// Add feature topic filters
for cmd in [
OperationType::Restart,
Expand Down Expand Up @@ -220,6 +230,8 @@ impl C8yMapperConfig {
clean_start,
c8y_prefix,
bridge_in_mapper,
software_management_api,
software_management_with_types,
))
}

Expand Down
Loading