From 7f35bc17759921582c01f3c40ae15d962cab0518 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Fri, 24 Jan 2025 10:08:53 +0100 Subject: [PATCH 01/10] Introduce tedge http command Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 118 ++++++++++++++++++++++ crates/core/tedge/src/cli/http/command.rs | 35 +++++++ crates/core/tedge/src/cli/http/mod.rs | 4 + crates/core/tedge/src/cli/mod.rs | 6 ++ 4 files changed, 163 insertions(+) create mode 100644 crates/core/tedge/src/cli/http/cli.rs create mode 100644 crates/core/tedge/src/cli/http/command.rs create mode 100644 crates/core/tedge/src/cli/http/mod.rs diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs new file mode 100644 index 0000000000..f26262ec07 --- /dev/null +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -0,0 +1,118 @@ +use crate::cli::http::command::HttpCommand; +use crate::command::BuildCommand; +use crate::command::BuildContext; +use crate::command::Command; +use crate::ConfigError; +use anyhow::anyhow; +use anyhow::Error; +use certificate::CloudRootCerts; +use reqwest::blocking; +use reqwest::Identity; +use tedge_config::OptionalConfig; +use tedge_config::ProfileName; + +#[derive(clap::Subcommand, Debug)] +pub enum TEdgeHttpCli { + /// POST content to thin-edge local HTTP servers + Post { + /// Target URI + uri: String, + + /// Content to post + content: String, + + /// Optional c8y cloud profile + #[clap(long)] + profile: Option, + }, + + /// GET content from thin-edge local HTTP servers + Get { + /// Source URI + uri: String, + + /// Optional c8y cloud profile + #[clap(long)] + profile: Option, + }, +} + +impl BuildCommand for TEdgeHttpCli { + fn build_command(self, context: BuildContext) -> Result, ConfigError> { + let config = context.load_config()?; + let uri = self.uri(); + + let (protocol, host, port) = if uri.starts_with("/c8y") { + let c8y_config = config.c8y.try_get(self.c8y_profile())?; + let client = &c8y_config.proxy.client; + let protocol = https_if_some(&c8y_config.proxy.cert_path); + (protocol, client.host.clone(), client.port) + } else if uri.starts_with("/tedge") { + let client = &config.http.client; + let protocol = https_if_some(&config.http.cert_path); + (protocol, client.host.clone(), client.port) + } else { + return Err(anyhow!("Not a local HTTP uri: {uri}").into()); + }; + + let url = format!("{protocol}://{host}:{port}{uri}"); + let verb_url = format!("{} {url}", self.verb()); + let identity = config.http.client.auth.identity()?; + let client = http_client(config.cloud_root_certs(), identity.as_ref())?; + + let request = match self { + TEdgeHttpCli::Post { content, .. } => client + .post(url) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .body(content), + TEdgeHttpCli::Get { .. } => client.get(url).header("Accept", "application/json"), + }; + + Ok(HttpCommand { + url: verb_url, + request, + } + .into_boxed()) + } +} + +impl TEdgeHttpCli { + fn uri(&self) -> &str { + match self { + TEdgeHttpCli::Post { uri, .. } | TEdgeHttpCli::Get { uri, .. } => uri.as_ref(), + } + } + + fn verb(&self) -> &str { + match self { + TEdgeHttpCli::Post { .. } => "POST", + TEdgeHttpCli::Get { .. } => "GET", + } + } + + fn c8y_profile(&self) -> Option<&ProfileName> { + match self { + TEdgeHttpCli::Post { profile, .. } | TEdgeHttpCli::Get { profile, .. } => { + profile.as_ref() + } + } + } +} + +fn https_if_some(cert_path: &OptionalConfig) -> &'static str { + cert_path.or_none().map_or("http", |_| "https") +} + +fn http_client( + root_certs: CloudRootCerts, + identity: Option<&Identity>, +) -> Result { + let builder = root_certs.blocking_client_builder(); + let builder = if let Some(identity) = identity { + builder.identity(identity.clone()) + } else { + builder + }; + Ok(builder.build()?) +} diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs new file mode 100644 index 0000000000..d09b522f49 --- /dev/null +++ b/crates/core/tedge/src/cli/http/command.rs @@ -0,0 +1,35 @@ +use crate::command::Command; +use crate::log::MaybeFancy; +use anyhow::Error; +use reqwest::blocking; + +pub struct HttpCommand { + /// Target url + pub url: String, + + /// HTTP request + pub request: blocking::RequestBuilder, +} + +impl Command for HttpCommand { + fn description(&self) -> String { + self.url.clone() + } + + fn execute(&self) -> Result<(), MaybeFancy> { + Ok(self.send()?) + } +} + +impl HttpCommand { + fn send(&self) -> Result<(), Error> { + if let Some(request) = self.request.try_clone() { + let http_result = request.send()?; + let http_response = http_result.error_for_status()?; + let bytes = http_response.bytes()?.to_vec(); + let content = String::from_utf8(bytes)?; + println!("{content}"); + } + Ok(()) + } +} diff --git a/crates/core/tedge/src/cli/http/mod.rs b/crates/core/tedge/src/cli/http/mod.rs new file mode 100644 index 0000000000..6e318796c4 --- /dev/null +++ b/crates/core/tedge/src/cli/http/mod.rs @@ -0,0 +1,4 @@ +mod cli; +mod command; + +pub use cli::TEdgeHttpCli; diff --git a/crates/core/tedge/src/cli/mod.rs b/crates/core/tedge/src/cli/mod.rs index 07a1802ccc..81fbc6d52e 100644 --- a/crates/core/tedge/src/cli/mod.rs +++ b/crates/core/tedge/src/cli/mod.rs @@ -21,6 +21,7 @@ mod completions; pub mod config; mod connect; mod disconnect; +mod http; mod init; pub mod log; mod mqtt; @@ -124,6 +125,10 @@ pub enum TEdgeOpt { #[clap(subcommand)] Mqtt(mqtt::TEdgeMqttCli), + /// Send HTTP requests to local thin-edge HTTP servers + #[clap(subcommand)] + Http(http::TEdgeHttpCli), + /// Run thin-edge services and plugins Run(ComponentOpt), @@ -196,6 +201,7 @@ impl BuildCommand for TEdgeOpt { TEdgeOpt::Disconnect(opt) => opt.build_command(context), TEdgeOpt::RefreshBridges => RefreshBridgesCmd::new(&context).map(Command::into_boxed), TEdgeOpt::Mqtt(opt) => opt.build_command(context), + TEdgeOpt::Http(opt) => opt.build_command(context), TEdgeOpt::Reconnect(opt) => opt.build_command(context), TEdgeOpt::Run(_) => { // This method has to be kept in sync with tedge::redirect_if_multicall() From bb192d3c5f9c620c2c0d3eebbcc9d58d17bfc419 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Wed, 29 Jan 2025 16:49:06 +0100 Subject: [PATCH 02/10] Add support for tedge http put/delete Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 42 ++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index f26262ec07..7bcf234831 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -26,6 +26,19 @@ pub enum TEdgeHttpCli { profile: Option, }, + /// PUT content to thin-edge local HTTP servers + Put { + /// Target URI + uri: String, + + /// Content to post + content: String, + + /// Optional c8y cloud profile + #[clap(long)] + profile: Option, + }, + /// GET content from thin-edge local HTTP servers Get { /// Source URI @@ -35,6 +48,16 @@ pub enum TEdgeHttpCli { #[clap(long)] profile: Option, }, + + /// DELETE resource from thin-edge local HTTP servers + Delete { + /// Source URI + uri: String, + + /// Optional c8y cloud profile + #[clap(long)] + profile: Option, + }, } impl BuildCommand for TEdgeHttpCli { @@ -66,7 +89,12 @@ impl BuildCommand for TEdgeHttpCli { .header("Accept", "application/json") .header("Content-Type", "application/json") .body(content), + TEdgeHttpCli::Put { content, .. } => client + .put(url) + .header("Content-Type", "application/json") + .body(content), TEdgeHttpCli::Get { .. } => client.get(url).header("Accept", "application/json"), + TEdgeHttpCli::Delete { .. } => client.delete(url), }; Ok(HttpCommand { @@ -80,22 +108,28 @@ impl BuildCommand for TEdgeHttpCli { impl TEdgeHttpCli { fn uri(&self) -> &str { match self { - TEdgeHttpCli::Post { uri, .. } | TEdgeHttpCli::Get { uri, .. } => uri.as_ref(), + TEdgeHttpCli::Post { uri, .. } + | TEdgeHttpCli::Put { uri, .. } + | TEdgeHttpCli::Get { uri, .. } + | TEdgeHttpCli::Delete { uri, .. } => uri.as_ref(), } } fn verb(&self) -> &str { match self { TEdgeHttpCli::Post { .. } => "POST", + TEdgeHttpCli::Put { .. } => "PUT", TEdgeHttpCli::Get { .. } => "GET", + TEdgeHttpCli::Delete { .. } => "DELETE", } } fn c8y_profile(&self) -> Option<&ProfileName> { match self { - TEdgeHttpCli::Post { profile, .. } | TEdgeHttpCli::Get { profile, .. } => { - profile.as_ref() - } + TEdgeHttpCli::Post { profile, .. } + | TEdgeHttpCli::Put { profile, .. } + | TEdgeHttpCli::Get { profile, .. } + | TEdgeHttpCli::Delete { profile, .. } => profile.as_ref(), } } } From 09517fa9a04a11269530820708c948da334e2ab8 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Tue, 4 Feb 2025 10:21:06 +0100 Subject: [PATCH 03/10] Posting a file with tedge http post --file Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 80 +++++++++++++++-------- crates/core/tedge/src/cli/http/command.rs | 62 ++++++++++++++---- 2 files changed, 102 insertions(+), 40 deletions(-) diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index 7bcf234831..f120b29589 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -1,3 +1,4 @@ +use crate::cli::http::command::HttpAction; use crate::cli::http::command::HttpCommand; use crate::command::BuildCommand; use crate::command::BuildContext; @@ -5,9 +6,12 @@ use crate::command::Command; use crate::ConfigError; use anyhow::anyhow; use anyhow::Error; +use camino::Utf8PathBuf; use certificate::CloudRootCerts; +use clap::Args; use reqwest::blocking; use reqwest::Identity; +use std::fs::File; use tedge_config::OptionalConfig; use tedge_config::ProfileName; @@ -18,8 +22,9 @@ pub enum TEdgeHttpCli { /// Target URI uri: String, - /// Content to post - content: String, + /// Content to send + #[command(flatten)] + content: Content, /// Optional c8y cloud profile #[clap(long)] @@ -31,8 +36,9 @@ pub enum TEdgeHttpCli { /// Target URI uri: String, - /// Content to post - content: String, + /// Content to send + #[command(flatten)] + content: Content, /// Optional c8y cloud profile #[clap(long)] @@ -60,6 +66,40 @@ pub enum TEdgeHttpCli { }, } +#[derive(Args, Clone, Debug)] +#[group(required = true, multiple = false)] +pub struct Content { + /// Content to send + #[arg(name = "content")] + arg2: Option, + + /// Content to send + #[arg(long)] + data: Option, + + /// File which content is sent + #[arg(long)] + file: Option, +} + +impl TryFrom for blocking::Body { + type Error = std::io::Error; + + fn try_from(content: Content) -> Result { + let body: blocking::Body = if let Some(data) = content.arg2 { + data.into() + } else if let Some(data) = content.data { + data.into() + } else if let Some(file) = content.file { + File::open(file)?.into() + } else { + "".into() + }; + + Ok(body) + } +} + impl BuildCommand for TEdgeHttpCli { fn build_command(self, context: BuildContext) -> Result, ConfigError> { let config = context.load_config()?; @@ -79,27 +119,20 @@ impl BuildCommand for TEdgeHttpCli { }; let url = format!("{protocol}://{host}:{port}{uri}"); - let verb_url = format!("{} {url}", self.verb()); let identity = config.http.client.auth.identity()?; let client = http_client(config.cloud_root_certs(), identity.as_ref())?; - let request = match self { - TEdgeHttpCli::Post { content, .. } => client - .post(url) - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .body(content), - TEdgeHttpCli::Put { content, .. } => client - .put(url) - .header("Content-Type", "application/json") - .body(content), - TEdgeHttpCli::Get { .. } => client.get(url).header("Accept", "application/json"), - TEdgeHttpCli::Delete { .. } => client.delete(url), + let action = match self { + TEdgeHttpCli::Post { content, .. } => HttpAction::Post(content), + TEdgeHttpCli::Put { content, .. } => HttpAction::Put(content), + TEdgeHttpCli::Get { .. } => HttpAction::Get, + TEdgeHttpCli::Delete { .. } => HttpAction::Delete, }; Ok(HttpCommand { - url: verb_url, - request, + client, + url, + action, } .into_boxed()) } @@ -115,15 +148,6 @@ impl TEdgeHttpCli { } } - fn verb(&self) -> &str { - match self { - TEdgeHttpCli::Post { .. } => "POST", - TEdgeHttpCli::Put { .. } => "PUT", - TEdgeHttpCli::Get { .. } => "GET", - TEdgeHttpCli::Delete { .. } => "DELETE", - } - } - fn c8y_profile(&self) -> Option<&ProfileName> { match self { TEdgeHttpCli::Post { profile, .. } diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs index d09b522f49..a1dcbc13f2 100644 --- a/crates/core/tedge/src/cli/http/command.rs +++ b/crates/core/tedge/src/cli/http/command.rs @@ -1,35 +1,73 @@ +use crate::cli::http::cli::Content; use crate::command::Command; use crate::log::MaybeFancy; use anyhow::Error; use reqwest::blocking; pub struct HttpCommand { + /// HTTP client + pub client: blocking::Client, + /// Target url pub url: String, - /// HTTP request - pub request: blocking::RequestBuilder, + /// Action + pub action: HttpAction, +} + +pub enum HttpAction { + Post(Content), + Put(Content), + Get, + Delete, } impl Command for HttpCommand { fn description(&self) -> String { - self.url.clone() + let verb = match self.action { + HttpAction::Post(_) => "POST", + HttpAction::Put(_) => "PUT", + HttpAction::Get => "GET", + HttpAction::Delete => "DELETE", + }; + format!("{verb} {}", self.url) } fn execute(&self) -> Result<(), MaybeFancy> { - Ok(self.send()?) + let request = self.request()?; + HttpCommand::send(request)?; + Ok(()) } } impl HttpCommand { - fn send(&self) -> Result<(), Error> { - if let Some(request) = self.request.try_clone() { - let http_result = request.send()?; - let http_response = http_result.error_for_status()?; - let bytes = http_response.bytes()?.to_vec(); - let content = String::from_utf8(bytes)?; - println!("{content}"); - } + fn request(&self) -> Result { + let client = &self.client; + let url = &self.url; + let request = match &self.action { + HttpAction::Post(content) => client + .post(url) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .body(blocking::Body::try_from(content.clone())?), + HttpAction::Put(content) => client + .put(url) + .header("Content-Type", "application/json") + .body(blocking::Body::try_from(content.clone())?), + HttpAction::Get => client.get(url).header("Accept", "application/json"), + HttpAction::Delete => client.delete(url), + }; + + Ok(request) + } + + fn send(request: blocking::RequestBuilder) -> Result<(), Error> { + let http_result = request.send()?; + let http_response = http_result.error_for_status()?; + let bytes = http_response.bytes()?.to_vec(); + let content = String::from_utf8(bytes)?; + + println!("{content}"); Ok(()) } } From 788e74f3d58e963ed130ba667fceea3ab02c7774 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Tue, 4 Feb 2025 17:58:14 +0100 Subject: [PATCH 04/10] Set content type posted by tedge http post Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 59 ++++++++++++++++++++--- crates/core/tedge/src/cli/http/command.rs | 40 ++++++++++----- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index f120b29589..207fc7cd31 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -26,6 +26,16 @@ pub enum TEdgeHttpCli { #[command(flatten)] content: Content, + /// MIME type of the content + #[clap(long, default_value = "application/json")] + #[arg(value_parser = parse_mime_type)] + content_type: String, + + /// MIME type of the expected content + #[clap(long, default_value = "application/json")] + #[arg(value_parser = parse_mime_type)] + accept_type: String, + /// Optional c8y cloud profile #[clap(long)] profile: Option, @@ -40,6 +50,11 @@ pub enum TEdgeHttpCli { #[command(flatten)] content: Content, + /// MIME type of the content + #[clap(long, default_value = "application/json")] + #[arg(value_parser = parse_mime_type)] + content_type: String, + /// Optional c8y cloud profile #[clap(long)] profile: Option, @@ -50,6 +65,11 @@ pub enum TEdgeHttpCli { /// Source URI uri: String, + /// MIME type of the expected content + #[clap(long, default_value = "application/json")] + #[arg(value_parser = parse_mime_type)] + accept_type: String, + /// Optional c8y cloud profile #[clap(long)] profile: Option, @@ -82,6 +102,10 @@ pub struct Content { file: Option, } +fn parse_mime_type(input: &str) -> Result { + Ok(input.parse::()?.to_string()) +} + impl TryFrom for blocking::Body { type Error = std::io::Error; @@ -121,13 +145,7 @@ impl BuildCommand for TEdgeHttpCli { let url = format!("{protocol}://{host}:{port}{uri}"); let identity = config.http.client.auth.identity()?; let client = http_client(config.cloud_root_certs(), identity.as_ref())?; - - let action = match self { - TEdgeHttpCli::Post { content, .. } => HttpAction::Post(content), - TEdgeHttpCli::Put { content, .. } => HttpAction::Put(content), - TEdgeHttpCli::Get { .. } => HttpAction::Get, - TEdgeHttpCli::Delete { .. } => HttpAction::Delete, - }; + let action = self.into(); Ok(HttpCommand { client, @@ -138,6 +156,33 @@ impl BuildCommand for TEdgeHttpCli { } } +impl From for HttpAction { + fn from(value: TEdgeHttpCli) -> Self { + match value { + TEdgeHttpCli::Post { + content, + content_type, + accept_type, + .. + } => HttpAction::Post { + content, + content_type, + accept_type, + }, + TEdgeHttpCli::Put { + content, + content_type, + .. + } => HttpAction::Put { + content, + content_type, + }, + TEdgeHttpCli::Get { accept_type, .. } => HttpAction::Get { accept_type }, + TEdgeHttpCli::Delete { .. } => HttpAction::Delete, + } + } +} + impl TEdgeHttpCli { fn uri(&self) -> &str { match self { diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs index a1dcbc13f2..78ea4c2075 100644 --- a/crates/core/tedge/src/cli/http/command.rs +++ b/crates/core/tedge/src/cli/http/command.rs @@ -16,18 +16,27 @@ pub struct HttpCommand { } pub enum HttpAction { - Post(Content), - Put(Content), - Get, + Post { + content: Content, + content_type: String, + accept_type: String, + }, + Put { + content: Content, + content_type: String, + }, + Get { + accept_type: String, + }, Delete, } impl Command for HttpCommand { fn description(&self) -> String { let verb = match self.action { - HttpAction::Post(_) => "POST", - HttpAction::Put(_) => "PUT", - HttpAction::Get => "GET", + HttpAction::Post { .. } => "POST", + HttpAction::Put { .. } => "PUT", + HttpAction::Get { .. } => "GET", HttpAction::Delete => "DELETE", }; format!("{verb} {}", self.url) @@ -45,16 +54,23 @@ impl HttpCommand { let client = &self.client; let url = &self.url; let request = match &self.action { - HttpAction::Post(content) => client + HttpAction::Post { + content, + content_type, + accept_type, + } => client .post(url) - .header("Accept", "application/json") - .header("Content-Type", "application/json") + .header("Accept", accept_type) + .header("Content-Type", content_type) .body(blocking::Body::try_from(content.clone())?), - HttpAction::Put(content) => client + HttpAction::Put { + content, + content_type, + } => client .put(url) - .header("Content-Type", "application/json") + .header("Content-Type", content_type) .body(blocking::Body::try_from(content.clone())?), - HttpAction::Get => client.get(url).header("Accept", "application/json"), + HttpAction::Get { accept_type } => client.get(url).header("Accept", accept_type), HttpAction::Delete => client.delete(url), }; From db15573be09cf1f51f12371fcbfc70f414048c53 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Fri, 7 Feb 2025 10:39:20 +0100 Subject: [PATCH 05/10] Stream tedge http get response Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/command.rs | 7 ++----- .../tests/tedge/http_file_transfer_api.robot | 9 +++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs index 78ea4c2075..261b34b39a 100644 --- a/crates/core/tedge/src/cli/http/command.rs +++ b/crates/core/tedge/src/cli/http/command.rs @@ -79,11 +79,8 @@ impl HttpCommand { fn send(request: blocking::RequestBuilder) -> Result<(), Error> { let http_result = request.send()?; - let http_response = http_result.error_for_status()?; - let bytes = http_response.bytes()?.to_vec(); - let content = String::from_utf8(bytes)?; - - println!("{content}"); + let mut http_response = http_result.error_for_status()?; + http_response.copy_to(&mut std::io::stdout())?; Ok(()) } } diff --git a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot index 5bd34ce4d1..fe1d94895c 100644 --- a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot +++ b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot @@ -27,6 +27,15 @@ Get Put Delete Should Be Equal ${get} test of put Execute Command curl -X DELETE http://${DEVICE_IP}:${PORT}/tedge/file-transfer/file_a +File transfer using tedge cli + Setup skip_bootstrap=False + + Execute Command tedge http put /tedge/file-transfer/file_b "content to be transfered" + ${content}= Execute Command tedge http get /tedge/file-transfer/file_b + Should Be Equal ${content} content to be transfered + Execute Command tedge http delete /tedge/file-transfer/file_b + Execute Command tedge http get /tedge/file-transfer/file_b exp_exit_code=1 + *** Keywords *** Custom Setup From d6306fca4b246bc306c1e0cd6d61f87df700ab77 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Fri, 7 Feb 2025 15:42:15 +0100 Subject: [PATCH 06/10] Make content-type optional, guessed using file ext Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 46 +++++++++-- crates/core/tedge/src/cli/http/command.rs | 95 ++++++++++++++++++----- 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index 207fc7cd31..862dc1871d 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -27,14 +27,14 @@ pub enum TEdgeHttpCli { content: Content, /// MIME type of the content - #[clap(long, default_value = "application/json")] + #[clap(long)] #[arg(value_parser = parse_mime_type)] - content_type: String, + content_type: Option, /// MIME type of the expected content - #[clap(long, default_value = "application/json")] + #[clap(long)] #[arg(value_parser = parse_mime_type)] - accept_type: String, + accept_type: Option, /// Optional c8y cloud profile #[clap(long)] @@ -51,9 +51,14 @@ pub enum TEdgeHttpCli { content: Content, /// MIME type of the content - #[clap(long, default_value = "application/json")] + #[clap(long)] #[arg(value_parser = parse_mime_type)] - content_type: String, + content_type: Option, + + /// MIME type of the expected content + #[clap(long)] + #[arg(value_parser = parse_mime_type)] + accept_type: Option, /// Optional c8y cloud profile #[clap(long)] @@ -66,9 +71,9 @@ pub enum TEdgeHttpCli { uri: String, /// MIME type of the expected content - #[clap(long, default_value = "application/json")] + #[clap(long)] #[arg(value_parser = parse_mime_type)] - accept_type: String, + accept_type: Option, /// Optional c8y cloud profile #[clap(long)] @@ -172,10 +177,12 @@ impl From for HttpAction { TEdgeHttpCli::Put { content, content_type, + accept_type, .. } => HttpAction::Put { content, content_type, + accept_type, }, TEdgeHttpCli::Get { accept_type, .. } => HttpAction::Get { accept_type }, TEdgeHttpCli::Delete { .. } => HttpAction::Delete, @@ -219,3 +226,26 @@ fn http_client( }; Ok(builder.build()?) } + +impl Content { + pub fn length(&self) -> Option { + if let Some(content) = &self.arg2 { + Some(content.len()) + } else if let Some(data) = &self.data { + Some(data.len()) + } else if let Some(file) = &self.file { + Some(std::fs::metadata(file).ok()?.len().try_into().ok()?) + } else { + None + } + } + + pub fn mime_type(&self) -> Option { + let file = self.file.as_ref()?; + Some( + mime_guess::from_path(file) + .first_or_octet_stream() + .to_string(), + ) + } +} diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs index 261b34b39a..c24f66d9d3 100644 --- a/crates/core/tedge/src/cli/http/command.rs +++ b/crates/core/tedge/src/cli/http/command.rs @@ -2,7 +2,9 @@ use crate::cli::http::cli::Content; use crate::command::Command; use crate::log::MaybeFancy; use anyhow::Error; +use hyper::http::HeaderValue; use reqwest::blocking; +use reqwest::header::HeaderMap; pub struct HttpCommand { /// HTTP client @@ -18,15 +20,16 @@ pub struct HttpCommand { pub enum HttpAction { Post { content: Content, - content_type: String, - accept_type: String, + content_type: Option, + accept_type: Option, }, Put { content: Content, - content_type: String, + content_type: Option, + accept_type: Option, }, Get { - accept_type: String, + accept_type: Option, }, Delete, } @@ -53,25 +56,18 @@ impl HttpCommand { fn request(&self) -> Result { let client = &self.client; let url = &self.url; + let headers = self.action.headers(); let request = match &self.action { - HttpAction::Post { - content, - content_type, - accept_type, - } => client + HttpAction::Post { content, .. } => client .post(url) - .header("Accept", accept_type) - .header("Content-Type", content_type) + .headers(headers) .body(blocking::Body::try_from(content.clone())?), - HttpAction::Put { - content, - content_type, - } => client + HttpAction::Put { content, .. } => client .put(url) - .header("Content-Type", content_type) + .headers(headers) .body(blocking::Body::try_from(content.clone())?), - HttpAction::Get { accept_type } => client.get(url).header("Accept", accept_type), - HttpAction::Delete => client.delete(url), + HttpAction::Get { .. } => client.get(url).headers(headers), + HttpAction::Delete => client.delete(url).headers(headers), }; Ok(request) @@ -84,3 +80,66 @@ impl HttpCommand { Ok(()) } } + +impl HttpAction { + pub fn headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + + if let Some(content_length) = self.content_length() { + headers.insert("Content-Length", content_length); + } + if let Some(content_type) = self.content_type() { + headers.insert("Content-Type", content_type); + } + if let Some(accept_type) = self.accept_type() { + headers.insert("Accept", accept_type); + } + + headers + } + + pub fn content_type(&self) -> Option { + match self { + HttpAction::Post { + content, + content_type, + .. + } + | HttpAction::Put { + content, + content_type, + .. + } => content_type + .as_ref() + .cloned() + .or(content.mime_type()) + .or(Some("application/json".to_string())) + .and_then(|s| HeaderValue::from_str(&s).ok()), + + _ => None, + } + } + + pub fn accept_type(&self) -> Option { + match self { + HttpAction::Post { accept_type, .. } + | HttpAction::Put { accept_type, .. } + | HttpAction::Get { accept_type } => accept_type + .as_ref() + .and_then(|s| HeaderValue::from_str(s).ok()), + + _ => None, + } + } + + pub fn content_length(&self) -> Option { + match self { + HttpAction::Post { content, .. } | HttpAction::Put { content, .. } => content + .length() + .map(|length| length.to_string()) + .and_then(|s| HeaderValue::from_str(&s).ok()), + + _ => None, + } + } +} From fed0a1ceefdd2f32b2d9845dc0480a32e54bb1e6 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Wed, 29 Jan 2025 16:49:44 +0100 Subject: [PATCH 07/10] Test tedge http Signed-off-by: Didier Wenzek --- .../tests/tedge/tedge_http.robot | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/RobotFramework/tests/tedge/tedge_http.robot diff --git a/tests/RobotFramework/tests/tedge/tedge_http.robot b/tests/RobotFramework/tests/tedge/tedge_http.robot new file mode 100644 index 0000000000..7bee06882c --- /dev/null +++ b/tests/RobotFramework/tests/tedge/tedge_http.robot @@ -0,0 +1,93 @@ +*** Settings *** +Resource ../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Suite Setup Custom Setup +Test Teardown Test Teardown + +Test Tags theme:cli theme:http theme:childdevices + + +*** Test Cases *** +Sanity check: No HTTP service on a child device + Execute Command curl http://localhost:8000/tedge/entity-store/v1/entities exp_exit_code=7 + +Listing entities from a child device + ${entities}= Execute Command tedge http get /tedge/entity-store/v1/entities + Should Contain ${entities} device/main// + Should Contain ${entities} device/${CHILD_SN}// + +Updating entities from a child device + Execute Command + ... tedge http post /tedge/entity-store/v1/entities '{"@topic-id": "device/${CHILD_SN}/service/watchdog", "@type": "service", "@parent": "device/${CHILD_SN}//"}' + ${entity}= Execute Command tedge http get /tedge/entity-store/v1/entities/device/${CHILD_SN}/service/watchdog + Should Contain ${entity} "@topic-id":"device/${CHILD_SN}/service/watchdog" + Should Contain ${entity} "@parent":"device/${CHILD_SN}//" + Should Contain ${entity} "@type":"service" + Execute Command tedge http delete /tedge/entity-store/v1/entities/device/${CHILD_SN}/service/watchdog + Execute Command + ... tedge http get /tedge/entity-store/v1/entities/device/${CHILD_SN}/service/watchdog + ... exp_exit_code=1 + +Accessing c8y from a child device + ${external_id}= Execute Command + ... tedge http get /c8y/identity/externalIds/c8y_Serial/${PARENT_SN} | jq .externalId + Should Be Equal ${external_id} "${PARENT_SN}"\n + +Accessing file-transfer from a child device + Execute Command printf "source file content" >/tmp/source-file.txt + Execute Command tedge http put /tedge/file-transfer/target --file /tmp/source-file.txt --content-type text/plain + ${content}= Execute Command tedge http get /tedge/file-transfer/target + Should Be Equal ${content} source file content + Execute Command tedge http delete /tedge/file-transfer/target + Execute Command tedge http get /tedge/file-transfer/target exp_exit_code=1 + + +*** Keywords *** +Setup Child Device + ThinEdgeIO.Set Device Context ${CHILD_SN} + Execute Command sudo dpkg -i packages/tedge_*.deb + + Execute Command sudo tedge config set http.client.host ${PARENT_IP} + Execute Command sudo tedge config set http.client.port 8000 + Execute Command sudo tedge config set c8y.proxy.client.host ${PARENT_IP} + Execute Command sudo tedge config set c8y.proxy.client.port 8001 + Execute Command sudo tedge config set mqtt.client.host ${PARENT_IP} + Execute Command sudo tedge config set mqtt.client.port 1883 + Execute Command sudo tedge config set mqtt.topic_root te + Execute Command sudo tedge config set mqtt.device_topic_id "device/${CHILD_SN}//" + + # Install plugin after the default settings have been updated to prevent it from starting up as the main plugin + Execute Command sudo dpkg -i packages/tedge-agent*.deb + Execute Command sudo systemctl enable tedge-agent + Execute Command sudo systemctl start tedge-agent + +Custom Setup + # Parent + ${parent_sn}= Setup skip_bootstrap=${True} + Set Suite Variable $PARENT_SN ${parent_sn} + Execute Command test -f ./bootstrap.sh && ./bootstrap.sh --no-connect || true + + ${parent_ip}= Get IP Address + Set Suite Variable $PARENT_IP ${parent_ip} + Execute Command sudo tedge config set mqtt.external.bind.address ${PARENT_IP} + Execute Command sudo tedge config set mqtt.external.bind.port 1883 + Execute Command sudo tedge config set http.bind.address ${PARENT_IP} + Execute Command sudo tedge config set http.bind.port 8000 + Execute Command sudo tedge config set c8y.proxy.bind.address ${PARENT_IP} + Execute Command sudo tedge config set c8y.proxy.bind.port 8001 + + ThinEdgeIO.Connect Mapper c8y + ThinEdgeIO.Service Health Status Should Be Up tedge-mapper-c8y + + # Child + ${CHILD_SN}= Setup skip_bootstrap=${True} + Set Suite Variable $CHILD_SN + Set Suite Variable $CHILD_XID ${PARENT_SN}:device:${CHILD_SN} + Setup Child Device + Cumulocity.Device Should Exist ${CHILD_XID} + +Test Teardown + Get Logs name=${PARENT_SN} + Get Logs name=${CHILD_SN} From 7f90e3e1fba9a20cef5d61725054aadf2c28524e Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Fri, 7 Feb 2025 17:15:32 +0100 Subject: [PATCH 08/10] Adding examples to tedge http --help Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/cli.rs | 36 ++++ docs/src/references/cli/tedge-http.md | 229 ++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 docs/src/references/cli/tedge-http.md diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index 862dc1871d..1339d1bf43 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -18,6 +18,18 @@ use tedge_config::ProfileName; #[derive(clap::Subcommand, Debug)] pub enum TEdgeHttpCli { /// POST content to thin-edge local HTTP servers + /// + /// Examples: + /// # Create a new Cumulocity Managed Object via the proxy service + /// tedge http post /c8y/inventory/managedObjects '{"name":"test"}' --accept-type application/json + /// + /// # Create a new child device + /// tedge http post /tedge/entity-store/v1/entities '{ + /// "@topic-id": "device/a//", + /// "@type": "child-device", + /// "@parent": "device/main//" + /// }' + #[clap(verbatim_doc_comment)] Post { /// Target URI uri: String, @@ -42,6 +54,14 @@ pub enum TEdgeHttpCli { }, /// PUT content to thin-edge local HTTP servers + /// + /// Examples: + /// # Upload file to the file transfer service + /// tedge http put /tedge/file-transfer/target.txt --file source.txt + /// + /// # Update a Cumulocity Managed Object. Note: Assuming tedge is the owner of the managed object + /// tedge http put /c8y/inventory/managedObjects/2343978440 '{"name":"item A"}' --accept-type application/json + #[clap(verbatim_doc_comment)] Put { /// Target URI uri: String, @@ -66,6 +86,14 @@ pub enum TEdgeHttpCli { }, /// GET content from thin-edge local HTTP servers + /// + /// Examples: + /// # Download file from the file transfer service + /// tedge http get /tedge/file-transfer/target.txt + /// + /// # Download file from Cumulocity's binary api + /// tedge http get /c8y/inventory/binaries/104332 > my_file.bin + #[clap(verbatim_doc_comment)] Get { /// Source URI uri: String, @@ -81,6 +109,14 @@ pub enum TEdgeHttpCli { }, /// DELETE resource from thin-edge local HTTP servers + /// + /// Examples: + /// # Delete a file from the file transfer service + /// tedge http delete /tedge/file-transfer/target.txt + /// + /// # Delete a Cumulocity managed object. Note: Assuming tedge is the owner of the managed object + /// tedge http delete /c8y/inventory/managedObjects/2343978440 + #[clap(verbatim_doc_comment)] Delete { /// Source URI uri: String, diff --git a/docs/src/references/cli/tedge-http.md b/docs/src/references/cli/tedge-http.md new file mode 100644 index 0000000000..e4909900c2 --- /dev/null +++ b/docs/src/references/cli/tedge-http.md @@ -0,0 +1,229 @@ +--- +title: "tedge http" +tags: [Reference, CLI] +sidebar_position: 6 +--- + +# The tedge http command + +A `tedge` sub command to interact with the HTTP services hosted on the device by the Cumulocity mapper and the agent: + +- the [Cumulocity Proxy](/references/cumulocity-proxy/) +- the [File Transfer Service](/references/file-transfer-service/) +- the [Entity Store Service](/operate/registration/register/). + +This command uses `tedge config` to get the appropriate host, port and credentials to reach these local HTTP services. +So the same command can be used unchanged from the main device or a child device, with TLS or mTLS enabled or not. + +```sh title="tedge http" +Send HTTP requests to local thin-edge HTTP servers + +Usage: tedge http [OPTIONS] + +Commands: + post POST content to thin-edge local HTTP servers + put PUT content to thin-edge local HTTP servers + get GET content from thin-edge local HTTP servers + delete DELETE resource from thin-edge local HTTP servers + help Print this message or the help of the given subcommand(s) + +Options: + --config-dir [env: TEDGE_CONFIG_DIR, default: /etc/tedge] + --debug Turn-on the DEBUG log level + --log-level Configures the logging level + -h, --help Print help (see more with '--help') +``` + +## %%te%% HTTP services {#http-services} + + +`tedge http` forwards requests to the appropriate %%te%% HTTP service using the URL prefixes. + +The requests are forwarded to the appropriate service depending on the URL prefix. + +- URIs prefixed by `/c8y/` are forwarded to the [Cumulocity Proxy](/references/cumulocity-proxy/) + + ```sh title="Interacting with Cumulocity" + tedge http get /c8y/inventory/managedObjects + ``` + +- URIs starting with `/tedge/file-transfer/` are directed to the [File Transfer Service](/references/file-transfer-service) + + ```sh title="Transferring files to/from the main device" + tedge http put /tedge/file-transfer/target.txt --file source.txt + ``` + +- URIs starting with `/tedge/entity-store` are directed to the [Entity Store Service](/operate/registration/register) + + ```sh title="Listing all entities" + tedge http get /tedge/entity-store/v1/entities + ``` + + +## Configuration + +For `tedge http` to be used from the main device or any client device, with TLS or mTLS enabled or not, +the host, port and credentials of the local %%te%% HTTP services +have to be properly configured on the main device as well as the child devices. + +### On the host running the main agent + +The following `tedge config` settings control the access granted to child devices +on the HTTP services provided by the main agent +([file transfer](/references/file-transfer-service) and entity registration). +This can be done along three security levels. + +```sh title="Listening HTTP requests" + http.bind.port The port number of the File Transfer Service HTTP server binds to for internal use. + Example: 8000 + http.bind.address The address of the File Transfer Service HTTP server binds to for internal use. + Examples: 127.0.0.1, 192.168.1.2, 0.0.0.0 +``` + +```sh title="Enabling TLS aka HTTPS" + http.cert_path The file that will be used as the server certificate for the File Transfer Service. + Example: /etc/tedge/device-certs/file_transfer_certificate.pem + http.key_path The file that will be used as the server private key for the File Transfer Service. + Example: /etc/tedge/device-certs/file_transfer_key.pem +``` + +```sh title="Enforcing mTLS" + http.ca_path Path to a directory containing the PEM encoded CA certificates that are trusted when checking incoming client certificates for the File Transfer Service. + Example: /etc/ssl/certs +``` + +### On the host running the cumulocity mapper + +The following `tedge config` settings control the access granted to child devices +on the HTTP services provided by the Cumulocity mapper ([Cumulocity proxy](/references/cumulocity-proxy/)). +This can be done along three security levels. + +```sh title="Listening HTTP requests" + c8y.proxy.bind.address The IP address local Cumulocity HTTP proxy binds to. + Example: 127.0.0.1 + c8y.proxy.bind.port The port local Cumulocity HTTP proxy binds to. + Example: 8001 +``` + +```sh title="Enabling TLS aka HTTPS" + c8y.proxy.cert_path The file that will be used as the server certificate for the Cumulocity proxy. + Example: /etc/tedge/device-certs/c8y_proxy_certificate.pem + c8y.proxy.key_path The file that will be used as the server private key for the Cumulocity proxy. + Example: /etc/tedge/device-certs/c8y_proxy_key.pem +``` + +```sh title="Enforcing mTLS" + c8y.proxy.ca_path Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates for the Cumulocity Proxy. + Example: /etc/ssl/certs +``` + +### On all client hosts + +The following `tedge config` settings control how client devices access the local HTTP services. +This has to be done in consistent way with the main agent and Cumulocity mapper settings. + +```sh title="Reaching local HTTP services" + http.client.port The port number on the remote host on which the File Transfer Service HTTP server is running. + Example: 8000 + http.client.host The address of the host on which the File Transfer Service HTTP server is running. + Examples: 127.0.0.1, 192.168.1.2, tedge-hostname + c8y.proxy.client.host The address of the host on which the local Cumulocity HTTP Proxy is running, used by the Cumulocity mapper. + Examples: 127.0.0.1, 192.168.1.2, tedge-hostname + c8y.proxy.client.port The port number on the remote host on which the local Cumulocity HTTP Proxy is running, used by the Cumulocity mapper. + Example: 8001 +``` + +```sh title="Using TLS aka HTTPS" + http.ca_path Path to a directory containing the PEM encoded CA certificates that are trusted when checking incoming client certificates for the File Transfer Service. + Example: /etc/ssl/certs + c8y.proxy.ca_path Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates for the Cumulocity Proxy. + Example: /etc/ssl/certs +``` + +```sh title="Using mTLS" +http.client.auth.cert_file Path to the certificate which is used by the agent when connecting to external services. + http.client.auth.key_file Path to the private key which is used by the agent when connecting to external services. +``` + +## tedge http post + +```sh title="tedge http post" +POST content to thin-edge local HTTP servers + +Usage: tedge http post [OPTIONS] |--file > + +Arguments: + Target URI + [content] Content to send + +Options: + --config-dir [env: TEDGE_CONFIG_DIR, default: /etc/tedge] + --data Content to send + --debug Turn-on the DEBUG log level + --file File which content is sent + --content-type MIME type of the content + --log-level Configures the logging level + --accept-type MIME type of the expected content + --profile Optional c8y cloud profile + -h, --help Print help (see more with '--help') +``` + +## tedge http put + +```sh title="tedge http put" +PUT content to thin-edge local HTTP servers + +Usage: tedge http put [OPTIONS] |--file > + +Arguments: + Target URI + [content] Content to send + +Options: + --config-dir [env: TEDGE_CONFIG_DIR, default: /etc/tedge] + --data Content to send + --debug Turn-on the DEBUG log level + --file File which content is sent + --content-type MIME type of the content + --log-level Configures the logging level + --accept-type MIME type of the expected content + --profile Optional c8y cloud profile + -h, --help Print help (see more with '--help') +``` + +## tedge http get + +```sh title="tedge http get" +GET content from thin-edge local HTTP servers + +Usage: tedge http get [OPTIONS] + +Arguments: + Source URI + +Options: + --accept-type MIME type of the expected content + --config-dir [env: TEDGE_CONFIG_DIR, default: /etc/tedge] + --debug Turn-on the DEBUG log level + --profile Optional c8y cloud profile + --log-level Configures the logging level + -h, --help Print help (see more with '--help') +``` + +## tedge http delete + +```sh title="tedge http delete" +DELETE resource from thin-edge local HTTP servers + +Usage: tedge http delete [OPTIONS] + +Arguments: + Source URI + +Options: + --config-dir [env: TEDGE_CONFIG_DIR, default: /etc/tedge] + --profile Optional c8y cloud profile + --debug Turn-on the DEBUG log level + --log-level Configures the logging level + -h, --help Print help (see more with '--help') +``` From bc697fff13383dbf8749825f0cc109d120b5d0dd Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Mon, 10 Feb 2025 10:55:20 +0100 Subject: [PATCH 09/10] Display server response on 4xx-5xx Signed-off-by: Didier Wenzek --- crates/core/tedge/src/cli/http/command.rs | 26 ++++++++++++++++--- .../tests/tedge/tedge_http.robot | 7 +++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/core/tedge/src/cli/http/command.rs b/crates/core/tedge/src/cli/http/command.rs index c24f66d9d3..5d67447420 100644 --- a/crates/core/tedge/src/cli/http/command.rs +++ b/crates/core/tedge/src/cli/http/command.rs @@ -1,6 +1,7 @@ use crate::cli::http::cli::Content; use crate::command::Command; use crate::log::MaybeFancy; +use anyhow::anyhow; use anyhow::Error; use hyper::http::HeaderValue; use reqwest::blocking; @@ -74,10 +75,27 @@ impl HttpCommand { } fn send(request: blocking::RequestBuilder) -> Result<(), Error> { - let http_result = request.send()?; - let mut http_response = http_result.error_for_status()?; - http_response.copy_to(&mut std::io::stdout())?; - Ok(()) + let mut http_result = request.send()?; + let status = http_result.status(); + if status.is_success() { + http_result.copy_to(&mut std::io::stdout())?; + Ok(()) + } else { + let kind = if status.is_client_error() { + "HTTP client error" + } else if status.is_server_error() { + "HTTP server error" + } else { + "HTTP error" + }; + let error = format!( + "{kind}: {} {}\n{}", + status.as_u16(), + status.canonical_reason().unwrap_or(""), + http_result.text().unwrap_or("".to_string()) + ); + Err(anyhow!(error))? + } } } diff --git a/tests/RobotFramework/tests/tedge/tedge_http.robot b/tests/RobotFramework/tests/tedge/tedge_http.robot index 7bee06882c..4bfd9bd42a 100644 --- a/tests/RobotFramework/tests/tedge/tedge_http.robot +++ b/tests/RobotFramework/tests/tedge/tedge_http.robot @@ -43,6 +43,13 @@ Accessing file-transfer from a child device Execute Command tedge http delete /tedge/file-transfer/target Execute Command tedge http get /tedge/file-transfer/target exp_exit_code=1 +Displaying server errors + ${error_msg}= Execute Command + ... tedge http post /tedge/entity-store/v1/entities '{"@topic-id": "device/a//", "@type": "child-device", "@parent": "device/unknown//"}' 2>&1 + ... exp_exit_code=1 + Should Contain ${error_msg} 400 Bad Request + Should Contain ${error_msg} Specified parent "device/unknown//" does not exist in the store + *** Keywords *** Setup Child Device From 8049b706254d538eca9c4b8cb6822f3127f34bfd Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Wed, 5 Feb 2025 18:01:44 +0100 Subject: [PATCH 10/10] Tedge http reference documentation Signed-off-by: Didier Wenzek --- docs/src/operate/registration/register.md | 3 +-- docs/src/references/cli/tedge-upload.md | 2 +- docs/src/references/cumulocity-proxy.md | 13 +++++++++++++ docs/src/references/file-transfer-service.md | 12 ++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/src/operate/registration/register.md b/docs/src/operate/registration/register.md index 5a403d9b37..43714c2261 100644 --- a/docs/src/operate/registration/register.md +++ b/docs/src/operate/registration/register.md @@ -1,9 +1,8 @@ --- -title: 🚧 Entity management REST APIs +title: Entity management REST APIs tags: [Child-Device, Registration] sidebar_position: 1 description: Register child devices and services with %%te%% -draft: true --- # REST APIs for Entity Management diff --git a/docs/src/references/cli/tedge-upload.md b/docs/src/references/cli/tedge-upload.md index cd4494de55..3d5735c2de 100644 --- a/docs/src/references/cli/tedge-upload.md +++ b/docs/src/references/cli/tedge-upload.md @@ -1,7 +1,7 @@ --- title: "tedge upload" tags: [Reference, CLI] -sidebar_position: 6 +sidebar_position: 7 --- # The tedge upload command diff --git a/docs/src/references/cumulocity-proxy.md b/docs/src/references/cumulocity-proxy.md index 1dc2502d47..140bba87a5 100644 --- a/docs/src/references/cumulocity-proxy.md +++ b/docs/src/references/cumulocity-proxy.md @@ -45,3 +45,16 @@ The proxy server currently forwards this response directly to the client, as wel Cumulocity. If there is an error connecting to Cumulocity to make the request, a plain text response with the status code `502 Bad Gateway` will be returned. + +## Using tedge http + +[`tedge http`](../references/cli/tedge-http.md) can be used to access Cumulocity from any child devices, +provided [proper configuration](../references/cli/tedge-http.md#configuration). + +For example, you can access the current tenant information +from the machine running `tedge-mapper` as well as any child device: + +```sh title="Interacting with Cumulocity" + tedge http get /c8y/tenant/currentTenant +``` + diff --git a/docs/src/references/file-transfer-service.md b/docs/src/references/file-transfer-service.md index 46ef0088db..94cce9a181 100644 --- a/docs/src/references/file-transfer-service.md +++ b/docs/src/references/file-transfer-service.md @@ -44,3 +44,15 @@ Once HTTPS is enabled for the file-transfer service, certificate-based authentic The directory containing the certificates that the agent will trust can be configured using `http.ca_path`, and the mapper as well as the child device agents can be configured to use a trusted certificate using the `http.client.auth.cert_file` and `http.client.auth.key_file` settings. + +## Using tedge http + +[`tedge http`](../references/cli/tedge-http.md) can be used to access the file transfer service from any child devices, +provided [proper configuration](../references/cli/tedge-http.md#configuration). + + +|Type| Command | +|----|---------------------------------------------------------------------------------------| +|Upload| `tedge http put tedge/file-transfer/{path}/{to}/{resource} --file /{path}/{to}/{file}` | +|Download| `tedge http get /tedge/file-transfer/{path}/{to}/{resource} >/{path}/{to}/{file}` | +|Delete| `tedge http delete /tedge/file-transfer/{path}/{to}/{resource}` |