Skip to content
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
5 changes: 5 additions & 0 deletions crates/common/tedge_config/src/tedge_toml/tedge_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,11 @@ define_tedge_config! {
#[tedge_config(example = "true", default(value = true))]
#[tedge_config(exposable)]
shell_execute: bool,

/// Enable service commands feature
#[tedge_config(example = "true", default(value = true))]
#[tedge_config(exposable)]
service_commands: bool,
},

mapper: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ impl FromCloudConfig for C8yMapperSpecificConfig {
device_restart: c8y.enable.device_restart,
software_update: c8y.enable.software_update,
shell_execute: c8y.enable.shell_execute,
service_commands: c8y.enable.service_commands,
},
mqtt_service: MqttServiceConfig {
enabled: c8y.mqtt_service.enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ pub struct EnableConfig {

/// Enable shell_execute feature
pub shell_execute: bool,

/// Enable service_commands feature
pub service_commands: bool,
}

/// Bridge include configuration
Expand Down
38 changes: 38 additions & 0 deletions crates/core/c8y_api/src/json_c8y_deserializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum C8yDeviceControlOperation {
DownloadConfigFile(C8yDownloadConfigFile),
Firmware(C8yFirmware),
DeviceProfile(C8yDeviceProfile),
ServiceCommand(C8yServiceCommand),
Custom,
}

Expand Down Expand Up @@ -71,6 +72,10 @@ impl C8yDeviceControlOperation {
C8yDeviceControlOperation::DeviceProfile(C8yDeviceProfile::from_json_value(
value.clone(),
)?)
} else if let Some(value) = hashmap.get("c8y_ServiceCommand") {
C8yDeviceControlOperation::ServiceCommand(C8yServiceCommand::from_json_value(
value.clone(),
)?)
} else {
C8yDeviceControlOperation::Custom
};
Expand Down Expand Up @@ -164,6 +169,37 @@ impl C8yOperation {
#[derive(Debug, Deserialize, Eq, PartialEq)]
pub struct C8yRestart {}

/// Representation of c8y_ServiceCommand JSON object
///
/// Cumulocity addresses this operation to a service, not to a device.
///
/// ```rust
/// use c8y_api::json_c8y_deserializer::C8yServiceCommand;
///
/// // Example input from c8y
/// let data = r#"{
/// "command": "RESTART",
/// "serviceName": "collectd",
/// "serviceType": "container"
/// }"#;
///
/// // Parse the data
/// let req: C8yServiceCommand = serde_json::from_str(data).unwrap();
///
/// assert_eq!(req.command, "RESTART");
/// assert_eq!(req.service_name, "collectd");
/// assert_eq!(req.service_type, "container");
/// ```
#[derive(Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct C8yServiceCommand {
pub command: String,
#[serde(default)]
pub service_name: String,
#[serde(default)]
pub service_type: String,
}

/// Representation of c8y_SoftwareUpdate JSON object
///
/// ```rust
Expand Down Expand Up @@ -574,6 +610,8 @@ impl C8yDeviceControlOperationHelper for C8yFirmware {}

impl C8yDeviceControlOperationHelper for C8yDeviceProfile {}

impl C8yDeviceControlOperationHelper for C8yServiceCommand {}

#[derive(thiserror::Error, Debug)]
pub enum C8yJsonOverMqttDeserializerError {
#[error("Parameter {parameter} is not recognized. {hint}")]
Expand Down
2 changes: 2 additions & 0 deletions crates/core/c8y_api/src/smartrest/smartrest_serializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ pub enum CumulocitySupportedOperations {
C8yDownloadConfigFile,
C8yFirmware,
C8yDeviceProfile,
C8yServiceCommand,
C8yCustom(String),
}

Expand All @@ -216,6 +217,7 @@ impl CumulocitySupportedOperations {
CumulocitySupportedOperations::C8yDownloadConfigFile => "c8y_DownloadConfigFile",
CumulocitySupportedOperations::C8yFirmware => "c8y_Firmware",
CumulocitySupportedOperations::C8yDeviceProfile => "c8y_DeviceProfile",
CumulocitySupportedOperations::C8yServiceCommand => "c8y_ServiceCommand",
CumulocitySupportedOperations::C8yCustom(operation) => operation.as_str(),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/extensions/c8y_mapper_ext/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ impl C8yMapperConfig {
device_restart: c8y_config.cloud_specific.enable.device_restart,
software_update: c8y_config.cloud_specific.enable.software_update,
shell_execute: c8y_config.cloud_specific.enable.shell_execute,
service_commands: c8y_config.cloud_specific.enable.service_commands,
};
let bridge_config = BridgeConfig {
c8y_prefix: c8y_config.bridge.topic_prefix.clone(),
Expand Down
43 changes: 35 additions & 8 deletions crates/extensions/c8y_mapper_ext/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::error::MessageConversionError;
use crate::mea::events::EventConverter;
use crate::operations;
use crate::operations::OperationHandler;
use crate::service_command::ServiceCommands;
use crate::shell_execute::SHELL_EXECUTE_OPERATION;
use crate::supported_operations::operation::get_child_ops;
use crate::supported_operations::operation::Operation;
Expand Down Expand Up @@ -180,6 +181,8 @@ pub struct CumulocityConverter {

pub supported_operations: SupportedOperations,
pub operation_handler: OperationHandler,
// Keep the service actions declared by the services of this device and its children
pub(crate) service_commands: ServiceCommands,
}

impl CumulocityConverter {
Expand Down Expand Up @@ -261,6 +264,7 @@ impl CumulocityConverter {
recently_completed_commands: HashMap::new(),
active_commands_last_cleared: Instant::now(),
operation_handler,
service_commands: ServiceCommands::default(),
})
}

Expand All @@ -283,6 +287,7 @@ impl CumulocityConverter {
if message.payload().is_empty() {
// Clear cached entity
self.entity_cache.delete(&topic_id);
self.service_commands.remove_service(&topic_id);
return Ok(UpdateOutcome::Deleted);
}

Expand Down Expand Up @@ -585,6 +590,14 @@ impl CumulocityConverter {
vec![]
}
}
C8yDeviceControlOperation::ServiceCommand(request) => {
if self.config.capabilities.service_commands {
self.convert_service_command_request(device_xid, cmd_id, &operation_id, request)
} else {
warn!("Received a c8y_ServiceCommand operation, however, service_commands feature is disabled");
vec![]
}
}
C8yDeviceControlOperation::Custom => {
return self
.process_json_custom_operation(
Expand Down Expand Up @@ -1135,12 +1148,16 @@ impl CumulocityConverter {
}

Channel::CommandMetadata { operation } => {
// Service commands are considered as actions of the service
if entity_type == EntityType::Service {
return self
.convert_service_command_metadata(&source, operation, message)
.await;
}

// https://github.com/thin-edge/thin-edge.io/issues/2739
if message.payload().is_empty() {
warn!(topic = ?message.topic.name, "Ignoring command metadata clearing message: clearing capabilities is not currently supported");
return Ok(vec![]);
}
if entity_type == EntityType::Service {
warn!(topic = ?message.topic.name, "Ignoring command metadata clearing message: clearing device capabilities is not currently supported");
return Ok(vec![]);
}
Comment thread
jarhodes314 marked this conversation as resolved.
match operation {
Expand Down Expand Up @@ -1183,6 +1200,7 @@ impl CumulocityConverter {
let entity = operations::EntityTarget {
topic_id: entity.topic_id().clone(),
external_id: entity.external_id.clone(),
entity_type: entity.r#type(),
smartrest_publish_topic: self
.smartrest_publish_topic_for_entity(entity.topic_id())?,
};
Expand Down Expand Up @@ -3095,7 +3113,7 @@ pub(crate) mod tests {
}

#[tokio::test]
async fn a_service_capability_registers_no_supported_operation() {
async fn a_restart_capability_of_a_service_registers_a_service_command() {
let tmp_dir = TempTedgeDir::new();
let config = c8y_converter_config(&tmp_dir);
let (mut converter, _http_proxy) = create_c8y_converter_from_config(config);
Expand Down Expand Up @@ -3126,7 +3144,16 @@ pub(crate) mod tests {
let operation_msg = MqttMessage::new(&operation_topic, "{}");

let msgs = converter.convert(&operation_msg).await;
assert!(msgs.is_empty());
assert_messages_matching(
&msgs,
[
("c8y/s/us/service0", "114,c8y_ServiceCommand".into()),
(
"c8y/inventory/managedObjects/update/service0",
json!({"c8y_SupportedServiceCommands": ["RESTART"]}).into(),
),
],
);
}

fn registered_entities_into_mqtt_messages(
Expand All @@ -3149,7 +3176,7 @@ pub(crate) mod tests {
create_c8y_converter_from_config(config)
}

fn c8y_converter_config(tmp_dir: &TempTedgeDir) -> C8yMapperConfig {
pub(crate) fn c8y_converter_config(tmp_dir: &TempTedgeDir) -> C8yMapperConfig {
tmp_dir.dir("operations").dir("c8y");
tmp_dir.dir("tedge").dir("agent");
tmp_dir.dir(".tedge-mapper-c8y");
Expand Down Expand Up @@ -3208,7 +3235,7 @@ pub(crate) mod tests {
)
}

fn create_c8y_converter_from_config(
pub(crate) fn create_c8y_converter_from_config(
config: C8yMapperConfig,
) -> (CumulocityConverter, FakeServerBox<HttpRequest, HttpResult>) {
let mqtt_builder: SimpleMessageBoxBuilder<MqttMessage, MqttMessage> =
Expand Down
8 changes: 8 additions & 0 deletions crates/extensions/c8y_mapper_ext/src/entity_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ impl CloudEntityMetadata {
.unwrap_or_else(|| self.external_id.as_ref())
}

pub fn registered_name(&self) -> Option<&str> {
self.metadata.twin_data.get("name").and_then(|v| v.as_str())
}

pub fn registered_type(&self) -> Option<&str> {
self.metadata.display_type()
}

pub fn display_type(&self) -> &str {
self.metadata
.display_type()
Expand Down
2 changes: 1 addition & 1 deletion crates/extensions/c8y_mapper_ext/src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl CumulocityConverter {
}

/// Create a Cumulocity inventory update message from a JSON fragment
fn inventory_update_message(
pub(crate) fn inventory_update_message(
&self,
source: &EntityTopicId,
fragment_value: JsonValue,
Expand Down
3 changes: 3 additions & 0 deletions crates/extensions/c8y_mapper_ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod json;
mod mea;
mod operations;
mod serializer;
mod service_command;
pub mod service_monitor;
mod shell_execute;
mod signals;
Expand All @@ -35,6 +36,7 @@ pub struct Capabilities {
pub device_restart: bool,
pub software_update: bool,
pub shell_execute: bool,
pub service_commands: bool,
}

#[cfg(test)]
Expand All @@ -49,6 +51,7 @@ impl Default for Capabilities {
device_restart: true,
software_update: true,
shell_execute: true,
service_commands: true,
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions crates/extensions/c8y_mapper_ext/src/operations/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ mod tests {
use tedge_api::commands::ConfigSnapshotCmd;
use tedge_api::commands::ConfigSnapshotCmdPayload;
use tedge_api::entity::EntityExternalId;
use tedge_api::entity::EntityType;
use tedge_api::mqtt_topics::EntityTopicId;
use tedge_api::mqtt_topics::OperationType;
use tedge_api::CommandStatus;
Expand All @@ -340,6 +341,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -372,6 +374,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -405,6 +408,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -464,6 +468,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -551,6 +556,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -585,6 +591,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -626,6 +633,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -677,6 +685,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down Expand Up @@ -770,6 +779,7 @@ mod tests {
let entity_target = EntityTarget {
topic_id: entity_topic_id.clone(),
external_id: EntityExternalId::from("anything"),
entity_type: EntityType::MainDevice,
smartrest_publish_topic: Topic::new("anything").unwrap(),
};

Expand Down
Loading
Loading