From bb0b172d38f449271791b1b095fb3b7ffe08e637 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 17 Dec 2024 10:32:06 -0800 Subject: [PATCH] [sled agent] Remove enum bodies from support bundle API (#7259) Responding to feedback in https://github.com/oxidecomputer/omicron/pull/7008#discussion_r1884644021 , this PR aligns the Sled Agent Support Bundle API with the incoming Nexus Support Bundle API (in https://github.com/oxidecomputer/omicron/pull/7008) --- openapi/sled-agent.json | 352 +++++++++++++++-------- sled-agent/api/src/lib.rs | 71 +++-- sled-agent/src/http_entrypoints.rs | 113 +++++++- sled-agent/src/sim/http_entrypoints.rs | 90 +++++- sled-agent/src/support_bundle/storage.rs | 11 + 5 files changed, 482 insertions(+), 155 deletions(-) diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 14a4c92692..c7d2b36de4 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -785,9 +785,9 @@ } }, "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}": { - "get": { - "summary": "Fetch a support bundle from a particular dataset", - "operationId": "support_bundle_get", + "post": { + "summary": "Create a support bundle within a particular dataset", + "operationId": "support_bundle_create", "parameters": [ { "in": "path", @@ -815,32 +815,50 @@ "schema": { "$ref": "#/components/schemas/TypedUuidForZpoolKind" } + }, + { + "in": "query", + "name": "hash", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } } ], "requestBody": { "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/SupportBundleGetQueryParams" + "type": "string", + "format": "binary" } } }, "required": true }, "responses": { - "default": { - "description": "", + "201": { + "description": "successful creation", "content": { - "*/*": { - "schema": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } } } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" } } }, - "post": { - "summary": "Create a support bundle within a particular dataset", - "operationId": "support_bundle_create", + "delete": { + "summary": "Delete a support bundle from a particular dataset", + "operationId": "support_bundle_delete", "parameters": [ { "in": "path", @@ -868,50 +886,165 @@ "schema": { "$ref": "#/components/schemas/TypedUuidForZpoolKind" } + } + ], + "responses": { + "204": { + "description": "successful deletion" }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download": { + "get": { + "summary": "Fetch a support bundle from a particular dataset", + "operationId": "support_bundle_download", + "parameters": [ { - "in": "query", - "name": "hash", + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", "required": true, "schema": { - "type": "string", - "format": "hex string (32 bytes)" + "$ref": "#/components/schemas/TypedUuidForDatasetKind" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForSupportBundleKind" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForZpoolKind" } } ], - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} } } + } + } + }, + "head": { + "summary": "Fetch metadata about a support bundle from a particular dataset", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForDatasetKind" + } }, - "required": true - }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForSupportBundleKind" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForZpoolKind" + } + } + ], "responses": { - "201": { - "description": "successful creation", + "default": { + "description": "", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SupportBundleMetadata" - } + "*/*": { + "schema": {} } } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}": { + "get": { + "summary": "Fetch a file within a support bundle from a particular dataset", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForDatasetKind" + } }, - "4XX": { - "$ref": "#/components/responses/Error" + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } }, - "5XX": { - "$ref": "#/components/responses/Error" + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForSupportBundleKind" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForZpoolKind" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } } } }, - "delete": { - "summary": "Delete a support bundle from a particular dataset", - "operationId": "support_bundle_delete", + "head": { + "summary": "Fetch metadata about a file within a support bundle from a particular dataset", + "operationId": "support_bundle_head_file", "parameters": [ { "in": "path", @@ -922,6 +1055,15 @@ "$ref": "#/components/schemas/TypedUuidForDatasetKind" } }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, { "in": "path", "name": "support_bundle_id", @@ -942,20 +1084,64 @@ } ], "responses": { - "204": { - "description": "successful deletion" + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index": { + "get": { + "summary": "Fetch the index (list of files within a support bundle)", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForDatasetKind" + } }, - "4XX": { - "$ref": "#/components/responses/Error" + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForSupportBundleKind" + } }, - "5XX": { - "$ref": "#/components/responses/Error" + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/TypedUuidForZpoolKind" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } } } }, "head": { - "summary": "Fetch a support bundle from a particular dataset", - "operationId": "support_bundle_head", + "summary": "Fetch metadata about the list of files within a support bundle", + "operationId": "support_bundle_head_index", "parameters": [ { "in": "path", @@ -985,16 +1171,6 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SupportBundleGetQueryParams" - } - } - }, - "required": true - }, "responses": { "default": { "description": "", @@ -5613,18 +5789,6 @@ "format": "uint8", "minimum": 0 }, - "SupportBundleGetQueryParams": { - "description": "Query parameters for reading the support bundle", - "type": "object", - "properties": { - "query_type": { - "$ref": "#/components/schemas/SupportBundleQueryType" - } - }, - "required": [ - "query_type" - ] - }, "SupportBundleMetadata": { "description": "Metadata about a support bundle", "type": "object", @@ -5641,60 +5805,6 @@ "support_bundle_id" ] }, - "SupportBundleQueryType": { - "description": "Describes the type of access to the support bundle", - "oneOf": [ - { - "description": "Access the whole support bundle", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "whole" - ] - } - }, - "required": [ - "type" - ] - }, - { - "description": "Access the names of all files within the support bundle", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "index" - ] - } - }, - "required": [ - "type" - ] - }, - { - "description": "Access a specific file within the support bundle", - "type": "object", - "properties": { - "file_path": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "path" - ] - } - }, - "required": [ - "file_path", - "type" - ] - } - ] - }, "SupportBundleState": { "type": "string", "enum": [ diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 634640079a..56c6760be7 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -184,23 +184,61 @@ pub trait SledAgentApi { /// Fetch a support bundle from a particular dataset #[endpoint { method = GET, - path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}" + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download" }] - async fn support_bundle_get( + async fn support_bundle_download( rqctx: RequestContext, path_params: Path, - body: TypedBody, ) -> Result, HttpError>; - /// Fetch a support bundle from a particular dataset + /// Fetch a file within a support bundle from a particular dataset + #[endpoint { + method = GET, + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}" + }] + async fn support_bundle_download_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError>; + + /// Fetch the index (list of files within a support bundle) + #[endpoint { + method = GET, + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index" + }] + async fn support_bundle_index( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError>; + + /// Fetch metadata about a support bundle from a particular dataset #[endpoint { method = HEAD, - path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}" + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download" }] async fn support_bundle_head( rqctx: RequestContext, path_params: Path, - body: TypedBody, + ) -> Result, HttpError>; + + /// Fetch metadata about a file within a support bundle from a particular dataset + #[endpoint { + method = HEAD, + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}" + }] + async fn support_bundle_head_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError>; + + /// Fetch metadata about the list of files within a support bundle + #[endpoint { + method = HEAD, + path = "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index" + }] + async fn support_bundle_head_index( + rqctx: RequestContext, + path_params: Path, ) -> Result, HttpError>; /// Delete a support bundle from a particular dataset @@ -689,6 +727,9 @@ pub struct SupportBundlePathParam { pub struct SupportBundleFilePathParam { #[serde(flatten)] pub parent: SupportBundlePathParam, + + /// The path of the file within the support bundle to query + pub file: String, } /// Metadata about a support bundle @@ -702,24 +743,6 @@ pub struct SupportBundleGetHeaders { range: String, } -/// Query parameters for reading the support bundle -#[derive(Deserialize, Serialize, JsonSchema)] -pub struct SupportBundleGetQueryParams { - pub query_type: SupportBundleQueryType, -} - -/// Describes the type of access to the support bundle -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum SupportBundleQueryType { - /// Access the whole support bundle - Whole, - /// Access the names of all files within the support bundle - Index, - /// Access a specific file within the support bundle - Path { file_path: String }, -} - #[derive(Deserialize, Debug, Serialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub enum SupportBundleState { diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 88259537d2..edba1c47b1 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -6,6 +6,7 @@ use super::sled_agent::SledAgent; use crate::sled_agent::Error as SledAgentError; +use crate::support_bundle::storage::SupportBundleQueryType; use crate::zone_bundle::BundleError; use bootstore::schemes::v0::NetworkConfig; use camino::Utf8PathBuf; @@ -267,37 +268,135 @@ impl SledAgentApi for SledAgentImpl { Ok(HttpResponseCreated(metadata)) } - async fn support_bundle_get( + async fn support_bundle_download( rqctx: RequestContext, path_params: Path, - body: TypedBody, ) -> Result, HttpError> { let sa = rqctx.context(); let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = path_params.into_inner(); let range = rqctx.range(); - let query = body.into_inner().query_type; Ok(sa .as_support_bundle_storage() - .get(zpool_id, dataset_id, support_bundle_id, range, query) + .get( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Whole, + ) + .await?) + } + + async fn support_bundle_download_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundleFilePathParam { + parent: + SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id }, + file, + } = path_params.into_inner(); + + let range = rqctx.range(); + Ok(sa + .as_support_bundle_storage() + .get( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Path { file_path: file }, + ) + .await?) + } + + async fn support_bundle_index( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = + path_params.into_inner(); + + let range = rqctx.range(); + Ok(sa + .as_support_bundle_storage() + .get( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Index, + ) .await?) } async fn support_bundle_head( rqctx: RequestContext, path_params: Path, - body: TypedBody, ) -> Result, HttpError> { let sa = rqctx.context(); let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = path_params.into_inner(); let range = rqctx.range(); - let query = body.into_inner().query_type; Ok(sa .as_support_bundle_storage() - .head(zpool_id, dataset_id, support_bundle_id, range, query) + .head( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Whole, + ) + .await?) + } + + async fn support_bundle_head_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundleFilePathParam { + parent: + SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id }, + file, + } = path_params.into_inner(); + + let range = rqctx.range(); + Ok(sa + .as_support_bundle_storage() + .head( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Path { file_path: file }, + ) + .await?) + } + + async fn support_bundle_head_index( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = + path_params.into_inner(); + + let range = rqctx.range(); + Ok(sa + .as_support_bundle_storage() + .head( + zpool_id, + dataset_id, + support_bundle_id, + range, + SupportBundleQueryType::Index, + ) .await?) } diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 2d23f9150b..60dcb1be31 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -438,10 +438,9 @@ impl SledAgentApi for SledAgentSimImpl { )) } - async fn support_bundle_get( + async fn support_bundle_download( rqctx: RequestContext, path_params: Path, - _body: TypedBody, ) -> Result, HttpError> { let sa = rqctx.context(); let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = @@ -458,10 +457,95 @@ impl SledAgentApi for SledAgentSimImpl { .unwrap()) } + async fn support_bundle_download_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundleFilePathParam { + parent: + SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id }, + file: _, + } = path_params.into_inner(); + + sa.support_bundle_get(zpool_id, dataset_id, support_bundle_id).await?; + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .header(http::header::CONTENT_TYPE, "text/html") + .body(dropshot::Body::with_content( + "simulated support bundle file; do not eat", + )) + .unwrap()) + } + + async fn support_bundle_index( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = + path_params.into_inner(); + + sa.support_bundle_get(zpool_id, dataset_id, support_bundle_id).await?; + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .header(http::header::CONTENT_TYPE, "text/html") + .body(dropshot::Body::with_content( + "simulated support bundle index; do not eat", + )) + .unwrap()) + } + async fn support_bundle_head( rqctx: RequestContext, path_params: Path, - _body: TypedBody, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = + path_params.into_inner(); + + sa.support_bundle_get(zpool_id, dataset_id, support_bundle_id).await?; + + let fictional_length = 10000; + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .header(http::header::CONTENT_TYPE, "text/html") + .header(hyper::header::ACCEPT_RANGES, "bytes") + .header(hyper::header::CONTENT_LENGTH, fictional_length) + .body(dropshot::Body::empty()) + .unwrap()) + } + + async fn support_bundle_head_file( + rqctx: RequestContext, + path_params: Path, + ) -> Result, HttpError> { + let sa = rqctx.context(); + let SupportBundleFilePathParam { + parent: + SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id }, + file: _, + } = path_params.into_inner(); + + sa.support_bundle_get(zpool_id, dataset_id, support_bundle_id).await?; + + let fictional_length = 10000; + + Ok(http::Response::builder() + .status(http::StatusCode::OK) + .header(http::header::CONTENT_TYPE, "text/html") + .header(hyper::header::ACCEPT_RANGES, "bytes") + .header(hyper::header::CONTENT_LENGTH, fictional_length) + .body(dropshot::Body::empty()) + .unwrap()) + } + + async fn support_bundle_head_index( + rqctx: RequestContext, + path_params: Path, ) -> Result, HttpError> { let sa = rqctx.context(); let SupportBundlePathParam { zpool_id, dataset_id, support_bundle_id } = diff --git a/sled-agent/src/support_bundle/storage.rs b/sled-agent/src/support_bundle/storage.rs index e51f35e146..97d345a8d2 100644 --- a/sled-agent/src/support_bundle/storage.rs +++ b/sled-agent/src/support_bundle/storage.rs @@ -100,6 +100,17 @@ impl From for HttpError { } } +/// Describes the type of access to the support bundle +#[derive(Clone, Debug)] +pub(crate) enum SupportBundleQueryType { + /// Access the whole support bundle + Whole, + /// Access the names of all files within the support bundle + Index, + /// Access a specific file within the support bundle + Path { file_path: String }, +} + // Implements "seeking" and "putting a capacity on a file" manually. // // TODO: When https://github.com/zip-rs/zip2/issues/231 is resolved,