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

Add Cumulocity remote access plugin #1694

Merged

Conversation

jarhodes314
Copy link
Contributor

Signed-off-by: James Rhodes james.rhodes@softwareag.com

Proposed changes

This adds support for the c8y_RemoteAccessConnect operation, which enables the remote access tab in the Cumulocity UI. It reads a 530 SmartREST message, based on which it sets up a proxy between a TCP connection to the specified host and port (e.g. 127.0.0.1:22, whatever is specified by the user in the Cumulocity UI), and the corresponding Websocket on cloud. It then copies all data between this Websocket and the TCP socket and vice versa, shutting down when one or other of the connections closes.

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


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

Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
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.

Thank you. This looks good. And I have only an unrelated request regarding the Cargo.toml

plugins/c8y_remote_access_plugin/Cargo.toml Outdated Show resolved Hide resolved
futures = "0.3.25"
futures-util = "0.3.25"
http = "0.2.8"
miette = { version = "5.5.0", features = ["fancy"] }
Copy link
Contributor

Choose a reason for hiding this comment

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

First time I cross this crate, and I'm a bit curious as reporting errors to users is non-trivial.

What's your experience with it? Would you recommend to use it for tedge?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer the output compared to anyhow, and it obviously has support for more complex things (although with a lot of focus on parse errors). I don't think it makes a huge difference, but it's an easy drop-in replacement for anyhow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For instance, by slightly tweaking tedge_config to contain something like this

#[derive(miette::Diagnostic, thiserror::Error, Debug)]
pub enum ConfigSettingError {
    #[error("A value for `{key}` is missing.")]
    #[diagnostic(help("A value can be set with `tedge config set {key} <value>`"))]
    ConfigNotSet { key: &'static str },

   ...
}

I could get this output (with colour when running in a terminal):

Error: 
  × A value for `c8y.url` is missing.
  help: A value can be set with `tedge config set c8y.url <value>`

}

impl TedgeConfig {
pub async fn read_from_disk() -> miette::Result<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a crate tedge_config that we are supposed to use to read /etc/tedge/tedge.toml.

Reading these straightforward lines of code, one really wonders why tedge_config is so huge. To be fair with this crate, it has been written with strong validation constraints that have been removed meantime.

Comment on lines +28 to +31
ensure!(
id == 530,
"SmartREST message is not a RemoteAccessConnect operation"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I really like this style to validate inputs.

One must consider this for thin-edge inputs.

plugins/c8y_remote_access_plugin/src/proxy.rs Outdated Show resolved Hide resolved
jarhodes314 and others added 2 commits January 26, 2023 11:30
Co-authored-by: Didier Wenzek <didier.wenzek@free.fr>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
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.

I will approve. However can you fix:

  • Cargo.lock
  • cargo +nightly fmt
  • cargo clippy -- -D warnings.

Thank you!

Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
@reubenmiller reubenmiller added idea ideas/opportunities/feature requests which need to be further investigated before implementation theme:troubleshooting Theme: Troubleshooting and remote control theme:plugins Theme: Plugin interfaces or general plugin topics which don't fit in any other themes labels Jan 27, 2023
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
@@ -0,0 +1,42 @@
# Cumulocity RemoteAcessConnect plugin

To access a device remotely that runs thin-edge.io, a plugin of the operation plugin concept is used. The tedge_agent is checking for cloud remote access operation and is triggering the particular plugin. You can use the remote access tab in device management to access the device via SSH or VNC.
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
To access a device remotely that runs thin-edge.io, a plugin of the operation plugin concept is used. The tedge_agent is checking for cloud remote access operation and is triggering the particular plugin. You can use the remote access tab in device management to access the device via SSH or VNC.
To access a device remotely that runs thin-edge.io, a plugin of the operation plugin concept is used. The tedge-agent is checking for cloud remote access operation and is triggering the particular plugin. You can use the remote access tab in device management to access the device via SSH or VNC.

docs/src/howto-guides/027_remote_access_with_cumulocity.md Outdated Show resolved Hide resolved
height: 500px;
object-position: left;
}
</style>
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel the doc must explain how to start the remote access plugin. Is this going to be a systemd service?

Copy link
Contributor

Choose a reason for hiding this comment

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

No this plugin is launched by the mapper on Smartrest 530.

Copy link
Contributor

Choose a reason for hiding this comment

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

If mapper launches this by mapper then it will be mandatory right? If someone does not need this feature then one can't disable it, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not entirely sure what the install flow should be for this, but this isn't really true in any case, as the user would just need to delete /etc/tedge/operations/c8y/c8y_RemoteAccessConnect to disable this, and then they can delete /usr/bin/c8y_remote_access_plugin as it's no longer needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

@reubenmiller can you please help us here, how you would like to package and install it?

Copy link
Contributor

Choose a reason for hiding this comment

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

The /etc/tedge/operations/c8y/c8y_RemoteAccessConnect file should only be installed when the c8y-remote-access-plugin.deb package is installed.

And if the c8y-remote-access-plugin package is removed, then it should also delete the /etc/tedge/operations/c8y/c8y_RemoteAccessConnect file.

.await
.into_diagnostic()
.with_context(|| format!("Waiting {duration:?} for Cumulocity to respond with JWT"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The above code is for retrieving the JWT token right. There is already a library that exists, can reuse this code I feel.

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately there is no true API for that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just reuse C8YHttpProxy::get_jwt_token? Would add a dependency on c8y_api which I feel is OK.

Copy link
Contributor

Choose a reason for hiding this comment

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

The issue with C8yJwtTokenRetriever::get_jwt_token is that this comes with a lot of specific stuffs (whole TEdgeConfig, SMCumulocityMapperError, SmartRestJwtResponse ...).

So, yes, it would be good to share this code by behind a lighter API (MqttConfig, JwtToken, JwtError) .

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we park the "lighter API" part for another ticket? And for now use the C8YHttpProxy::get_jwt_token as the c8y-remote-access-plugin is hosted in the thin-edge.io repository, and it will be eventually merged into the future c8y-devicemanagement-plugin once the refactoring topic is done.

plugins/c8y_remote_access_plugin/src/main.rs Outdated Show resolved Hide resolved
plugins/c8y_remote_access_plugin/src/main.rs Outdated Show resolved Hide resolved
plugins/c8y_remote_access_plugin/src/main.rs Outdated Show resolved Hide resolved
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
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.

I will merge it despite the error on Cargo.lock. However, the check for unused deps is also failing now.

@@ -186,6 +186,22 @@ fn create_directories(config_dir: &Path) -> Result<(), anyhow::Error> {
0o644,
None,
)?;
create_file_with_user_group(
Copy link
Contributor

Choose a reason for hiding this comment

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

This code should be moved form this main mapper to the plugin itself so that this operation is enabled only when the plugin is installed. For reference check the c8y_config_plugin impl here:

create_file_with_user_group(

Copy link
Contributor

Choose a reason for hiding this comment

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

The convention that we use to do such initialisations during installation is by adding a --init option to the script, which is invoked during the installation the debian package via its postinst script.

Here is how it's done by the c8y-config-plugin:

fn init(cfg_dir: PathBuf) -> Result<(), anyhow::Error> {

And here is the postinst script:

use miette::IntoDiagnostic;
use std::net::IpAddr;

pub struct TedgeConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd have named this the RemoteAccessPluginConfig to avoid any ambiguity with tedge_config::TEdgeConfig.

}

impl TedgeConfig {
pub fn read_from_disk() -> miette::Result<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: How about implementing a TryFrom<TEdgeConfig> for RemoteAccessPluginConfig instead of this function?

Comment on lines 21 to 24
use tedge_config::C8yUrlSetting;
use tedge_config::ConfigSettingAccessor;
use tedge_config::MqttBindAddressSetting;
use tedge_config::MqttPortSetting;
Copy link
Contributor

Choose a reason for hiding this comment

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

Quite an unusual style, at least for me. Are there any benefits in keeping these use statements at the method level instead of the module?

Copy link
Contributor

Choose a reason for hiding this comment

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

On the contrary, I like this style that avoids to pollute the module namespace.

.await
.into_diagnostic()
.with_context(|| format!("Waiting {duration:?} for Cumulocity to respond with JWT"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just reuse C8YHttpProxy::get_jwt_token? Would add a dependency on c8y_api which I feel is OK.


select(incoming, outgoing).await;
}
println!("STOPPING");
Copy link
Contributor

Choose a reason for hiding this comment

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

When does this plugin stop realistically? Can we "end" the session remotely from the cloud "Remote Access" UI?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Usually it would stop when someone closes the VNC/SSH window, which causes the Websocket to be closed.

@@ -26,3 +26,4 @@
24. [How to manage configuration files with Cumulocity](./025_config_management_plugin.md)
25. [How to install thin-edge manually with openrc](./026_how_to_install_thin_edge_manually.md)
26. [How to enable configuration management on child devices](./child_device_config_management_agent.md)
27. [How to connect to your thin-edge.io device with Cumulocity remote access](./027_remote_access_with_cumulocity.md)
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
27. [How to connect to your thin-edge.io device with Cumulocity remote access](./027_remote_access_with_cumulocity.md)
27. [How to remotely connect to your thin-edge.io device via SSH/VNC/Telnet with Cumulocity remote access](./027_remote_access_with_cumulocity.md)

@@ -0,0 +1,41 @@
# Cumulocity RemoteAcessConnect plugin

Copy link
Contributor

Choose a reason for hiding this comment

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

A link to the Cumulocity remote access feature somewhere in the doc would be nice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes I would agree, a link to the Cumulocity IoT docs would be useful, as one of the default mistakes is to forget to add the Remote access admin permission to your user/role. Otherwise you don't see the Remote access tab in the Device Management application

Copy link
Contributor

@reubenmiller reubenmiller left a comment

Choose a reason for hiding this comment

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

The c8y-remote-access-plugin entry needs to be added to ci/package_list.sh which is used by variables scripts (most notable the main build script ci/build_scripts/build.sh).

For example just add the package to the RELEASE_PACKAGES entries:

RELEASE_PACKAGES=(
    tedge
    tedge-mapper
    tedge-agent
    tedge-watchdog
    tedge-apt-plugin
    c8y-log-plugin
    c8y-configuration-plugin
    c8y-remote-access-plugin
)

Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
}

#[derive(Debug, PartialEq, Eq)]
pub enum Command {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would have been nice if clap could parse the input arguments to this enum directly as that would eliminate the need for us to validate exclusive arg combinations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't think this is possible unfortunately. The #[clap(group(...))] does at least give good error messages when multiple arguments are provided.

plugins/c8y_remote_access_plugin/src/auth.rs Show resolved Hide resolved
plugins/c8y_remote_access_plugin/src/config.rs Outdated Show resolved Hide resolved
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
@reubenmiller
Copy link
Contributor

reubenmiller commented Feb 1, 2023

I retested the changes and the install/uninstall now works perfectly (except for the fact that the mapper needs to be restarted sometimes, but that is not in the scope of this ticket).

However we do need to change how/when the local proxy is being spawned and when the main process should return to tedge (so that the operation status is transitioned in the correct order).

At the moment, the plugin only returns when the plugin process is finished. This means if there are two connections at the same time, the wrong operation will be transitioned in the cloud.

Example

The following process leads to the incorrect Cumulocity IoT Operation being updated to SUCCESSFUL. The actions are performed on the same device.

  1. User A connects using webssh. Operation 1 is kept in EXECUTING status until the window is closed
  2. User B connects using webssh. Operation 2 is kept in EXECUTING status until the window is closed
  3. User B closes the window first. This results in Operation 1 being set to SUCCESSFUL instead of Operation 2!!!

Suggested solution

  1. tedge launches the plugin
  2. The plugin creates a background process which tries to connect to the endpoint given to the plugin via the arguments.
    1. If the child process successfully connects to the endpoint, then it should signal back to the main process that the connection was successful.
    2. If the child process is not able to successfully connect to the endpoint, then it should signal back to the main process that the connection failed (with a fitting failure reason)
  3. The main process waits for a success or failure signal from the child process (also using a timeout if it does not receive anything from the service)
  4. The main process exits with an exit code of 0 or non-zero, depending on the signal received from the child process. The child process should continue running after the main process exits.

Note

  • The tricky part might be the detection if the child process is "successful" or not...if the criteria is too difficult to define or observe, then we could always just send a "SUCCESSFUL" message back to tedge. But ideally having a SUCCESSFUL or FAILED status would be the most useful for users, as they know when something did not work correctly.

@didier-wenzek
Copy link
Contributor

However we do need to change how/when the local proxy is being spawned and when the main process should return to tedge (so that the operation status is transitioned in the correct order).

I fully understand the need. However, this cannot be done easily/quickly as described because as of now there is no communication channel between the mapper and the plugin process. Adding this channel is beyond the scope of this PR.

What can be done, as a workaround for that PR, is to let the plugin itself send the executing status of the operation to C8Y.

@albinsuresh
Copy link
Contributor

What can be done, as a o for that PR, is to let the plugin itself send the executing status of the operation to C8Y.

Wondering if this will lead to double reporting of the operation statuses, which would still lead to interference with other operations of the same kind. For eg: the plugin first sends the successful operation status and then the mapper also sends the same successful status on exit code 0. The second message sent by the mapper could then interfere with another "executing" operation. Unless there is some mechanism in the mapper with which the plugin can tell the mapper not to send the operation status.

If such a mechanism doesn't exist, I'd be in favour of merging this PR now and introducing that feature in the mapper in a follow-up task.

@reubenmiller
Copy link
Contributor

However we do need to change how/when the local proxy is being spawned and when the main process should return to tedge (so that the operation status is transitioned in the correct order).

I fully understand the need. However, this cannot be done easily/quickly as described because as of now there is no communication channel between the mapper and the plugin process. Adding this channel is beyond the scope of this PR.

What can be done, as a workaround for that PR, is to let the plugin itself send the executing status of the operation to C8Y.

As a compromise I would then propose that main process exits as soon as the background has been spawned. As the operation is just asking to "establish" a connection, not wait until the connection has been closed again. That should be able to be quickly done.

@jarhodes314
Copy link
Contributor Author

I fully understand the need. However, this cannot be done easily/quickly as described because as of now there is no communication channel between the mapper and the plugin process. Adding this channel is beyond the scope of this PR.

What can be done, as a workaround for that PR, is to let the plugin itself send the executing status of the operation to C8Y.

I'm not sure this is necessarily that complicated to achieve. All the logic for this would be within the plugin. As far as the mapper is concerned, the plugin exits within a few seconds to indicate success or failure of the connection.

Communicating between the plugin and a child process can be done in a similar way to how the PoC implementation spawned tedge to handle MQTT communication, so I don't anticipate problems there.

Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
@reubenmiller
Copy link
Contributor

reubenmiller commented Feb 1, 2023

Tested the updated version and works like a dream :) The operation is transitioned to either SUCCESSFUL/FAILED after feedback from the child process is returned. So the operation does not stay in the EXECUTING state for long (which reduces the risk of updating the wrong operation).

We also have an failure reason (which is super useful for users!). Below is an example error when the ssh port is misconfigured (or not open)

image

And the happy case, when the operation is set to SUCCESSFUL but the connection is still alive :)
image

Copy link
Contributor

@reubenmiller reubenmiller left a comment

Choose a reason for hiding this comment

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

Approved

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

struct Unreachable<E: std::error::Error + 'static>(#[source] E, &'static str);

async fn spawn_child(command: String) -> miette::Result<()> {
let mut command = tokio::process::Command::new("/usr/bin/c8y-remote-access-plugin")
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better to use the $0 argument of the process, rather than assuming the plugin installed at a very specific location.

@didier-wenzek didier-wenzek merged commit a01187d into thin-edge:main Feb 1, 2023
@reubenmiller reubenmiller mentioned this pull request Feb 1, 2023
11 tasks
@reubenmiller reubenmiller added this to the 0.10.0 milestone Feb 10, 2023
@gligorisaev gligorisaev self-assigned this Feb 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
idea ideas/opportunities/feature requests which need to be further investigated before implementation release:highlight theme:plugins Theme: Plugin interfaces or general plugin topics which don't fit in any other themes theme:troubleshooting Theme: Troubleshooting and remote control
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants