From cbda1975a0a00fbda7048d3a6dd0e99b12e5576f Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Thu, 11 Apr 2024 14:02:04 +0200 Subject: [PATCH] Store invoking command topic in sub command state Signed-off-by: Didier Wenzek --- .../src/tedge_operation_converter/actor.rs | 112 +++++++------ crates/core/tedge_api/src/commands.rs | 6 +- crates/core/tedge_api/src/workflow/on_disk.rs | 6 +- crates/core/tedge_api/src/workflow/state.rs | 151 ++++++++++++------ .../core/tedge_api/src/workflow/supervisor.rs | 55 +++---- .../workflows/custom_operation.robot | 18 +-- .../workflows/lite_device_profile.toml | 16 +- 7 files changed, 215 insertions(+), 149 deletions(-) diff --git a/crates/core/tedge_agent/src/tedge_operation_converter/actor.rs b/crates/core/tedge_agent/src/tedge_operation_converter/actor.rs index e1ff939ca1b..2b6191c18e9 100644 --- a/crates/core/tedge_agent/src/tedge_operation_converter/actor.rs +++ b/crates/core/tedge_agent/src/tedge_operation_converter/actor.rs @@ -20,10 +20,10 @@ use tedge_api::commands::SoftwareCommandMetadata; use tedge_api::commands::SoftwareListCommand; use tedge_api::commands::SoftwareUpdateCommand; use tedge_api::mqtt_topics::Channel; +use tedge_api::mqtt_topics::EntityTopicError; use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::mqtt_topics::MqttSchema; use tedge_api::mqtt_topics::OperationType; -use tedge_api::workflow::extract_command_identifier; use tedge_api::workflow::extract_json_output; use tedge_api::workflow::CommandBoard; use tedge_api::workflow::CommandId; @@ -31,7 +31,6 @@ use tedge_api::workflow::GenericCommandState; use tedge_api::workflow::GenericStateUpdate; use tedge_api::workflow::OperationAction; use tedge_api::workflow::OperationName; -use tedge_api::workflow::TopicName; use tedge_api::workflow::WorkflowExecutionError; use tedge_api::workflow::WorkflowSupervisor; use tedge_api::Jsonify; @@ -125,38 +124,35 @@ impl TedgeOperationConverterActor { } async fn process_mqtt_message(&mut self, message: MqttMessage) -> Result<(), RuntimeError> { - let (operation, cmd_id) = match self.mqtt_schema.entity_channel_of(&message.topic) { - Ok((_, Channel::Command { operation, cmd_id })) => (operation, cmd_id), + let Ok((_, operation, cmd_id)) = self.extract_command_identifiers(&message.topic.name) + else { + log::error!("Unknown command channel: {}", &message.topic.name); + return Ok(()); + }; - _ => { - log::error!("Unknown command channel: {}", message.topic.name); - return Ok(()); - } + let Ok(state) = GenericCommandState::from_command_message(&message) else { + log::error!("Invalid command payload: {}", &message.topic.name); + return Ok(()); }; + let step = state.status.clone(); - let mut log_file = self.open_command_log(&message.topic.name, &operation, &cmd_id); + let mut log_file = + self.open_command_log(state.invoking_command_topic(), &operation, &cmd_id); - match self.workflows.apply_external_update(&operation, &message) { - Ok(None) => { - if message.payload_bytes().is_empty() { - log_file - .log_step("", "The command has been fully processed") - .await; - self.persist_command_board().await?; - } - } - Ok(Some(state)) => { + match self.workflows.apply_external_update(&operation, state) { + Ok(None) => (), + Ok(Some(new_state)) => { self.persist_command_board().await?; - self.process_command_state_update(state).await?; + if new_state.is_init() { + self.process_command_state_update(new_state).await?; + } } Err(WorkflowExecutionError::UnknownOperation { operation }) => { info!("Ignoring {operation} operation which is not registered"); } Err(err) => { error!("{operation} operation request cannot be processed: {err}"); - log_file - .log_step("Unknown", &format!("Error: {err}\n")) - .await; + log_file.log_step(&step, &format!("Error: {err}\n")).await; } } @@ -167,15 +163,13 @@ impl TedgeOperationConverterActor { &mut self, state: GenericCommandState, ) -> Result<(), RuntimeError> { - let (target, operation, cmd_id) = match self.mqtt_schema.entity_channel_of(&state.topic) { - Ok((target, Channel::Command { operation, cmd_id })) => (target, operation, cmd_id), - - _ => { - log::error!("Unknown command channel: {}", state.topic.name); - return Ok(()); - } + let Ok((target, operation, cmd_id)) = self.extract_command_identifiers(&state.topic.name) + else { + log::error!("Unknown command channel: {}", state.topic.name); + return Ok(()); }; - let mut log_file = self.open_command_log(&state.topic.name, &operation, &cmd_id); + let mut log_file = + self.open_command_log(state.invoking_command_topic(), &operation, &cmd_id); let action = match self.workflows.get_action(&state) { Ok(action) => action, @@ -187,7 +181,7 @@ impl TedgeOperationConverterActor { Err(err) => { error!("{operation} operation request cannot be processed: {err}"); log_file - .log_step("Unknown", &format!("Error: {err}\n")) + .log_step(&state.status, &format!("Error: {err}\n")) .await; return Ok(()); } @@ -198,12 +192,17 @@ impl TedgeOperationConverterActor { match action { OperationAction::Clear => { if let Some(invoking_command) = self.workflows.invoking_command_state(&state) { + info!( + "Resuming invoking command {}", + invoking_command.topic.as_ref() + ); self.command_sender.send(invoking_command.clone()).await?; + } else { + info!( + "Waiting {} {operation} operation to be cleared", + state.status + ); } - info!( - "Waiting {} {operation} operation to be cleared", - state.status - ); Ok(()) } OperationAction::MoveTo(next_step) => { @@ -350,13 +349,13 @@ impl TedgeOperationConverterActor { .update_with_json(sub_cmd_output) .update(handlers.on_success); self.publish_command_state(new_state).await?; - self.mqtt_publisher.send(sub_state.clear_message()).await?; + self.publish_command_state(sub_state.terminate()).await?; } else if sub_state.is_failed() { let new_state = state.update(handlers.on_error.unwrap_or_else(|| { GenericStateUpdate::failed("sub-operation failed".to_string()) })); self.publish_command_state(new_state).await?; - self.mqtt_publisher.send(sub_state.clear_message()).await?; + self.publish_command_state(sub_state.terminate()).await?; } else { // Nothing specific has to be done: the current state has been persisted // and will be resumed on completion of the sub-operation @@ -371,18 +370,15 @@ impl TedgeOperationConverterActor { fn open_command_log( &mut self, - command_topic: &TopicName, + root_command: Option<&str>, operation: &OperationType, cmd_id: &str, ) -> CommandLog { - let (root_operation, root_cmd_id) = match self - .workflows - .command_invocation_chain(command_topic) - .pop() - .and_then(|topic| extract_command_identifier(&topic)) + let (root_operation, root_cmd_id) = match root_command + .and_then(|root_topic| self.extract_command_identifiers(root_topic).ok()) { None => (None, None), - Some((op, id)) => (Some(op), Some(id)), + Some((_, op, id)) => (Some(op.to_string()), Some(id)), }; CommandLog::new( @@ -470,7 +466,9 @@ impl TedgeOperationConverterActor { error!("Fail to persist workflow operation state: {err}"); } self.persist_command_board().await?; - self.command_sender.send(new_state.clone()).await?; + if !new_state.is_term() { + self.command_sender.send(new_state.clone()).await?; + } self.mqtt_publisher.send(new_state.into_message()).await?; Ok(()) } @@ -510,6 +508,26 @@ impl TedgeOperationConverterActor { Ok(()) } + + fn extract_command_identifiers( + &self, + topic: impl AsRef, + ) -> Result<(EntityTopicId, OperationType, CommandId), CommandTopicError> { + let (target, channel) = self.mqtt_schema.entity_channel_of(topic)?; + match channel { + Channel::Command { operation, cmd_id } => Ok((target, operation, cmd_id)), + _ => Err(CommandTopicError::InvalidCommandTopic), + } + } +} + +#[derive(Debug, thiserror::Error)] +enum CommandTopicError { + #[error(transparent)] + InvalidTopic(#[from] EntityTopicError), + + #[error("Not a command topic")] + InvalidCommandTopic, } /// Log all command steps @@ -582,7 +600,7 @@ Action: {action} let cmd_id = &self.cmd_id; let message = format!( r#"------------------------------------ -{operation}/{cmd_id}/{step}: {now} +{operation}/{cmd_id} status={step} time={now} {action} "# diff --git a/crates/core/tedge_api/src/commands.rs b/crates/core/tedge_api/src/commands.rs index 0a9f42f7a7f..8cec7e95cb5 100644 --- a/crates/core/tedge_api/src/commands.rs +++ b/crates/core/tedge_api/src/commands.rs @@ -133,11 +133,7 @@ where let topic = self.topic(schema); let status = self.status().to_string(); let payload = serde_json::to_value(self.payload).unwrap(); // any command payload can be converted into JSON - GenericCommandState { - topic, - status, - payload, - } + GenericCommandState::new(topic, status, payload) } /// Return the MQTT message for this command diff --git a/crates/core/tedge_api/src/workflow/on_disk.rs b/crates/core/tedge_api/src/workflow/on_disk.rs index 0e58f4f3218..1ddf0acfb2a 100644 --- a/crates/core/tedge_api/src/workflow/on_disk.rs +++ b/crates/core/tedge_api/src/workflow/on_disk.rs @@ -57,11 +57,7 @@ impl TryFrom for CommandBoard { value: command.unix_timestamp, } })?; - let state = GenericCommandState { - topic, - status: command.status, - payload: command.payload, - }; + let state = GenericCommandState::new(topic, command.status, command.payload); commands.insert(topic_name, (timestamp, state)); } Ok(CommandBoard::new(commands)) diff --git a/crates/core/tedge_api/src/workflow/state.rs b/crates/core/tedge_api/src/workflow/state.rs index b205c54ef61..3c661afd311 100644 --- a/crates/core/tedge_api/src/workflow/state.rs +++ b/crates/core/tedge_api/src/workflow/state.rs @@ -6,7 +6,6 @@ use crate::workflow::CommandId; use crate::workflow::ExitHandlers; use crate::workflow::OperationName; use crate::workflow::StateExcerptError; -use crate::workflow::TopicName; use crate::workflow::WorkflowExecutionError; use mqtt_channel::MqttMessage; use mqtt_channel::QoS::AtLeastOnce; @@ -16,6 +15,7 @@ use serde::Serialize; use serde_json::json; use serde_json::Value; use std::collections::HashMap; +use std::fmt::Display; /// Generic command state that can be used to manipulate any type of command payload. #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] @@ -23,6 +23,7 @@ pub struct GenericCommandState { pub topic: Topic, pub status: String, pub payload: Value, + invoking_command_topic: Option, } /// Update for a command state @@ -39,6 +40,16 @@ const FAILED: &str = "failed"; const REASON: &str = "reason"; impl GenericCommandState { + pub fn new(topic: Topic, status: String, payload: Value) -> Self { + let invoking_command_topic = Self::infer_invoking_command_topic(topic.as_ref()); + GenericCommandState { + topic, + status, + payload, + invoking_command_topic, + } + } + /// Create an init state for a sub-operation pub fn sub_command_init_state( schema: &MqttSchema, @@ -47,7 +58,7 @@ impl GenericCommandState { cmd_id: CommandId, sub_operation: OperationName, ) -> GenericCommandState { - let sub_cmd_id = sub_command_id(&operation.to_string(), &cmd_id); + let sub_cmd_id = Self::sub_command_id(&operation, &cmd_id); let topic = schema.topic_for( entity, &Channel::Command { @@ -55,6 +66,8 @@ impl GenericCommandState { cmd_id: sub_cmd_id, }, ); + let invoking_command_topic = + schema.topic_for(entity, &Channel::Command { operation, cmd_id }); let status = INIT.to_string(); let payload = json!({ STATUS: status @@ -64,27 +77,29 @@ impl GenericCommandState { topic, status, payload, + invoking_command_topic: Some(invoking_command_topic.name), } } /// Extract a command state from a json payload pub fn from_command_message(message: &MqttMessage) -> Result { let topic = message.topic.clone(); - let payload = message.payload_bytes(); - if payload.is_empty() { - return Ok(GenericCommandState { - topic, - status: "".to_string(), - payload: json!(null), - }); - } - let json: Value = serde_json::from_slice(payload)?; - let status = GenericCommandState::extract_text_property(&json, STATUS) - .ok_or(WorkflowExecutionError::MissingStatus)?; + let invoking_command_topic = Self::infer_invoking_command_topic(topic.as_ref()); + let bytes = message.payload_bytes(); + let (status, payload) = if bytes.is_empty() { + ("".to_string(), json!(null)) + } else { + let json: Value = serde_json::from_slice(bytes)?; + let status = GenericCommandState::extract_text_property(&json, STATUS) + .ok_or(WorkflowExecutionError::MissingStatus)?; + (status.to_string(), json) + }; + Ok(GenericCommandState { topic, - status: status.to_string(), - payload: json, + status, + payload, + invoking_command_topic, }) } @@ -102,7 +117,7 @@ impl GenericCommandState { } /// Build an MQTT message to clear the command state - pub fn clear_message(&self) -> MqttMessage { + fn clear_message(&self) -> MqttMessage { let topic = &self.topic; MqttMessage::new(topic, "") .with_retain() @@ -166,6 +181,15 @@ impl GenericCommandState { } } + /// Mark the command as terminated + pub fn terminate(self) -> Self { + GenericCommandState { + status: "".to_string(), + payload: json!(null), + ..self + } + } + /// Return the error reason if any pub fn failure_reason(&self) -> Option<&str> { GenericCommandState::extract_text_property(&self.payload, REASON) @@ -240,6 +264,38 @@ impl GenericCommandState { &self.topic.name } + /// Return the topic of the invoking command, if any + pub fn invoking_command_topic(&self) -> Option<&str> { + self.invoking_command_topic.as_deref() + } + + /// Infer the topic of the invoking command, given a sub command topic + fn infer_invoking_command_topic(sub_command_topic: &str) -> Option { + match sub_command_topic.split('/').collect::>()[..] { + [pre, t1, t2, t3, t4, "cmd", _, sub_id] => Self::extract_invoking_command_id(sub_id) + .map(|(op, id)| format!("{pre}/{t1}/{t2}/{t3}/{t4}/cmd/{op}/{id}")), + _ => None, + } + } + + /// Build a sub command identifier from its invoking command identifier + /// + /// Using such a structure command id for sub commands is key + /// to retrieve the invoking command of a sub-operation from its state using [extract_invoking_command_id]. + fn sub_command_id(operation: &impl Display, cmd_id: &impl Display) -> String { + format!("sub:{operation}:{cmd_id}") + } + + /// Extract the invoking command identifier from a sub command identifier + /// + /// Return None if the given id is not a sub command identifier, i.e. if not generated with [sub_command_id]. + fn extract_invoking_command_id(sub_cmd_id: &str) -> Option<(&str, &str)> { + match sub_cmd_id.split(':').collect::>()[..] { + ["sub", operation, cmd_id, ..] => Some((operation, cmd_id)), + _ => None, + } + } + fn target(&self) -> Option { match self.topic.name.split('/').collect::>()[..] { [_, t1, t2, t3, t4, "cmd", _, _] => Some(format!("{t1}/{t2}/{t3}/{t4}")), @@ -272,35 +328,7 @@ impl GenericCommandState { } } -/// Return the invoking command topic name, if any -pub fn invoking_command(sub_command: &TopicName) -> Option { - match sub_command.split('/').collect::>()[..] { - [pre, t1, t2, t3, t4, "cmd", _, sub_id] => extract_invoking_command_id(sub_id) - .map(|(op, id)| format!("{pre}/{t1}/{t2}/{t3}/{t4}/cmd/{op}/{id}")), - _ => None, - } -} - -/// Build a sub command identifier from its invoking command identifier -/// -/// Using such a structure command id for sub commands is key -/// to retrieve the invoking command of a sub-operation from its state using [extract_invoking_command_id]. -fn sub_command_id(operation: &str, cmd_id: &str) -> String { - format!("sub:{operation}:{cmd_id}") -} - -/// Extract the invoking command identifier from a sub command identifier -/// -/// Return None if the given id is not a sub command identifier -/// i.e. if not generated with [sub_command_id]. -fn extract_invoking_command_id(sub_cmd_id: &str) -> Option<(&str, &str)> { - match sub_cmd_id.split(':').collect::>()[..] { - ["sub", operation, cmd_id] => Some((operation, cmd_id)), - _ => None, - } -} - -pub fn extract_command_identifier(topic: &str) -> Option<(String, String)> { +fn extract_command_identifier(topic: &str) -> Option<(String, String)> { match topic.split('/').collect::>()[..] { [_, _, _, _, _, "cmd", operation, cmd_id] => { Some((operation.to_string(), cmd_id.to_string())) @@ -530,7 +558,8 @@ mod tests { "bar": { "extra": [1,2,3] } - }) + }), + invoking_command_topic: None, } ); @@ -546,7 +575,8 @@ mod tests { "bar": { "extra": [1,2,3] } - }) + }), + invoking_command_topic: None, } ); @@ -563,7 +593,32 @@ mod tests { "bar": { "extra": [1,2,3] } - }) + }), + invoking_command_topic: None, + } + ); + } + + #[test] + fn retrieve_invoking_command() { + let topic = Topic::new_unchecked("te/device/main///cmd/do_it/sub:make_it:456"); + let payload = r#"{ "status":"successful", "foo":42, "bar": { "extra": [1,2,3] }}"#; + let command = mqtt_channel::MqttMessage::new(&topic, payload); + let cmd = GenericCommandState::from_command_message(&command).expect("parsing error"); + assert!(cmd.is_successful()); + assert_eq!( + cmd, + GenericCommandState { + topic: topic.clone(), + status: "successful".to_string(), + payload: json!({ + "status": "successful", + "foo": 42, + "bar": { + "extra": [1,2,3] + } + }), + invoking_command_topic: Some("te/device/main///cmd/make_it/456".to_string()), } ); } diff --git a/crates/core/tedge_api/src/workflow/supervisor.rs b/crates/core/tedge_api/src/workflow/supervisor.rs index b652e167c16..e1b4552a3be 100644 --- a/crates/core/tedge_api/src/workflow/supervisor.rs +++ b/crates/core/tedge_api/src/workflow/supervisor.rs @@ -77,18 +77,17 @@ impl WorkflowSupervisor { pub fn apply_external_update( &mut self, operation: &OperationType, - message: &MqttMessage, + command_state: GenericCommandState, ) -> Result, WorkflowExecutionError> { if !self.workflows.contains_key(operation) { return Err(WorkflowExecutionError::UnknownOperation { operation: operation.to_string(), }); }; - let command_state = GenericCommandState::from_command_message(message)?; if command_state.is_term() { // The command has been cleared self.commands.remove(&command_state.topic.name); - Ok(None) + Ok(Some(command_state)) } else if command_state.is_init() { // This is a new command request self.commands.insert(command_state.clone())?; @@ -133,8 +132,9 @@ impl WorkflowSupervisor { &self, sub_command: &GenericCommandState, ) -> Option<&GenericCommandState> { - invoking_command(&sub_command.topic.name) - .and_then(|invoking_topic| self.get_state(&invoking_topic)) + sub_command + .invoking_command_topic() + .and_then(|invoking_topic| self.get_state(invoking_topic)) } /// Return the sub command of a command, if any @@ -144,12 +144,20 @@ impl WorkflowSupervisor { ) -> Option<&GenericCommandState> { self.commands .lookup_sub_command(command_state.command_topic()) - .and_then(|sub_topic| self.get_state(sub_topic)) } - /// Return the chain of sub-operation invocation leading to the given leaf command - pub fn command_invocation_chain(&self, leaf_command: &TopicName) -> Vec { - self.commands.command_invocation_chain(leaf_command) + /// Return the state of the root command which execution leads to the execution of a leaf-command + /// + /// Return None, if the given command is not a sub-command + pub fn root_invoking_command_state( + &self, + leaf_command: &GenericCommandState, + ) -> Option<&GenericCommandState> { + let invoking_command = self.invoking_command_state(leaf_command)?; + let root_command = self + .root_invoking_command_state(invoking_command) + .unwrap_or(invoking_command); + Some(root_command) } /// Update the state of the command board on reception of new state for a command @@ -159,7 +167,12 @@ impl WorkflowSupervisor { &mut self, new_command_state: GenericCommandState, ) -> Result<(), WorkflowExecutionError> { - self.commands.update(new_command_state) + if new_command_state.is_term() { + self.commands.remove(new_command_state.command_topic()); + Ok(()) + } else { + self.commands.update(new_command_state) + } } /// Resume the given command when the agent is restarting after an interruption @@ -211,24 +224,12 @@ impl CommandBoard { } /// Return the sub command of a command, if any - pub fn lookup_sub_command(&self, command_topic: &TopicName) -> Option<&TopicName> { - // The sequential search is okay because in practice there is no more than 10 concurrent commands + pub fn lookup_sub_command(&self, command_topic: &TopicName) -> Option<&GenericCommandState> { + // Sequential search is okay because in practice there is no more than 10 concurrent commands self.commands - .keys() - .find(|sub_command| invoking_command(sub_command).as_ref() == Some(command_topic)) - } - - /// Return the chain of command / sub-operation invocation leading to the given leaf command - pub fn command_invocation_chain(&self, leaf_command: &TopicName) -> Vec { - let mut invoking_commands = vec![]; - let mut command = leaf_command.clone(); - while let Some(super_command) = invoking_command(&command) { - if self.commands.contains_key(&super_command) { - invoking_commands.push(super_command.clone()); - } - command = super_command; - } - invoking_commands + .values() + .find(|(_, command)| command.invoking_command_topic() == Some(command_topic)) + .map(|(_, command)| command) } /// Iterate over the pending commands diff --git a/tests/RobotFramework/tests/tedge_agent/workflows/custom_operation.robot b/tests/RobotFramework/tests/tedge_agent/workflows/custom_operation.robot index f9171f80e66..dcde46ffdc6 100644 --- a/tests/RobotFramework/tests/tedge_agent/workflows/custom_operation.robot +++ b/tests/RobotFramework/tests/tedge_agent/workflows/custom_operation.robot @@ -60,7 +60,7 @@ Trigger native-reboot within workflow (on_success) ${pid_after}= Execute Command sudo systemctl show --property MainPID tedge-agent Should Not Be Equal ${pid_before} ${pid_after} msg=tedge-agent should have been restarted ${workflow_log}= Execute Command cat /var/log/tedge/agent/workflow-native-reboot-robot-1.log - Should Contain ${workflow_log} restarted: msg=restarted state should have been executed + Should Contain ${workflow_log} item=status=restarted msg=restarted state should have been executed Trigger native-reboot within workflow (on_error) - missing sudoers entry for reboot Execute Command cmd=echo 'tedge ALL = (ALL) NOPASSWD: /usr/bin/tedge, /etc/tedge/sm-plugins/[a-zA-Z0-9]*, /bin/sync' > /etc/sudoers.d/tedge @@ -70,7 +70,7 @@ Trigger native-reboot within workflow (on_error) - missing sudoers entry for reb ${pid_after}= Execute Command sudo systemctl show --property MainPID tedge-agent Should Be Equal ${pid_before} ${pid_after} msg=tedge-agent should not have been restarted ${workflow_log}= Execute Command cat /var/log/tedge/agent/workflow-native-reboot-robot-2.log - Should Not Contain ${workflow_log} restarted: msg=restarted state should not have been executed + Should Not Contain ${workflow_log} item=status=restarted msg=restarted state should not have been executed Invoke sub-operation from a super-command operation Execute Command tedge mqtt pub --retain te/device/main///cmd/super_command/test-42 '{"status":"init", "output_file":"/tmp/test-42.json"}' @@ -81,13 +81,13 @@ Invoke sub-operation from a super-command operation Should Be Equal ${actual_log} ${expected_log} # Remove all dates from the workflow log ${workflow_log}= Execute Command cat /var/log/tedge/agent/workflow-super_command-test-42.log - Should Contain ${workflow_log} super_command/test-42/init: - Should Contain ${workflow_log} super_command/test-42/executing: - Should Contain ${workflow_log} super_command/test-42/awaiting_completion: - Should Contain ${workflow_log} sub_command/sub:super_command:test-42/init: msg=main command log should contain sub command steps - Should Contain ${workflow_log} sub_command/sub:super_command:test-42/executing: msg=main command log should contain sub command steps - Should Contain ${workflow_log} sub_command/sub:super_command:test-42/successful: msg=main command log should contain sub command steps - Should Contain ${workflow_log} super_command/test-42/successful: + Should Contain ${workflow_log} item=super_command/test-42 status=init + Should Contain ${workflow_log} item=super_command/test-42 status=executing + Should Contain ${workflow_log} item=super_command/test-42 status=awaiting_completion + Should Contain ${workflow_log} item=sub_command/sub:super_command:test-42 status=init msg=main command log should contain sub command steps + Should Contain ${workflow_log} item=sub_command/sub:super_command:test-42 status=executing msg=main command log should contain sub command steps + Should Contain ${workflow_log} item=sub_command/sub:super_command:test-42 status=successful msg=main command log should contain sub command steps + Should Contain ${workflow_log} item=super_command/test-42 status=successful Use scripts to create sub-operation init states Execute Command tedge mqtt pub --retain te/device/main///cmd/lite_device_profile/test-42 '{"status":"init", "logfile":"/tmp/profile-42.log", "profile":"/etc/tedge/operations/lite_device_profile.example.txt"}' diff --git a/tests/RobotFramework/tests/tedge_agent/workflows/lite_device_profile.toml b/tests/RobotFramework/tests/tedge_agent/workflows/lite_device_profile.toml index 3003c88a0ad..168a259278c 100644 --- a/tests/RobotFramework/tests/tedge_agent/workflows/lite_device_profile.toml +++ b/tests/RobotFramework/tests/tedge_agent/workflows/lite_device_profile.toml @@ -2,15 +2,15 @@ operation = "lite_device_profile" [init] action = "proceed" -on_success = "step_1" +on_success = "step_1_install" -[step_1] +[step_1_install] operation = "lite_software_update" input_script = "/etc/tedge/operations/extract_updates.sh step_1 install ${.payload.profile}" input.logfile = "${.payload.logfile}" -on_exec = "awaiting_step_1_completion" +on_exec = "awaiting_step_1_install_completion" -[awaiting_step_1_completion] +[awaiting_step_1_install_completion] action = "await-operation-completion" on_success = "step_1_config" @@ -22,15 +22,15 @@ on_exec = "awaiting_step_1_config_completion" [awaiting_step_1_config_completion] action = "await-operation-completion" -on_success = "step_2" +on_success = "step_2_install" -[step_2] +[step_2_install] operation = "lite_software_update" input_script = "/etc/tedge/operations/extract_updates.sh step_2 install ${.payload.profile}" input.logfile = "${.payload.logfile}" -on_exec = "awaiting_step_2_completion" +on_exec = "awaiting_step_2_install_completion" -[awaiting_step_2_completion] +[awaiting_step_2_install_completion] action = "await-operation-completion" on_success = "step_2_config"