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

Fix the c8y custom operation status handling #1641

Merged

Conversation

rina23q
Copy link
Member

@rina23q rina23q commented Dec 9, 2022

Proposed changes

A custom operation managed in /etc/tedge/operations/c8y could be marked successful unconditionally and the status had been sent out before the operation has completed.

To fix, now the Cumulocity converter holds a mqtt publisher and sends executing and successful/failed message from the converter. Either successful or failed is determined by the exit code (0 or not).

Also, stdout is reported when successful, and stderr for a failed case.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Improvement (general improvements like code refactoring that doesn't explicitly fix a bug or add any new functionality)
  • Documentation Update (if none of the other choices apply)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Paste Link to the issue

#1603

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA (in all commits with git commit -s)
  • I ran cargo fmt as mentioned in CODING_GUIDELINES
  • I used cargo clippy as mentioned in CODING_GUIDELINES
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Further comments

A custom operation managed in /etc/tedge/operations/c8y could be marked
successful unconditionally and the status had been sent out before the
operation has completed.

To fix, now the Cumulocity converter holds a mqtt publisher and sends
executing and successful/failed message from the converter. Either
successful or failed is determined by the exit code (0 or not).

Also, stdout is reported when successful, and stderr for a failed case.

Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
Reduce the arguments from 9 to 7 by introducing a new struct to keep
c8y device information.

Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
@rina23q rina23q force-pushed the bugfix/1603/fix-custom-command-operation-status branch from a92422d to b02e6a8 Compare December 9, 2022 01:48
@didier-wenzek didier-wenzek self-requested a review December 9, 2022 09:39
crates/tests/mqtt_tests/src/test_mqtt_server.rs Outdated Show resolved Hide resolved
crates/core/tedge_mapper/src/core/mapper.rs Outdated Show resolved Hide resolved
crates/core/tedge_mapper/src/c8y/converter.rs Outdated Show resolved Hide resolved
crates/core/tedge_mapper/src/c8y/converter.rs Outdated Show resolved Hide resolved
@rina23q
Copy link
Member Author

rina23q commented Dec 12, 2022

[Solved] cargo test never ends for some reason although it passes on my dev machine. I will work on that.

@reubenmiller reubenmiller added this to the 0.9.0 milestone Dec 12, 2022
match output.status.code() {
Some(0) => {
let stdout = String::from_utf8(output.stdout).unwrap_or_default();
let successful_str = format!("503,{op_name},\"{stdout}\"");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this useful to send the whole output of the command to the cloud?

If yes, one needs to escape any character misleading for CSV (notably the quotes ") and to conform to what is supported by Cumulocity (are multiline stdout supported?).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @reubenmiller, what to report to the cloud and how the message should look.

For successful case, sending the stdout contents as much as possible (within the size limit of c8y) with some sanitising. The detailed, you can find from #1603 "Result/failure message parsing rules".

My commit e76e172 changed this part as described in the original ticket.

}
_ => {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
let failed_str = format!("502,{op_name},\"{stderr}\"");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One needs to escape any character misleading for CSV (notably the quotes ") and to conform to what is supported by Cumulocity (are multiline stderr supported?).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @reubenmiller, what to report to the cloud and how the message should look.

For error case, sending the last line of stdout as failure reason. The detailed, you can find from #1603 "Result/failure message parsing rules".

My commit e76e172 changed this part as described in the original ticket.

crates/core/tedge_mapper/src/c8y/tests.rs Outdated Show resolved Hide resolved
let topic = C8yTopic::SmartRestResponse.to_topic().unwrap();
let executing_str = format!("501,{op_name}");
mqtt_publisher
.send(Message::new(&topic, executing_str.as_str()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I agree that sending this executing status here before the actual command itself is executed(which could take time to complete), provides better UX from the cloud, it is done at the cost of breaking the "converter" abstraction of it "just being a message converter without any MQTT business".

Since this is just a short term hack anyway, I was wondering why not keep it simple like this:

  1. Run the command and wait for the exit code.
  2. If exit code is 0, return both EXECUTING and SUCCESSFUL messages in the response Vec<Message>
  3. If exit code is 1, return both EXECUTING and FAILED messages in the response Vec<Message>.

I understand that the executing response will be delayed for long running operations and results in suboptimal UX. So, we need to choose between a "complex hack for better UX" for vs "simple hack for suboptimal UX".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, I feel that your current approach is closer to the actor model that we are going to, where the C8yMapperActor would receive the conversion response from the C8yConvertorActor over a channel, like the mqtt_publisher that you're sharing between the mapper and the converter.

Copy link
Contributor

@didier-wenzek didier-wenzek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To sanitize an utf8 string I would work at the char level and not to byte level.

Something along the lines:

input.chars().filter( ...).map(...).take(max).collect()

There are then 2 tricky points:

  • How to sanitize a non-utf8 input? One can use the current code.
  • How to cut the utf8 output to fit the maximum size that is a byte count? I would try to use an iter::scan that accumulates the number of bytes used so far.
        input
            .chars()
            .filter(...)
            .flat_map(...)
            .scan(0, |bytes_count, char| {
                *bytes_count = *bytes_count + number_bytes(char);
                Some( (*bytes_count, char))
            })
            .take_while(|(size, _)| { size <= max })
            .map(|(_,char)| {char})
            .collect();

crates/core/c8y_api/src/smartrest/message.rs Outdated Show resolved Hide resolved
crates/core/c8y_api/src/smartrest/message.rs Outdated Show resolved Hide resolved
crates/core/c8y_api/src/smartrest/message.rs Outdated Show resolved Hide resolved
crates/core/c8y_api/src/smartrest/message.rs Outdated Show resolved Hide resolved
Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
The output of command execution cannot be sent as it is, because
- it might contain unacceptable characters.
- double quotes need to be escaped.
- the total message size might be too big.

Also, the failure reason should be brief, therefore, the sending the
whole output of stderr is not appropriate.

This commit adds sanitizing functions and take only the last line for
error case to report.

Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
@rina23q rina23q force-pushed the bugfix/1603/fix-custom-command-operation-status branch from 0ac32b9 to 7e79c47 Compare December 16, 2022 12:21
@rina23q
Copy link
Member Author

rina23q commented Dec 16, 2022

To sanitize an utf8 string I would work at the char level and not to byte level.

Something along the lines:

input.chars().filter( ...).map(...).take(max).collect()

There are then 2 tricky points:

  • How to sanitize a non-utf8 input? One can use the current code.
  • How to cut the utf8 output to fit the maximum size that is a byte count? I would try to use an iter::scan that accumulates the number of bytes used so far.
        input
            .chars()
            .filter(...)
            .flat_map(...)
            .scan(0, |bytes_count, char| {
                *bytes_count = *bytes_count + number_bytes(char);
                Some( (*bytes_count, char))
            })
            .take_while(|(size, _)| { size <= max })
            .map(|(_,char)| {char})
            .collect();

Thanks @didier-wenzek. You are absolutely right. Now all sanitising functions were re-written.

Copy link
Contributor

@didier-wenzek didier-wenzek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved - with two comments.

/// Sanitize the input to be SmartREST compatible.
/// If the input contains invalid UTF-8, it returns an empty String.
/// - Remove all control characters except for `\n`, `\t`, `\r`.
/// - Double quote is escaped as `\"`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - Double quote is escaped as `\"`.
/// - Double quote is escaped as `\"\"`.

Comment on lines +43 to +44
.collect::<String>()
.replace('"', "\"\"")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This intermediate collect could be avoid by using a flat_map instead of the replace.

Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
@rina23q rina23q force-pushed the bugfix/1603/fix-custom-command-operation-status branch from 0b3713c to 1245231 Compare December 16, 2022 15:10
@rina23q rina23q merged commit ba41ee3 into thin-edge:main Dec 16, 2022
@rina23q rina23q deleted the bugfix/1603/fix-custom-command-operation-status branch December 16, 2022 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants