-
Notifications
You must be signed in to change notification settings - Fork 54
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
Add Cumulocity remote access plugin #1694
Conversation
e5a7e13
to
f043225
Compare
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
f043225
to
a8b60a8
Compare
There was a problem hiding this 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
futures = "0.3.25" | ||
futures-util = "0.3.25" | ||
http = "0.2.8" | ||
miette = { version = "5.5.0", features = ["fancy"] } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
ensure!( | ||
id == 530, | ||
"SmartREST message is not a RemoteAccessConnect operation" | ||
); |
There was a problem hiding this comment.
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.
Co-authored-by: Didier Wenzek <didier.wenzek@free.fr>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
There was a problem hiding this 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>
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
height: 500px; | ||
object-position: left; | ||
} | ||
</style> |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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")) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
) .
There was a problem hiding this comment.
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.
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
a4aa8f8
to
71dca79
Compare
There was a problem hiding this 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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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:
c8y-configuration-plugin --init |
use miette::IntoDiagnostic; | ||
use std::net::IpAddr; | ||
|
||
pub struct TedgeConfig { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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?
use tedge_config::C8yUrlSetting; | ||
use tedge_config::ConfigSettingAccessor; | ||
use tedge_config::MqttBindAddressSetting; | ||
use tedge_config::MqttPortSetting; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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")) | ||
} |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 | |||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
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
Suggested solution
Note
|
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. |
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. |
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. |
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 |
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
Signed-off-by: James Rhodes <james.rhodes@softwareag.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved
There was a problem hiding this 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") |
There was a problem hiding this comment.
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.
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 a530
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
Paste Link to the issue
Checklist
cargo fmt
as mentioned in CODING_GUIDELINEScargo clippy
as mentioned in CODING_GUIDELINESFurther comments