From 0f062ea31b1f39ca671e22f5a1650f1c2617eea0 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 22 Jul 2024 18:57:17 -0700 Subject: [PATCH] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20initia?= =?UTF-8?q?l=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created using spr 1.3.6-beta.1 --- Cargo.lock | 34 +- Cargo.toml | 10 +- clients/gateway-client/src/lib.rs | 11 + dev-tools/openapi-manager/Cargo.toml | 1 + dev-tools/openapi-manager/src/spec.rs | 23 +- gateway-api/Cargo.toml | 18 + gateway-api/src/lib.rs | 565 ++++ gateway-test-utils/Cargo.toml | 1 + gateway-test-utils/src/sim_state.rs | 8 +- gateway-types/Cargo.toml | 21 + gateway-types/src/caboose.rs | 15 + gateway-types/src/component.rs | 286 ++ .../src}/component_details.rs | 2 - gateway-types/src/host.rs | 58 + gateway-types/src/ignition.rs | 140 + gateway-types/src/lib.rs | 14 + gateway-types/src/rot.rs | 355 +++ gateway-types/src/sensor.rs | 74 + gateway-types/src/update.rs | 129 + gateway/Cargo.toml | 2 + gateway/src/bin/mgs.rs | 8 +- gateway/src/http_entrypoints.rs | 2306 +++++------------ gateway/src/http_entrypoints/conversions.rs | 596 ----- gateway/src/lib.rs | 12 - gateway/src/management_switch.rs | 42 + gateway/tests/integration_tests/commands.rs | 39 - .../tests/integration_tests/component_list.rs | 8 +- .../integration_tests/location_discovery.rs | 4 +- gateway/tests/integration_tests/mod.rs | 1 - .../tests/integration_tests/serial_console.rs | 2 +- gateway/tests/output/cmd-mgs-openapi-stderr | 0 openapi/gateway.json | 8 +- sp-sim/Cargo.toml | 2 +- sp-sim/src/gimlet.rs | 5 +- sp-sim/src/lib.rs | 3 +- sp-sim/src/sidecar.rs | 5 +- 36 files changed, 2532 insertions(+), 2276 deletions(-) create mode 100644 gateway-api/Cargo.toml create mode 100644 gateway-api/src/lib.rs create mode 100644 gateway-types/Cargo.toml create mode 100644 gateway-types/src/caboose.rs create mode 100644 gateway-types/src/component.rs rename {gateway/src/http_entrypoints => gateway-types/src}/component_details.rs (99%) create mode 100644 gateway-types/src/host.rs create mode 100644 gateway-types/src/ignition.rs create mode 100644 gateway-types/src/lib.rs create mode 100644 gateway-types/src/rot.rs create mode 100644 gateway-types/src/sensor.rs create mode 100644 gateway-types/src/update.rs delete mode 100644 gateway/src/http_entrypoints/conversions.rs delete mode 100644 gateway/tests/integration_tests/commands.rs delete mode 100644 gateway/tests/output/cmd-mgs-openapi-stderr diff --git a/Cargo.lock b/Cargo.lock index 7831b5701c..67a4f78e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2697,6 +2697,20 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gateway-api" +version = "0.1.0" +dependencies = [ + "dropshot", + "gateway-types", + "omicron-common", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "schemars", + "serde", + "uuid", +] + [[package]] name = "gateway-cli" version = "0.1.0" @@ -2793,6 +2807,7 @@ dependencies = [ "camino", "dropshot", "gateway-messages", + "gateway-types", "omicron-gateway", "omicron-test-utils", "omicron-workspace-hack", @@ -2802,6 +2817,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "gateway-types" +version = "0.1.0" +dependencies = [ + "gateway-messages", + "hex", + "ipcc", + "omicron-common", + "omicron-workspace-hack", + "schemars", + "serde", + "uuid", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -5503,9 +5532,11 @@ dependencies = [ "dropshot", "expectorate", "futures", + "gateway-api", "gateway-messages", "gateway-sp-comms", "gateway-test-utils", + "gateway-types", "hex", "http 0.2.12", "hyper 0.14.28", @@ -6159,6 +6190,7 @@ dependencies = [ "dns-server-api", "dropshot", "fs-err", + "gateway-api", "indent_write", "installinator-api", "nexus-internal-api", @@ -9287,9 +9319,9 @@ dependencies = [ "dropshot", "futures", "gateway-messages", + "gateway-types", "hex", "omicron-common", - "omicron-gateway", "omicron-workspace-hack", "serde", "slog", diff --git a/Cargo.toml b/Cargo.toml index 8cc8260f66..eaa2967901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,11 @@ members = [ "dns-server", "dns-server-api", "end-to-end-tests", + "gateway", + "gateway-api", "gateway-cli", "gateway-test-utils", - "gateway", + "gateway-types", "illumos-utils", "installinator-api", "installinator-common", @@ -132,9 +134,11 @@ default-members = [ "dns-server-api", # Do not include end-to-end-tests in the list of default members, as its # tests only work on a deployed control plane. + "gateway", + "gateway-api", "gateway-cli", "gateway-test-utils", - "gateway", + "gateway-types", "illumos-utils", "installinator-api", "installinator-common", @@ -313,10 +317,12 @@ flume = "0.11.0" foreign-types = "0.3.2" fs-err = "2.11.0" futures = "0.3.30" +gateway-api = { path = "gateway-api" } gateway-client = { path = "clients/gateway-client" } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "c85a4ca043aaa389df12aac5348d8a3feda28762", default-features = false, features = ["std"] } gateway-sp-comms = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "c85a4ca043aaa389df12aac5348d8a3feda28762" } gateway-test-utils = { path = "gateway-test-utils" } +gateway-types = { path = "gateway-types" } gethostname = "0.4.3" glob = "0.3.1" guppy = "0.17.5" diff --git a/clients/gateway-client/src/lib.rs b/clients/gateway-client/src/lib.rs index 9693c5e62a..ab936de079 100644 --- a/clients/gateway-client/src/lib.rs +++ b/clients/gateway-client/src/lib.rs @@ -35,6 +35,17 @@ pub use gateway_messages::SpComponent; // If the format of inventory data desired by wicket or nexus changes such that // it is no longer useful to directly expose the JsonSchema types, we can go // back to reusing `omicron_common`. +// +// As another alternative, since the `derives` and `patch` directives were +// introduced, these types have moved to living in gateway-types. This means +// that we can choose to use `replace` on them. That hasn't felt necessary so +// far, but it's an option if it becomes desirable in the future. (One reason +// to do that might be that currently, `nexus-types` depends on +// `gateway-client`. Generally we expect the `-client` layer to depend on the +// `-types` layer to avoid a circular dependency, and we've had to resolve a +// rather thorny one between Nexus and sled-agent. But Nexus and sled-agent +// call into each other. Since `gateway` is a lower-level service and never +// calls into Nexus, the current scheme is okay.) progenitor::generate_api!( spec = "../../openapi/gateway.json", inner_type = slog::Logger, diff --git a/dev-tools/openapi-manager/Cargo.toml b/dev-tools/openapi-manager/Cargo.toml index 94be112895..e60000cc06 100644 --- a/dev-tools/openapi-manager/Cargo.toml +++ b/dev-tools/openapi-manager/Cargo.toml @@ -17,6 +17,7 @@ clap.workspace = true dns-server-api.workspace = true dropshot.workspace = true fs-err.workspace = true +gateway-api.workspace = true indent_write.workspace = true installinator-api.workspace = true nexus-internal-api.workspace = true diff --git a/dev-tools/openapi-manager/src/spec.rs b/dev-tools/openapi-manager/src/spec.rs index cb3b0e3e5c..2c99a83802 100644 --- a/dev-tools/openapi-manager/src/spec.rs +++ b/dev-tools/openapi-manager/src/spec.rs @@ -14,11 +14,21 @@ use openapiv3::OpenAPI; /// All APIs managed by openapi-manager. pub fn all_apis() -> Vec { vec![ + ApiSpec { + title: "Bootstrap Agent API", + version: "0.0.1", + description: "Per-sled API for setup and teardown", + boundary: ApiBoundary::Internal, + api_description: + bootstrap_agent_api::bootstrap_agent_api_mod::stub_api_description, + filename: "bootstrap-agent.json", + extra_validation: None, + }, ApiSpec { title: "CockroachDB Cluster Admin API", version: "0.0.1", - description: "API for interacting with the Oxide control plane's \ - CockroachDB cluster", + description: "API for interacting with the Oxide \ + control plane's CockroachDB cluster", boundary: ApiBoundary::Internal, api_description: cockroach_admin_api::cockroach_admin_api_mod::stub_api_description, @@ -26,13 +36,14 @@ pub fn all_apis() -> Vec { extra_validation: None, }, ApiSpec { - title: "Bootstrap Agent API", + title: "Oxide Management Gateway Service API", version: "0.0.1", - description: "Per-sled API for setup and teardown", + description: "API for interacting with the Oxide \ + control plane's gateway service", boundary: ApiBoundary::Internal, api_description: - bootstrap_agent_api::bootstrap_agent_api_mod::stub_api_description, - filename: "bootstrap-agent.json", + gateway_api::gateway_api_mod::stub_api_description, + filename: "gateway.json", extra_validation: None, }, ApiSpec { diff --git a/gateway-api/Cargo.toml b/gateway-api/Cargo.toml new file mode 100644 index 0000000000..840bb90b68 --- /dev/null +++ b/gateway-api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "gateway-api" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +dropshot.workspace = true +gateway-types.workspace = true +omicron-common.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true +uuid.workspace = true diff --git a/gateway-api/src/lib.rs b/gateway-api/src/lib.rs new file mode 100644 index 0000000000..261e669a26 --- /dev/null +++ b/gateway-api/src/lib.rs @@ -0,0 +1,565 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! HTTP API for the gateway service. + +use dropshot::{ + HttpError, HttpResponseOk, HttpResponseUpdatedNoContent, Path, Query, + RequestContext, TypedBody, UntypedBody, WebsocketEndpointResult, + WebsocketUpgrade, +}; +use gateway_types::{ + caboose::SpComponentCaboose, + component::{ + PowerState, SpComponentFirmwareSlot, SpComponentList, SpIdentifier, + SpState, + }, + component_details::SpComponentDetails, + host::HostStartupOptions, + ignition::{IgnitionCommand, SpIgnitionInfo}, + rot::{RotCfpa, RotCfpaSlot, RotCmpa, RotState}, + sensor::SpSensorReading, + update::{ + HostPhase2Progress, HostPhase2RecoveryImageId, InstallinatorImageId, + SpUpdateStatus, + }, +}; +use schemars::JsonSchema; +use serde::Deserialize; +use uuid::Uuid; + +#[dropshot::api_description { + module = "gateway_api_mod", +}] +pub trait GatewayApi { + type Context; + + /// Get info on an SP + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}", + }] + async fn sp_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Get host startup options for a sled + /// + /// This endpoint will currently fail for any `SpType` other than + /// `SpType::Sled`. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/startup-options", + }] + async fn sp_startup_options_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set host startup options for a sled + /// + /// This endpoint will currently fail for any `SpType` other than + /// `SpType::Sled`. + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/startup-options", + }] + async fn sp_startup_options_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Read the current value of a sensor by ID + /// + /// Sensor IDs come from the host topo tree. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/sensor/{sensor_id}/value", + }] + async fn sp_sensor_read_value( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// List components of an SP + /// + /// A component is a distinct entity under an SP's direct control. This + /// lists all those components for a given SP. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component", + }] + async fn sp_component_list( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Get info for an SP component + /// + /// This can be useful, for example, to poll the state of a component if + /// another interface has changed the power state of a component or updated + /// a component. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}", + }] + async fn sp_component_get( + rqctx: RequestContext, + path: Path, + ) -> Result>, HttpError>; + + /// Get the caboose of an SP component + /// + /// Not all components have a caboose. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/caboose", + }] + async fn sp_component_caboose_get( + rqctx: RequestContext, + path: Path, + query_params: Query, + ) -> Result, HttpError>; + + /// Clear status of a component + /// + /// For components that maintain event counters (e.g., the sidecar + /// `monorail`), this will reset the event counters to zero. + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/clear-status", + }] + async fn sp_component_clear_status( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Get the currently-active slot for an SP component + /// + /// Note that the meaning of "current" in "currently-active" may vary + /// depending on the component: for example, it may mean "the + /// actively-running slot" or "the slot that will become active the next + /// time the component is booted". + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/active-slot", + }] + async fn sp_component_active_slot_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set the currently-active slot for an SP component + /// + /// Note that the meaning of "current" in "currently-active" may vary + /// depending on the component: for example, it may mean "the + /// actively-running slot" or "the slot that will become active the next + /// time the component is booted". + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/active-slot", + }] + async fn sp_component_active_slot_set( + rqctx: RequestContext, + path: Path, + query_params: Query, + body: TypedBody, + ) -> Result; + + /// Upgrade into a websocket connection attached to the given SP + /// component's serial console. + // + // # Notes + // + // This is a websocket endpoint; normally we'd expect to use + // `dropshot::channel` with `protocol = WEBSOCKETS` instead of + // `dropshot::endpoint`, but `dropshot::channel` doesn't allow us to return + // an error _before_ upgrading the connection, and we want to bail out ASAP + // if the SP doesn't allow us to attach. + // + // Therefore, we use `dropshot::endpoint` with the special argument type + // `WebsocketUpgrade`: this inserts the correct marker for progenitor to + // know this is a websocket endpoint, and it allows us to call + // `WebsocketUpgrade::handle()` (the method `dropshot::channel` would call + // for us to upgrade the connection) by hand after our error checking. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/serial-console/attach", + }] + async fn sp_component_serial_console_attach( + rqctx: RequestContext, + path: Path, + websocket: WebsocketUpgrade, + ) -> WebsocketEndpointResult; + + /// Detach the websocket connection attached to the given SP component's + /// serial console, if such a connection exists. + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/serial-console/detach", + }] + async fn sp_component_serial_console_detach( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Reset an SP component (possibly the SP itself). + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/reset", + }] + async fn sp_component_reset( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Update an SP component + /// + /// Update a component of an SP according to its specific update mechanism. + /// This interface is generic for all component types, but resolves to a + /// mechanism specific to the given component type. This may fail for a + /// variety of reasons including the update bundle being invalid or + /// improperly specified or due to an error originating from the SP itself. + /// + /// Note that not all components may be updated; components without known + /// update mechanisms will return an error without any inspection of the + /// update bundle. + /// + /// Updating the SP itself is done via the component name `sp`. + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/update", + }] + async fn sp_component_update( + rqctx: RequestContext, + path: Path, + query_params: Query, + body: UntypedBody, + ) -> Result; + + /// Get the status of an update being applied to an SP component + /// + /// Getting the status of an update to the SP itself is done via the + /// component name `sp`. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/update-status", + }] + async fn sp_component_update_status( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Abort any in-progress update an SP component + /// + /// Aborting an update to the SP itself is done via the component name + /// `sp`. + /// + /// On a successful return, the update corresponding to the given UUID will + /// no longer be in progress (either aborted or applied). + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/component/{component}/update-abort", + }] + async fn sp_component_update_abort( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Read the CMPA from a root of trust. + /// + /// This endpoint is only valid for the `rot` component. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/cmpa", + }] + async fn sp_rot_cmpa_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Read the requested CFPA slot from a root of trust. + /// + /// This endpoint is only valid for the `rot` component. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/cfpa", + }] + async fn sp_rot_cfpa_get( + rqctx: RequestContext, + path: Path, + params: TypedBody, + ) -> Result, HttpError>; + + /// Read the RoT boot state from a root of trust + /// + /// This endpoint is only valid for the `rot` component. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/component/{component}/rot-boot-info", + }] + async fn sp_rot_boot_info( + rqctx: RequestContext, + path: Path, + params: TypedBody, + ) -> Result, HttpError>; + + /// List SPs via Ignition + /// + /// Retreive information for all SPs via the Ignition controller. This is + /// lower latency and has fewer possible failure modes than querying the SP + /// over the management network. + #[endpoint { + method = GET, + path = "/ignition", + }] + async fn ignition_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Get SP info via Ignition + /// + /// Retreive information for an SP via the Ignition controller. This is + /// lower latency and has fewer possible failure modes than querying the SP + /// over the management network. + #[endpoint { + method = GET, + path = "/ignition/{type}/{slot}", + }] + async fn ignition_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Send an ignition command targeting a specific SP. + /// + /// This endpoint can be used to transition a target between A2 and A3 (via + /// power-on / power-off) or reset it. + /// + /// The management network traffic caused by requests to this endpoint is + /// between this MGS instance and its local ignition controller, _not_ the + /// SP targeted by the command. + #[endpoint { + method = POST, + path = "/ignition/{type}/{slot}/{command}", + }] + async fn ignition_command( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Get the current power state of a sled via its SP. + /// + /// Note that if the sled is in A3, the SP is powered off and will not be able + /// to respond; use the ignition control endpoints for those cases. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/power-state", + }] + async fn sp_power_state_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set the current power state of a sled via its SP. + /// + /// Note that if the sled is in A3, the SP is powered off and will not be + /// able to respond; use the ignition control endpoints for those cases. + #[endpoint { + method = POST, + path = "/sp/{type}/{slot}/power-state", + }] + async fn sp_power_state_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Set the installinator image ID the sled should use for recovery. + /// + /// This value can be read by the host via IPCC; see the `ipcc` crate. + #[endpoint { + method = PUT, + path = "/sp/{type}/{slot}/ipcc/installinator-image-id", + }] + async fn sp_installinator_image_id_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Clear any previously-set installinator image ID on the target sled. + #[endpoint { + method = DELETE, + path = "/sp/{type}/{slot}/ipcc/installinator-image-id", + }] + async fn sp_installinator_image_id_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Get the most recent host phase2 request we've seen from the target SP. + /// + /// This method can be used as an indirect progress report for how far along a + /// host is when it is booting via the MGS -> SP -> UART recovery path. This + /// path is used to install the trampoline image containing installinator to + /// recover a sled. + #[endpoint { + method = GET, + path = "/sp/{type}/{slot}/host-phase2-progress", + }] + async fn sp_host_phase2_progress_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Clear the most recent host phase2 request we've seen from the target SP. + #[endpoint { + method = DELETE, + path = "/sp/{type}/{slot}/host-phase2-progress", + }] + async fn sp_host_phase2_progress_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Upload a host phase2 image that can be served to recovering hosts via the + /// host/SP control uart. + /// + /// MGS caches this image in memory and is limited to a small, fixed number of + /// images (potentially 1). Uploading a new image may evict the + /// least-recently-requested image if our cache is already full. + #[endpoint { + method = POST, + path = "/recovery/host-phase2", + }] + async fn recovery_host_phase2_upload( + rqctx: RequestContext, + body: UntypedBody, + ) -> Result, HttpError>; + + /// Get the identifier for the switch this MGS instance is connected to. + /// + /// Note that most MGS endpoints behave identically regardless of which + /// scrimlet the MGS instance is running on; this one, however, is + /// intentionally different. This endpoint is _probably_ only useful for + /// clients communicating with MGS over localhost (i.e., other services in + /// the switch zone) who need to know which sidecar they are connected to. + #[endpoint { + method = GET, + path = "/local/switch-id", + }] + async fn sp_local_switch_id( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Get the complete list of SP identifiers this MGS instance is configured to + /// find and communicate with. + /// + /// Note that unlike most MGS endpoints, this endpoint does not send any + /// communication on the management network. + #[endpoint { + method = GET, + path = "/local/all-sp-ids", + }] + async fn sp_all_ids( + rqctx: RequestContext, + ) -> Result>, HttpError>; +} + +#[derive(Deserialize, JsonSchema)] +pub struct PathSp { + /// ID for the SP that the gateway service translates into the appropriate + /// port for communicating with the given SP. + #[serde(flatten)] + pub sp: SpIdentifier, +} + +#[derive(Deserialize, JsonSchema)] +pub struct PathSpSensorId { + /// ID for the SP that the gateway service translates into the appropriate + /// port for communicating with the given SP. + #[serde(flatten)] + pub sp: SpIdentifier, + /// ID for the sensor on the SP. + pub sensor_id: u32, +} + +#[derive(Deserialize, JsonSchema)] +pub struct PathSpComponent { + /// ID for the SP that the gateway service translates into the appropriate + /// port for communicating with the given SP. + #[serde(flatten)] + pub sp: SpIdentifier, + /// ID for the component of the SP; this is the internal identifier used by + /// the SP itself to identify its components. + pub component: String, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ComponentCabooseSlot { + /// The firmware slot to for which we want to request caboose information. + pub firmware_slot: u16, +} + +#[derive(Deserialize, JsonSchema)] +pub struct SetComponentActiveSlotParams { + /// Persist this choice of active slot. + pub persist: bool, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ComponentUpdateIdSlot { + /// An identifier for this update. + /// + /// This ID applies to this single instance of the API call; it is not an + /// ID of `image` itself. Multiple API calls with the same `image` should + /// use different IDs. + pub id: Uuid, + /// The update slot to apply this image to. Supply 0 if the component only + /// has one update slot. + pub firmware_slot: u16, +} + +#[derive(Deserialize, JsonSchema)] +pub struct UpdateAbortBody { + /// The ID of the update to abort. + /// + /// If the SP is currently receiving an update with this ID, it will be + /// aborted. + /// + /// If the SP is currently receiving an update with a different ID, the + /// abort request will fail. + /// + /// If the SP is not currently receiving any update, the request to abort + /// should succeed but will not have actually done anything. + pub id: Uuid, +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, +)] +pub struct GetCfpaParams { + pub slot: RotCfpaSlot, +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, +)] +pub struct GetRotBootInfoParams { + pub version: u8, +} + +#[derive(Deserialize, JsonSchema)] +pub struct PathSpIgnitionCommand { + /// ID for the SP that the gateway service translates into the appropriate + /// port for communicating with the given SP. + #[serde(flatten)] + pub sp: SpIdentifier, + /// Ignition command to perform on the targeted SP. + pub command: IgnitionCommand, +} diff --git a/gateway-test-utils/Cargo.toml b/gateway-test-utils/Cargo.toml index 08e22228fe..fd70abdf66 100644 --- a/gateway-test-utils/Cargo.toml +++ b/gateway-test-utils/Cargo.toml @@ -11,6 +11,7 @@ workspace = true camino.workspace = true dropshot.workspace = true gateway-messages.workspace = true +gateway-types.workspace = true omicron-gateway.workspace = true omicron-test-utils.workspace = true slog.workspace = true diff --git a/gateway-test-utils/src/sim_state.rs b/gateway-test-utils/src/sim_state.rs index 71802e0104..b88b8e7afb 100644 --- a/gateway-test-utils/src/sim_state.rs +++ b/gateway-test-utils/src/sim_state.rs @@ -6,10 +6,10 @@ use gateway_messages::ignition::SystemPowerState; use gateway_messages::ignition::SystemType; -use omicron_gateway::http_entrypoints::SpIdentifier; -use omicron_gateway::http_entrypoints::SpIgnitionInfo; -use omicron_gateway::http_entrypoints::SpState; -use omicron_gateway::http_entrypoints::SpType; +use gateway_types::component::SpIdentifier; +use gateway_types::component::SpState; +use gateway_types::component::SpType; +use gateway_types::ignition::SpIgnitionInfo; use sp_sim::Gimlet; use sp_sim::SimRack; use sp_sim::SimulatedSp; diff --git a/gateway-types/Cargo.toml b/gateway-types/Cargo.toml new file mode 100644 index 0000000000..9d78f366b7 --- /dev/null +++ b/gateway-types/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "gateway-types" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +gateway-messages.workspace = true +# Avoid depending on gateway-sp-comms! It is a pretty heavy dependency and +# would only be used for From impls anyway. We put those impls in +# omicron-gateway instead, and don't use `From`. +hex.workspace = true +ipcc.workspace = true +omicron-common.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true +uuid.workspace = true diff --git a/gateway-types/src/caboose.rs b/gateway-types/src/caboose.rs new file mode 100644 index 0000000000..97c9cfd7dc --- /dev/null +++ b/gateway-types/src/caboose.rs @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SpComponentCaboose { + pub git_commit: String, + pub board: String, + pub name: String, + pub version: String, +} diff --git a/gateway-types/src/component.rs b/gateway-types/src/component.rs new file mode 100644 index 0000000000..f0ca730b49 --- /dev/null +++ b/gateway-types/src/component.rs @@ -0,0 +1,286 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::{fmt, str}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::rot::RotState; + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum SpType { + Sled, + Power, + Switch, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +pub struct SpIdentifier { + #[serde(rename = "type")] + pub typ: SpType, + #[serde(deserialize_with = "deserializer_u32_from_string")] + pub slot: u32, +} + +impl fmt::Display for SpIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?} {}", self.typ, self.slot) + } +} + +// We can't use the default `Deserialize` derivation for `SpIdentifier::slot` +// because it's embedded in other structs via `serde(flatten)`, which does not +// play well with the way dropshot parses HTTP queries/paths. serde ends up +// trying to deserialize the flattened struct as a map of strings to strings, +// which breaks on `slot` (but not on `typ` for reasons I don't entirely +// understand). We can work around by using an enum that allows either `String` +// or `u32` (which gets us past the serde map of strings), and then parsing the +// string into a u32 ourselves (which gets us to the `slot` we want). More +// background: https://github.com/serde-rs/serde/issues/1346 +fn deserializer_u32_from_string<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::{self, Unexpected}; + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + enum StringOrU32 { + String(String), + U32(u32), + } + + match StringOrU32::deserialize(deserializer)? { + StringOrU32::String(s) => s + .parse() + .map_err(|_| de::Error::invalid_type(Unexpected::Str(&s), &"u32")), + StringOrU32::U32(n) => Ok(n), + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub struct SpState { + pub serial_number: String, + pub model: String, + pub revision: u32, + pub hubris_archive_id: String, + pub base_mac_address: [u8; 6], + pub power_state: PowerState, + pub rot: RotState, +} + +// We expect serial and model numbers to be ASCII and 0-padded: find the first 0 +// byte and convert to a string. If that fails, hexlify the entire slice. +fn stringify_byte_string(bytes: &[u8]) -> String { + let first_zero = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + + str::from_utf8(&bytes[..first_zero]) + .map(|s| s.to_string()) + .unwrap_or_else(|_err| hex::encode(bytes)) +} + +impl From<(gateway_messages::SpStateV1, RotState)> for SpState { + fn from(all: (gateway_messages::SpStateV1, RotState)) -> Self { + let (state, rot) = all; + Self { + serial_number: stringify_byte_string(&state.serial_number), + model: stringify_byte_string(&state.model), + revision: state.revision, + hubris_archive_id: hex::encode(state.hubris_archive_id), + base_mac_address: state.base_mac_address, + power_state: PowerState::from(state.power_state), + rot, + } + } +} + +impl From for SpState { + fn from(state: gateway_messages::SpStateV1) -> Self { + Self::from((state, RotState::from(state.rot))) + } +} + +impl From<(gateway_messages::SpStateV2, RotState)> for SpState { + fn from(all: (gateway_messages::SpStateV2, RotState)) -> Self { + let (state, rot) = all; + Self { + serial_number: stringify_byte_string(&state.serial_number), + model: stringify_byte_string(&state.model), + revision: state.revision, + hubris_archive_id: hex::encode(state.hubris_archive_id), + base_mac_address: state.base_mac_address, + power_state: PowerState::from(state.power_state), + rot, + } + } +} + +impl From for SpState { + fn from(state: gateway_messages::SpStateV2) -> Self { + Self::from((state, RotState::from(state.rot))) + } +} + +impl From<(gateway_messages::SpStateV3, RotState)> for SpState { + fn from(all: (gateway_messages::SpStateV3, RotState)) -> Self { + let (state, rot) = all; + Self { + serial_number: stringify_byte_string(&state.serial_number), + model: stringify_byte_string(&state.model), + revision: state.revision, + hubris_archive_id: hex::encode(state.hubris_archive_id), + base_mac_address: state.base_mac_address, + power_state: PowerState::from(state.power_state), + rot, + } + } +} + +/// See RFD 81. +/// +/// This enum only lists power states the SP is able to control; higher power +/// states are controlled by ignition. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +pub enum PowerState { + A0, + A1, + A2, +} + +impl From for PowerState { + fn from(power_state: gateway_messages::PowerState) -> Self { + match power_state { + gateway_messages::PowerState::A0 => Self::A0, + gateway_messages::PowerState::A1 => Self::A1, + gateway_messages::PowerState::A2 => Self::A2, + } + } +} + +impl From for gateway_messages::PowerState { + fn from(power_state: PowerState) -> Self { + match power_state { + PowerState::A0 => Self::A0, + PowerState::A1 => Self::A1, + PowerState::A2 => Self::A2, + } + } +} + +/// List of components from a single SP. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct SpComponentList { + pub components: Vec, +} + +/// Overview of a single SP component. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct SpComponentInfo { + /// The unique identifier for this component. + pub component: String, + /// The name of the physical device. + pub device: String, + /// The component's serial number, if it has one. + pub serial_number: Option, + /// A human-readable description of the component. + pub description: String, + /// `capabilities` is a bitmask; interpret it via + /// [`gateway_messages::DeviceCapabilities`]. + pub capabilities: u32, + /// Whether or not the component is present, to the best of the SP's ability + /// to judge. + pub presence: SpComponentPresence, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Description of the presence or absence of a component. +/// +/// The presence of some components may vary based on the power state of the +/// sled (e.g., components that time out or appear unavailable if the sled is in +/// A2 may become present when the sled moves to A0). +pub enum SpComponentPresence { + /// The component is present. + Present, + /// The component is not present. + NotPresent, + /// The component is present but in a failed or faulty state. + Failed, + /// The SP is unable to determine the presence of the component. + Unavailable, + /// The SP's attempt to determine the presence of the component timed out. + Timeout, + /// The SP's attempt to determine the presence of the component failed. + Error, +} + +impl From for SpComponentPresence { + fn from(p: gateway_messages::DevicePresence) -> Self { + match p { + gateway_messages::DevicePresence::Present => Self::Present, + gateway_messages::DevicePresence::NotPresent => Self::NotPresent, + gateway_messages::DevicePresence::Failed => Self::Failed, + gateway_messages::DevicePresence::Unavailable => Self::Unavailable, + gateway_messages::DevicePresence::Timeout => Self::Timeout, + gateway_messages::DevicePresence::Error => Self::Error, + } + } +} + +/// Identifier for an SP's component's firmware slot; e.g., slots 0 and 1 for +/// the host boot flash. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, +)] +pub struct SpComponentFirmwareSlot { + pub slot: u16, +} diff --git a/gateway/src/http_entrypoints/component_details.rs b/gateway-types/src/component_details.rs similarity index 99% rename from gateway/src/http_entrypoints/component_details.rs rename to gateway-types/src/component_details.rs index 65e074fec6..2b41d4b6f7 100644 --- a/gateway/src/http_entrypoints/component_details.rs +++ b/gateway-types/src/component_details.rs @@ -2,8 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -// Copyright 2023 Oxide Computer Company - use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; diff --git a/gateway-types/src/host.rs b/gateway-types/src/host.rs new file mode 100644 index 0000000000..73130b0a1a --- /dev/null +++ b/gateway-types/src/host.rs @@ -0,0 +1,58 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use gateway_messages::StartupOptions; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, +)] +pub struct HostStartupOptions { + pub phase2_recovery_mode: bool, + pub kbm: bool, + pub bootrd: bool, + pub prom: bool, + pub kmdb: bool, + pub kmdb_boot: bool, + pub boot_ramdisk: bool, + pub boot_net: bool, + pub verbose: bool, +} + +impl From for StartupOptions { + fn from(mgs_opt: HostStartupOptions) -> Self { + let mut opt = StartupOptions::empty(); + opt.set( + StartupOptions::PHASE2_RECOVERY_MODE, + mgs_opt.phase2_recovery_mode, + ); + opt.set(StartupOptions::STARTUP_KBM, mgs_opt.kbm); + opt.set(StartupOptions::STARTUP_BOOTRD, mgs_opt.bootrd); + opt.set(StartupOptions::STARTUP_PROM, mgs_opt.prom); + opt.set(StartupOptions::STARTUP_KMDB, mgs_opt.kmdb); + opt.set(StartupOptions::STARTUP_KMDB_BOOT, mgs_opt.kmdb_boot); + opt.set(StartupOptions::STARTUP_BOOT_RAMDISK, mgs_opt.boot_ramdisk); + opt.set(StartupOptions::STARTUP_BOOT_NET, mgs_opt.boot_net); + opt.set(StartupOptions::STARTUP_VERBOSE, mgs_opt.verbose); + opt + } +} + +impl From for HostStartupOptions { + fn from(opt: StartupOptions) -> Self { + Self { + phase2_recovery_mode: opt + .contains(StartupOptions::PHASE2_RECOVERY_MODE), + kbm: opt.contains(StartupOptions::STARTUP_KBM), + bootrd: opt.contains(StartupOptions::STARTUP_BOOTRD), + prom: opt.contains(StartupOptions::STARTUP_PROM), + kmdb: opt.contains(StartupOptions::STARTUP_KMDB), + kmdb_boot: opt.contains(StartupOptions::STARTUP_KMDB_BOOT), + boot_ramdisk: opt.contains(StartupOptions::STARTUP_BOOT_RAMDISK), + boot_net: opt.contains(StartupOptions::STARTUP_BOOT_NET), + verbose: opt.contains(StartupOptions::STARTUP_VERBOSE), + } + } +} diff --git a/gateway-types/src/ignition.rs b/gateway-types/src/ignition.rs new file mode 100644 index 0000000000..00cd158ef8 --- /dev/null +++ b/gateway-types/src/ignition.rs @@ -0,0 +1,140 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::component::SpIdentifier; + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +pub struct SpIgnitionInfo { + pub id: SpIdentifier, + pub details: SpIgnition, +} + +/// State of an ignition target. +/// +/// TODO: Ignition returns much more information than we're reporting here: do +/// we want to expand this? +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "present")] +pub enum SpIgnition { + #[serde(rename = "no")] + Absent, + #[serde(rename = "yes")] + Present { + id: SpIgnitionSystemType, + power: bool, + ctrl_detect_0: bool, + ctrl_detect_1: bool, + flt_a3: bool, + flt_a2: bool, + flt_rot: bool, + flt_sp: bool, + }, +} + +impl From for SpIgnition { + fn from(state: gateway_messages::IgnitionState) -> Self { + use gateway_messages::ignition::SystemPowerState; + + if let Some(target_state) = state.target { + Self::Present { + id: target_state.system_type.into(), + power: matches!( + target_state.power_state, + SystemPowerState::On | SystemPowerState::PoweringOn + ), + ctrl_detect_0: target_state.controller0_present, + ctrl_detect_1: target_state.controller1_present, + flt_a3: target_state.faults.power_a3, + flt_a2: target_state.faults.power_a2, + flt_rot: target_state.faults.rot, + flt_sp: target_state.faults.sp, + } + } else { + Self::Absent + } + } +} + +/// Ignition command. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum IgnitionCommand { + PowerOn, + PowerOff, + PowerReset, +} + +impl From for gateway_messages::IgnitionCommand { + fn from(cmd: IgnitionCommand) -> Self { + match cmd { + IgnitionCommand::PowerOn => { + gateway_messages::IgnitionCommand::PowerOn + } + IgnitionCommand::PowerOff => { + gateway_messages::IgnitionCommand::PowerOff + } + IgnitionCommand::PowerReset => { + gateway_messages::IgnitionCommand::PowerReset + } + } + } +} + +/// TODO: Do we want to bake in specific board names, or use raw u16 ID numbers? +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "system_type", rename_all = "snake_case")] +pub enum SpIgnitionSystemType { + Gimlet, + Sidecar, + Psc, + Unknown { id: u16 }, +} + +impl From for SpIgnitionSystemType { + fn from(st: gateway_messages::ignition::SystemType) -> Self { + use gateway_messages::ignition::SystemType; + match st { + SystemType::Gimlet => Self::Gimlet, + SystemType::Sidecar => Self::Sidecar, + SystemType::Psc => Self::Psc, + SystemType::Unknown(id) => Self::Unknown { id }, + } + } +} diff --git a/gateway-types/src/lib.rs b/gateway-types/src/lib.rs new file mode 100644 index 0000000000..61bc291510 --- /dev/null +++ b/gateway-types/src/lib.rs @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Common types for MGS. + +pub mod caboose; +pub mod component; +pub mod component_details; +pub mod host; +pub mod ignition; +pub mod rot; +pub mod sensor; +pub mod update; diff --git a/gateway-types/src/rot.rs b/gateway-types/src/rot.rs new file mode 100644 index 0000000000..66ac20afdb --- /dev/null +++ b/gateway-types/src/rot.rs @@ -0,0 +1,355 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum RotState { + V2 { + active: RotSlot, + persistent_boot_preference: RotSlot, + pending_persistent_boot_preference: Option, + transient_boot_preference: Option, + slot_a_sha3_256_digest: Option, + slot_b_sha3_256_digest: Option, + }, + CommunicationFailed { + message: String, + }, + V3 { + active: RotSlot, + persistent_boot_preference: RotSlot, + pending_persistent_boot_preference: Option, + transient_boot_preference: Option, + + slot_a_fwid: String, + slot_b_fwid: String, + stage0_fwid: String, + stage0next_fwid: String, + + slot_a_error: Option, + slot_b_error: Option, + stage0_error: Option, + stage0next_error: Option, + }, +} + +impl RotState { + fn fwid_to_string(fwid: gateway_messages::Fwid) -> String { + match fwid { + gateway_messages::Fwid::Sha3_256(digest) => hex::encode(digest), + } + } +} + +impl From> + for RotState +{ + fn from( + result: Result, + ) -> Self { + match result { + Ok(state) => Self::from(state), + Err(err) => Self::CommunicationFailed { message: err.to_string() }, + } + } +} + +impl From> + for RotState +{ + fn from( + result: Result< + gateway_messages::RotStateV2, + gateway_messages::RotError, + >, + ) -> Self { + match result { + Ok(state) => Self::from(state), + Err(err) => Self::CommunicationFailed { message: err.to_string() }, + } + } +} + +impl From for RotState { + fn from(state: gateway_messages::RotState) -> Self { + let boot_state = state.rot_updates.boot_state; + Self::V2 { + active: boot_state.active.into(), + slot_a_sha3_256_digest: boot_state + .slot_a + .map(|details| hex::encode(details.digest)), + slot_b_sha3_256_digest: boot_state + .slot_b + .map(|details| hex::encode(details.digest)), + // RotState(V1) didn't have the following fields, so we make + // it up as best we can. This RoT version is pre-shipping + // and should only exist on (not updated recently) test + // systems. + persistent_boot_preference: boot_state.active.into(), + pending_persistent_boot_preference: None, + transient_boot_preference: None, + } + } +} + +impl From for RotState { + fn from(state: gateway_messages::RotStateV2) -> Self { + Self::V2 { + active: state.active.into(), + persistent_boot_preference: state.persistent_boot_preference.into(), + pending_persistent_boot_preference: state + .pending_persistent_boot_preference + .map(Into::into), + transient_boot_preference: state + .transient_boot_preference + .map(Into::into), + slot_a_sha3_256_digest: state + .slot_a_sha3_256_digest + .map(hex::encode), + slot_b_sha3_256_digest: state + .slot_b_sha3_256_digest + .map(hex::encode), + } + } +} + +impl From for RotState { + fn from(state: gateway_messages::RotStateV3) -> Self { + Self::V3 { + active: state.active.into(), + persistent_boot_preference: state.persistent_boot_preference.into(), + pending_persistent_boot_preference: state + .pending_persistent_boot_preference + .map(Into::into), + transient_boot_preference: state + .transient_boot_preference + .map(Into::into), + slot_a_fwid: Self::fwid_to_string(state.slot_a_fwid), + slot_b_fwid: Self::fwid_to_string(state.slot_b_fwid), + + stage0_fwid: Self::fwid_to_string(state.stage0_fwid), + stage0next_fwid: Self::fwid_to_string(state.stage0next_fwid), + + slot_a_error: state.slot_a_status.err().map(From::from), + slot_b_error: state.slot_b_status.err().map(From::from), + + stage0_error: state.stage0_status.err().map(From::from), + stage0next_error: state.stage0next_status.err().map(From::from), + } + } +} + +impl From for RotState { + fn from(value: gateway_messages::RotBootInfo) -> Self { + match value { + gateway_messages::RotBootInfo::V1(s) => Self::from(s), + gateway_messages::RotBootInfo::V2(s) => Self::from(s), + gateway_messages::RotBootInfo::V3(s) => Self::from(s), + } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "slot", rename_all = "snake_case")] +pub enum RotSlot { + A, + B, +} + +impl From for RotSlot { + fn from(slot: gateway_messages::RotSlotId) -> Self { + match slot { + gateway_messages::RotSlotId::A => Self::A, + gateway_messages::RotSlotId::B => Self::B, + } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +pub struct RotImageDetails { + pub digest: String, + pub version: ImageVersion, +} + +impl From for RotImageDetails { + fn from(details: gateway_messages::RotImageDetails) -> Self { + Self { + digest: hex::encode(details.digest), + version: details.version.into(), + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +pub struct ImageVersion { + pub epoch: u32, + pub version: u32, +} + +impl From for ImageVersion { + fn from(v: gateway_messages::ImageVersion) -> Self { + Self { epoch: v.epoch, version: v.version } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum RotImageError { + Unchecked, + FirstPageErased, + PartiallyProgrammed, + InvalidLength, + HeaderNotProgrammed, + BootloaderTooSmall, + BadMagic, + HeaderImageSize, + UnalignedLength, + UnsupportedType, + ResetVectorNotThumb2, + ResetVector, + Signature, +} + +impl From for RotImageError { + fn from(error: gateway_messages::ImageError) -> Self { + match error { + gateway_messages::ImageError::Unchecked => RotImageError::Unchecked, + gateway_messages::ImageError::FirstPageErased => { + RotImageError::FirstPageErased + } + gateway_messages::ImageError::PartiallyProgrammed => { + RotImageError::PartiallyProgrammed + } + gateway_messages::ImageError::InvalidLength => { + RotImageError::InvalidLength + } + gateway_messages::ImageError::HeaderNotProgrammed => { + RotImageError::HeaderNotProgrammed + } + gateway_messages::ImageError::BootloaderTooSmall => { + RotImageError::BootloaderTooSmall + } + gateway_messages::ImageError::BadMagic => RotImageError::BadMagic, + gateway_messages::ImageError::HeaderImageSize => { + RotImageError::HeaderImageSize + } + gateway_messages::ImageError::UnalignedLength => { + RotImageError::UnalignedLength + } + gateway_messages::ImageError::UnsupportedType => { + RotImageError::UnsupportedType + } + gateway_messages::ImageError::ResetVectorNotThumb2 => { + RotImageError::ResetVectorNotThumb2 + } + gateway_messages::ImageError::ResetVector => { + RotImageError::ResetVector + } + gateway_messages::ImageError::Signature => RotImageError::Signature, + } + } +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +pub struct RotCmpa { + pub base64_data: String, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "slot", rename_all = "snake_case")] +pub enum RotCfpaSlot { + Active, + Inactive, + Scratch, +} + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, +)] +pub struct RotCfpa { + pub base64_data: String, + pub slot: RotCfpaSlot, +} diff --git a/gateway-types/src/sensor.rs b/gateway-types/src/sensor.rs new file mode 100644 index 0000000000..59ae9e9542 --- /dev/null +++ b/gateway-types/src/sensor.rs @@ -0,0 +1,74 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Result of reading an SP sensor. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + PartialOrd, + Serialize, + Deserialize, + JsonSchema, +)] +pub struct SpSensorReading { + /// SP-centric timestamp of when `result` was recorded from this sensor. + /// + /// Currently this value represents "milliseconds since the last SP boot" + /// and is primarily useful as a delta between sensors on this SP (assuming + /// no reboot in between). The meaning could change with future SP releases. + pub timestamp: u64, + /// Value (or error) from the sensor. + pub result: SpSensorReadingResult, +} + +impl From for SpSensorReading { + fn from(value: gateway_messages::SensorReading) -> Self { + Self { + timestamp: value.timestamp, + result: match value.value { + Ok(value) => SpSensorReadingResult::Success { value }, + Err(err) => err.into(), + }, + } + } +} + +/// Single reading (or error) from an SP sensor. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + PartialOrd, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum SpSensorReadingResult { + Success { value: f32 }, + DeviceOff, + DeviceError, + DeviceNotPresent, + DeviceUnavailable, + DeviceTimeout, +} + +impl From for SpSensorReadingResult { + fn from(value: gateway_messages::SensorDataMissing) -> Self { + use gateway_messages::SensorDataMissing; + match value { + SensorDataMissing::DeviceOff => Self::DeviceOff, + SensorDataMissing::DeviceError => Self::DeviceError, + SensorDataMissing::DeviceNotPresent => Self::DeviceNotPresent, + SensorDataMissing::DeviceUnavailable => Self::DeviceUnavailable, + SensorDataMissing::DeviceTimeout => Self::DeviceTimeout, + } + } +} diff --git a/gateway-types/src/update.rs b/gateway-types/src/update.rs new file mode 100644 index 0000000000..916927bd90 --- /dev/null +++ b/gateway-types/src/update.rs @@ -0,0 +1,129 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::time::Duration; + +use gateway_messages::UpdateStatus; +use omicron_common::update::ArtifactHash; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum SpUpdateStatus { + /// The SP has no update status. + None, + /// The SP is preparing to receive an update. + /// + /// May or may not include progress, depending on the capabilities of the + /// component being updated. + Preparing { id: Uuid, progress: Option }, + /// The SP is currently receiving an update. + InProgress { id: Uuid, bytes_received: u32, total_bytes: u32 }, + /// The SP has completed receiving an update. + Complete { id: Uuid }, + /// The SP has aborted an in-progress update. + Aborted { id: Uuid }, + /// The update process failed. + Failed { id: Uuid, code: u32 }, + /// The update process failed with an RoT-specific error. + RotError { id: Uuid, message: String }, +} + +impl From for SpUpdateStatus { + fn from(status: UpdateStatus) -> Self { + match status { + UpdateStatus::None => Self::None, + UpdateStatus::Preparing(status) => Self::Preparing { + id: status.id.into(), + progress: status.progress.map(Into::into), + }, + UpdateStatus::SpUpdateAuxFlashChckScan { + id, total_size, .. + } => Self::InProgress { + id: id.into(), + bytes_received: 0, + total_bytes: total_size, + }, + UpdateStatus::InProgress(status) => Self::InProgress { + id: status.id.into(), + bytes_received: status.bytes_received, + total_bytes: status.total_size, + }, + UpdateStatus::Complete(id) => Self::Complete { id: id.into() }, + UpdateStatus::Aborted(id) => Self::Aborted { id: id.into() }, + UpdateStatus::Failed { id, code } => { + Self::Failed { id: id.into(), code } + } + UpdateStatus::RotError { id, error } => { + Self::RotError { id: id.into(), message: format!("{error:?}") } + } + } + } +} + +/// Progress of an SP preparing to update. +/// +/// The units of `current` and `total` are unspecified and defined by the SP; +/// e.g., if preparing for an update requires erasing a flash device, this may +/// indicate progress of that erasure without defining units (bytes, pages, +/// sectors, etc.). +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct UpdatePreparationProgress { + pub current: u32, + pub total: u32, +} + +impl From + for UpdatePreparationProgress +{ + fn from(progress: gateway_messages::UpdatePreparationProgress) -> Self { + Self { current: progress.current, total: progress.total } + } +} + +// This type is a duplicate of the type in `ipcc`, and we provide a +// `From<_>` impl to convert to it. We keep these types distinct to allow us to +// choose different representations for MGS's HTTP API (this type) and the wire +// format passed through the SP to installinator +// (`ipcc::InstallinatorImageId`), although _currently_ they happen to +// be defined identically. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub struct InstallinatorImageId { + pub update_id: Uuid, + pub host_phase_2: ArtifactHash, + pub control_plane: ArtifactHash, +} + +impl From for ipcc::InstallinatorImageId { + fn from(id: InstallinatorImageId) -> Self { + Self { + update_id: id.update_id, + host_phase_2: id.host_phase_2, + control_plane: id.control_plane, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "progress", rename_all = "snake_case")] +pub enum HostPhase2Progress { + Available { + image_id: HostPhase2RecoveryImageId, + offset: u64, + total_size: u64, + age: Duration, + }, + None, +} + +/// Identity of a host phase2 recovery image. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct HostPhase2RecoveryImageId { + pub sha256_hash: ArtifactHash, +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 2ddd9421b7..50bd253952 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -14,8 +14,10 @@ camino.workspace = true clap.workspace = true dropshot.workspace = true futures.workspace = true +gateway-api.workspace = true gateway-messages.workspace = true gateway-sp-comms.workspace = true +gateway-types.workspace = true hex.workspace = true http.workspace = true hyper.workspace = true diff --git a/gateway/src/bin/mgs.rs b/gateway/src/bin/mgs.rs index 39810ea06a..91290bffae 100644 --- a/gateway/src/bin/mgs.rs +++ b/gateway/src/bin/mgs.rs @@ -9,7 +9,7 @@ use camino::Utf8PathBuf; use clap::Parser; use futures::StreamExt; use omicron_common::cmd::{fatal, CmdError}; -use omicron_gateway::{run_openapi, start_server, Config, MgsArguments}; +use omicron_gateway::{start_server, Config, MgsArguments}; use signal_hook::consts::signal; use signal_hook_tokio::Signals; use std::net::SocketAddrV6; @@ -18,9 +18,6 @@ use uuid::Uuid; #[derive(Debug, Parser)] #[clap(name = "gateway", about = "See README.adoc for more information")] enum Args { - /// Print the external OpenAPI Spec document and exit - Openapi, - /// Start an MGS server Run { #[clap(name = "CONFIG_FILE_PATH", action)] @@ -71,9 +68,6 @@ async fn do_run() -> Result<(), CmdError> { let args = Args::parse(); match args { - Args::Openapi => { - run_openapi().map_err(|e| CmdError::Failure(anyhow!(e))) - } Args::Run { config_file_path, id_and_address_from_smf, diff --git a/gateway/src/http_entrypoints.rs b/gateway/src/http_entrypoints.rs index fa91bcebf5..e833c660f0 100644 --- a/gateway/src/http_entrypoints.rs +++ b/gateway/src/http_entrypoints.rs @@ -6,18 +6,11 @@ //! HTTP entrypoint functions for the gateway service -mod component_details; -mod conversions; - -use self::component_details::SpComponentDetails; -use self::conversions::component_from_str; use crate::error::SpCommsError; use crate::http_err_with_message; use crate::ServerContext; use base64::Engine; -use dropshot::endpoint; use dropshot::ApiDescription; -use dropshot::ApiDescriptionRegisterError; use dropshot::HttpError; use dropshot::HttpResponseOk; use dropshot::HttpResponseUpdatedNoContent; @@ -29,1693 +22,828 @@ use dropshot::UntypedBody; use dropshot::WebsocketEndpointResult; use dropshot::WebsocketUpgrade; use futures::TryFutureExt; +use gateway_api::*; +use gateway_messages::RotBootInfo; use gateway_messages::SpComponent; +use gateway_sp_comms::error::CommunicationError; use gateway_sp_comms::HostPhase2Provider; +use gateway_sp_comms::VersionedSpState; +use gateway_types::caboose::SpComponentCaboose; +use gateway_types::component::PowerState; +use gateway_types::component::SpComponentFirmwareSlot; +use gateway_types::component::SpComponentInfo; +use gateway_types::component::SpComponentList; +use gateway_types::component::SpIdentifier; +use gateway_types::component::SpState; +use gateway_types::component_details::SpComponentDetails; +use gateway_types::host::HostStartupOptions; +use gateway_types::ignition::SpIgnitionInfo; +use gateway_types::rot::RotCfpa; +use gateway_types::rot::RotCfpaSlot; +use gateway_types::rot::RotCmpa; +use gateway_types::rot::RotState; +use gateway_types::sensor::SpSensorReading; +use gateway_types::update::HostPhase2Progress; +use gateway_types::update::HostPhase2RecoveryImageId; +use gateway_types::update::InstallinatorImageId; +use gateway_types::update::SpUpdateStatus; use omicron_common::update::ArtifactHash; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Display; use std::str; use std::sync::Arc; -use std::time::Duration; -use uuid::Uuid; - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub struct SpState { - pub serial_number: String, - pub model: String, - pub revision: u32, - pub hubris_archive_id: String, - pub base_mac_address: [u8; 6], - pub power_state: PowerState, - pub rot: RotState, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub enum RotImageError { - Unchecked, - FirstPageErased, - PartiallyProgrammed, - InvalidLength, - HeaderNotProgrammed, - BootloaderTooSmall, - BadMagic, - HeaderImageSize, - UnalignedLength, - UnsupportedType, - ResetVectorNotThumb2, - ResetVector, - Signature, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "state", rename_all = "snake_case")] -pub enum RotState { - V2 { - active: RotSlot, - persistent_boot_preference: RotSlot, - pending_persistent_boot_preference: Option, - transient_boot_preference: Option, - slot_a_sha3_256_digest: Option, - slot_b_sha3_256_digest: Option, - }, - CommunicationFailed { - message: String, - }, - V3 { - active: RotSlot, - persistent_boot_preference: RotSlot, - pending_persistent_boot_preference: Option, - transient_boot_preference: Option, - - slot_a_fwid: String, - slot_b_fwid: String, - stage0_fwid: String, - stage0next_fwid: String, - - slot_a_error: Option, - slot_b_error: Option, - stage0_error: Option, - stage0next_error: Option, - }, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "slot", rename_all = "snake_case")] -pub enum RotSlot { - A, - B, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct RotImageDetails { - pub digest: String, - pub version: ImageVersion, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct RotCmpa { - pub base64_data: String, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "slot", rename_all = "snake_case")] -pub enum RotCfpaSlot { - Active, - Inactive, - Scratch, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct GetCfpaParams { - pub slot: RotCfpaSlot, -} -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct RotCfpa { - pub base64_data: String, - pub slot: RotCfpaSlot, -} - -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct GetRotBootInfoParams { - pub version: u8, -} +// TODO +// The gateway service will get asynchronous notifications both from directly +// SPs over the management network and indirectly from Ignition via the Sidecar +// SP. +// TODO The Ignition controller will send an interrupt to its local SP. Will +// that SP then notify both gateway services or just its local gateway service? +// Both Ignition controller should both do the same thing at about the same +// time so is there a real benefit to them both sending messages to both +// gateways? This would cause a single message to effectively be replicated 4x +// (Nexus would need to dedup these). -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct SpIgnitionInfo { - pub id: SpIdentifier, - pub details: SpIgnition, -} +type GatewayApiDescription = ApiDescription>; -/// State of an ignition target. -/// -/// TODO: Ignition returns much more information than we're reporting here: do -/// we want to expand this? -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "present")] -pub enum SpIgnition { - #[serde(rename = "no")] - Absent, - #[serde(rename = "yes")] - Present { - id: SpIgnitionSystemType, - power: bool, - ctrl_detect_0: bool, - ctrl_detect_1: bool, - flt_a3: bool, - flt_a2: bool, - flt_rot: bool, - flt_sp: bool, - }, +/// Returns a description of the gateway API +pub fn api() -> GatewayApiDescription { + gateway_api_mod::api_description::() + .expect("entrypoints registered successfully") } +enum GatewayImpl {} -/// Ignition command. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub enum IgnitionCommand { - PowerOn, - PowerOff, - PowerReset, -} +impl GatewayApi for GatewayImpl { + type Context = Arc; -/// TODO: Do we want to bake in specific board names, or use raw u16 ID numbers? -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "system_type", rename_all = "snake_case")] -pub enum SpIgnitionSystemType { - Gimlet, - Sidecar, - Psc, - Unknown { id: u16 }, -} + /// Get info on an SP + async fn sp_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "state", rename_all = "snake_case")] -enum SpUpdateStatus { - /// The SP has no update status. - None, - /// The SP is preparing to receive an update. - /// - /// May or may not include progress, depending on the capabilities of the - /// component being updated. - Preparing { id: Uuid, progress: Option }, - /// The SP is currently receiving an update. - InProgress { id: Uuid, bytes_received: u32, total_bytes: u32 }, - /// The SP has completed receiving an update. - Complete { id: Uuid }, - /// The SP has aborted an in-progress update. - Aborted { id: Uuid }, - /// The update process failed. - Failed { id: Uuid, code: u32 }, - /// The update process failed with an RoT-specific error. - RotError { id: Uuid, message: String }, -} + let state = sp.state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -/// Progress of an SP preparing to update. -/// -/// The units of `current` and `total` are unspecified and defined by the SP; -/// e.g., if preparing for an update requires erasing a flash device, this may -/// indicate progress of that erasure without defining units (bytes, pages, -/// sectors, etc.). -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] -struct UpdatePreparationProgress { - current: u32, - total: u32, -} + let rot_state = sp + .rot_state(gateway_messages::RotBootInfo::HIGHEST_KNOWN_VERSION) + .await; -/// Result of reading an SP sensor. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - PartialOrd, - Serialize, - Deserialize, - JsonSchema, -)] -pub struct SpSensorReading { - /// SP-centric timestamp of when `result` was recorded from this sensor. - /// - /// Currently this value represents "milliseconds since the last SP boot" - /// and is primarily useful as a delta between sensors on this SP (assuming - /// no reboot in between). The meaning could change with future SP releases. - pub timestamp: u64, - /// Value (or error) from the sensor. - pub result: SpSensorReadingResult, -} + let final_state = sp_state_from_comms(state, rot_state); + Ok(HttpResponseOk(final_state)) + } -/// Single reading (or error) from an SP sensor. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - PartialOrd, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SpSensorReadingResult { - Success { value: f32 }, - DeviceOff, - DeviceError, - DeviceNotPresent, - DeviceUnavailable, - DeviceTimeout, -} + async fn sp_startup_options_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let mgmt_switch = &apictx.mgmt_switch; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; + + let options = sp.get_startup_options().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -/// List of components from a single SP. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct SpComponentList { - pub components: Vec, -} + Ok(HttpResponseOk(options.into())) + } -/// Overview of a single SP component. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct SpComponentInfo { - /// The unique identifier for this component. - pub component: String, - /// The name of the physical device. - pub device: String, - /// The component's serial number, if it has one. - pub serial_number: Option, - /// A human-readable description of the component. - pub description: String, - /// `capabilities` is a bitmask; interpret it via - /// [`gateway_messages::DeviceCapabilities`]. - pub capabilities: u32, - /// Whether or not the component is present, to the best of the SP's ability - /// to judge. - pub presence: SpComponentPresence, -} + async fn sp_startup_options_set( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let apictx = rqctx.context(); + let mgmt_switch = &apictx.mgmt_switch; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; + + sp.set_startup_options(body.into_inner().into()).await.map_err( + |err| SpCommsError::SpCommunicationFailed { sp: sp_id, err }, + )?; + + Ok(HttpResponseUpdatedNoContent {}) + } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -/// Description of the presence or absence of a component. -/// -/// The presence of some components may vary based on the power state of the -/// sled (e.g., components that time out or appear unavailable if the sled is in -/// A2 may become present when the sled moves to A0). -pub enum SpComponentPresence { - /// The component is present. - Present, - /// The component is not present. - NotPresent, - /// The component is present but in a failed or faulty state. - Failed, - /// The SP is unable to determine the presence of the component. - Unavailable, - /// The SP's attempt to determine the presence of the component timed out. - Timeout, - /// The SP's attempt to determine the presence of the component failed. - Error, -} + async fn sp_sensor_read_value( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let PathSpSensorId { sp, sensor_id } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let value = sp.read_sensor_value(sensor_id).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - JsonSchema, -)] -#[serde(rename_all = "lowercase")] -pub enum SpType { - Sled, - Power, - Switch, -} + Ok(HttpResponseOk(value.into())) + } -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - JsonSchema, -)] -pub struct SpIdentifier { - #[serde(rename = "type")] - pub typ: SpType, - #[serde(deserialize_with = "deserializer_u32_from_string")] - pub slot: u32, -} + async fn sp_component_list( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let inventory = sp.inventory().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -impl Display for SpIdentifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?} {}", self.typ, self.slot) + Ok(HttpResponseOk(sp_component_list_from_comms(inventory))) } -} - -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, -)] -pub struct HostStartupOptions { - pub phase2_recovery_mode: bool, - pub kbm: bool, - pub bootrd: bool, - pub prom: bool, - pub kmdb: bool, - pub kmdb_boot: bool, - pub boot_ramdisk: bool, - pub boot_net: bool, - pub verbose: bool, -} -/// See RFD 81. -/// -/// This enum only lists power states the SP is able to control; higher power -/// states are controlled by ignition. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - JsonSchema, -)] -pub enum PowerState { - A0, - A1, - A2, -} + async fn sp_component_get( + rqctx: RequestContext>, + path: Path, + ) -> Result>, HttpError> { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + + let details = sp.component_details(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - JsonSchema, -)] -pub struct ImageVersion { - pub epoch: u32, - pub version: u32, -} + Ok(HttpResponseOk( + details.entries.into_iter().map(Into::into).collect(), + )) + } -// This type is a duplicate of the type in `ipcc`, and we provide a -// `From<_>` impl to convert to it. We keep these types distinct to allow us to -// choose different representations for MGS's HTTP API (this type) and the wire -// format passed through the SP to installinator -// (`ipcc::InstallinatorImageId`), although _currently_ they happen to -// be defined identically. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub struct InstallinatorImageId { - pub update_id: Uuid, - pub host_phase_2: ArtifactHash, - pub control_plane: ArtifactHash, -} + // Implementation notes: + // + // 1. As of the time of this comment, the cannonical keys written to the hubris + // caboose are defined in https://github.com/oxidecomputer/hubtools; see + // `write_default_caboose()`. + // 2. We currently assume that the caboose always includes the same set of + // fields regardless of the component (e.g., the SP and RoT caboose have the + // same fields). If that becomes untrue, we may need to split this endpoint + // up to allow differently-typed responses. + async fn sp_component_caboose_get( + rqctx: RequestContext>, + path: Path, + query_params: Query, + ) -> Result, HttpError> { + const CABOOSE_KEY_GIT_COMMIT: [u8; 4] = *b"GITC"; + const CABOOSE_KEY_BOARD: [u8; 4] = *b"BORD"; + const CABOOSE_KEY_NAME: [u8; 4] = *b"NAME"; + const CABOOSE_KEY_VERSION: [u8; 4] = *b"VERS"; + + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let ComponentCabooseSlot { firmware_slot } = query_params.into_inner(); + let component = component_from_str(&component)?; + + let from_utf8 = |key: &[u8], bytes| { + // This helper closure is only called with the ascii-printable [u8; 4] + // key constants we define above, so we can unwrap this conversion. + let key = str::from_utf8(key).unwrap(); + String::from_utf8(bytes).map_err(|_| { + http_err_with_message( + http::StatusCode::SERVICE_UNAVAILABLE, + "InvalidCaboose", + format!("non-utf8 data returned for caboose key {key}"), + ) + }) + }; + + let git_commit = + sp.read_component_caboose( + component, + firmware_slot, + CABOOSE_KEY_GIT_COMMIT, + ) + .await + .map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; + let board = sp + .read_component_caboose(component, firmware_slot, CABOOSE_KEY_BOARD) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + let name = sp + .read_component_caboose(component, firmware_slot, CABOOSE_KEY_NAME) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + let version = + sp.read_component_caboose( + component, + firmware_slot, + CABOOSE_KEY_VERSION, + ) + .await + .map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "progress", rename_all = "snake_case")] -pub enum HostPhase2Progress { - Available { - image_id: HostPhase2RecoveryImageId, - offset: u64, - total_size: u64, - age: Duration, - }, - None, -} + let git_commit = from_utf8(&CABOOSE_KEY_GIT_COMMIT, git_commit)?; + let board = from_utf8(&CABOOSE_KEY_BOARD, board)?; + let name = from_utf8(&CABOOSE_KEY_NAME, name)?; + let version = from_utf8(&CABOOSE_KEY_VERSION, version)?; -/// Identifier for an SP's component's firmware slot; e.g., slots 0 and 1 for -/// the host boot flash. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, -)] -pub struct SpComponentFirmwareSlot { - pub slot: u16, -} + let caboose = SpComponentCaboose { git_commit, board, name, version }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct SpComponentCaboose { - pub git_commit: String, - pub board: String, - pub name: String, - pub version: String, -} + Ok(HttpResponseOk(caboose)) + } -/// Identity of a host phase2 recovery image. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct HostPhase2RecoveryImageId { - pub sha256_hash: ArtifactHash, -} + async fn sp_component_clear_status( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + + sp.component_clear_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -// We can't use the default `Deserialize` derivation for `SpIdentifier::slot` -// because it's embedded in other structs via `serde(flatten)`, which does not -// play well with the way dropshot parses HTTP queries/paths. serde ends up -// trying to deserialize the flattened struct as a map of strings to strings, -// which breaks on `slot` (but not on `typ` for reasons I don't entirely -// understand). We can work around by using an enum that allows either `String` -// or `u32` (which gets us past the serde map of strings), and then parsing the -// string into a u32 ourselves (which gets us to the `slot` we want). More -// background: https://github.com/serde-rs/serde/issues/1346 -fn deserializer_u32_from_string<'de, D>( - deserializer: D, -) -> Result -where - D: serde::Deserializer<'de>, -{ - use serde::de::{self, Unexpected}; - - #[derive(Debug, Deserialize)] - #[serde(untagged)] - enum StringOrU32 { - String(String), - U32(u32), + Ok(HttpResponseUpdatedNoContent {}) } - match StringOrU32::deserialize(deserializer)? { - StringOrU32::String(s) => s - .parse() - .map_err(|_| de::Error::invalid_type(Unexpected::Str(&s), &"u32")), - StringOrU32::U32(n) => Ok(n), + async fn sp_component_active_slot_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + + let slot = + sp.component_active_slot(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; + + Ok(HttpResponseOk(SpComponentFirmwareSlot { slot })) } -} -#[derive(Deserialize, JsonSchema)] -struct PathSp { - /// ID for the SP that the gateway service translates into the appropriate - /// port for communicating with the given SP. - #[serde(flatten)] - sp: SpIdentifier, -} - -#[derive(Deserialize, JsonSchema)] -struct PathSpSensorId { - /// ID for the SP that the gateway service translates into the appropriate - /// port for communicating with the given SP. - #[serde(flatten)] - sp: SpIdentifier, - /// ID for the sensor on the SP. - sensor_id: u32, -} - -#[derive(Serialize, Deserialize, JsonSchema)] -struct PathSpComponent { - /// ID for the SP that the gateway service translates into the appropriate - /// port for communicating with the given SP. - #[serde(flatten)] - sp: SpIdentifier, - /// ID for the component of the SP; this is the internal identifier used by - /// the SP itself to identify its components. - component: String, -} - -#[derive(Serialize, Deserialize, JsonSchema)] -struct PathSpIgnitionCommand { - /// ID for the SP that the gateway service translates into the appropriate - /// port for communicating with the given SP. - #[serde(flatten)] - sp: SpIdentifier, - /// Ignition command to perform on the targeted SP. - command: IgnitionCommand, -} - -/// Get info on an SP -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}", -}] -async fn sp_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - - let state = sp.state().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - let rot_state = sp - .rot_state(gateway_messages::RotBootInfo::HIGHEST_KNOWN_VERSION) - .await; - - let final_state = SpState::from((state, rot_state)); - Ok(HttpResponseOk(final_state)) -} - -/// Get host startup options for a sled -/// -/// This endpoint will currently fail for any `SpType` other than -/// `SpType::Sled`. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/startup-options", -}] -async fn sp_startup_options_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let mgmt_switch = &apictx.mgmt_switch; - let sp_id = path.into_inner().sp.into(); - let sp = mgmt_switch.sp(sp_id)?; - - let options = sp.get_startup_options().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(options.into())) -} - -/// Set host startup options for a sled -/// -/// This endpoint will currently fail for any `SpType` other than -/// `SpType::Sled`. -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/startup-options", -}] -async fn sp_startup_options_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let apictx = rqctx.context(); - let mgmt_switch = &apictx.mgmt_switch; - let sp_id = path.into_inner().sp.into(); - let sp = mgmt_switch.sp(sp_id)?; - - sp.set_startup_options(body.into_inner().into()).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseUpdatedNoContent {}) -} - -/// Read the current value of a sensor by ID -/// -/// Sensor IDs come from the host topo tree. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/sensor/{sensor_id}/value", -}] -async fn sp_sensor_read_value( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let PathSpSensorId { sp, sensor_id } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let value = sp.read_sensor_value(sensor_id).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(value.into())) -} - -/// List components of an SP -/// -/// A component is a distinct entity under an SP's direct control. This lists -/// all those components for a given SP. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component", -}] -async fn sp_component_list( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let inventory = sp.inventory().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(inventory.into())) -} - -/// Get info for an SP component -/// -/// This can be useful, for example, to poll the state of a component if -/// another interface has changed the power state of a component or updated a -/// component. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}", -}] -async fn sp_component_get( - rqctx: RequestContext>, - path: Path, -) -> Result>, HttpError> { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - let details = sp.component_details(component).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(details.entries.into_iter().map(Into::into).collect())) -} + async fn sp_component_active_slot_set( + rqctx: RequestContext>, + path: Path, + query_params: Query, + body: TypedBody, + ) -> Result { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + let slot = body.into_inner().slot; + let persist = query_params.into_inner().persist; + + sp.set_component_active_slot(component, slot, persist).await.map_err( + |err| SpCommsError::SpCommunicationFailed { sp: sp_id, err }, + )?; + + Ok(HttpResponseUpdatedNoContent {}) + } -// Implementation notes: -// -// 1. As of the time of this comment, the cannonical keys written to the hubris -// caboose are defined in https://github.com/oxidecomputer/hubtools; see -// `write_default_caboose()`. -// 2. We currently assume that the caboose always includes the same set of -// fields regardless of the component (e.g., the SP and RoT caboose have the -// same fields). If that becomes untrue, we may need to split this endpoint -// up to allow differently-typed responses. -/// Get the caboose of an SP component -/// -/// Not all components have a caboose. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/caboose", -}] -async fn sp_component_caboose_get( - rqctx: RequestContext>, - path: Path, - query_params: Query, -) -> Result, HttpError> { - const CABOOSE_KEY_GIT_COMMIT: [u8; 4] = *b"GITC"; - const CABOOSE_KEY_BOARD: [u8; 4] = *b"BORD"; - const CABOOSE_KEY_NAME: [u8; 4] = *b"NAME"; - const CABOOSE_KEY_VERSION: [u8; 4] = *b"VERS"; - - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let ComponentCabooseSlot { firmware_slot } = query_params.into_inner(); - let component = component_from_str(&component)?; - - let from_utf8 = |key: &[u8], bytes| { - // This helper closure is only called with the ascii-printable [u8; 4] - // key constants we define above, so we can unwrap this conversion. - let key = str::from_utf8(key).unwrap(); - String::from_utf8(bytes).map_err(|_| { - http_err_with_message( - http::StatusCode::SERVICE_UNAVAILABLE, - "InvalidCaboose", - format!("non-utf8 data returned for caboose key {key}"), - ) + async fn sp_component_serial_console_attach( + rqctx: RequestContext>, + path: Path, + websocket: WebsocketUpgrade, + ) -> WebsocketEndpointResult { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let component = component_from_str(&component)?; + + // Ensure we can attach to this SP's serial console. + let console = apictx + .mgmt_switch + .sp(sp_id)? + .serial_console_attach(component) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + + let log = apictx.log.new(slog::o!("sp" => format!("{sp:?}"))); + + // We've successfully attached to the SP's serial console: upgrade the + // websocket and run our side of that connection. + websocket.handle(move |conn| { + crate::serial_console::run(sp_id, console, conn, log) }) - }; + } - let git_commit = sp - .read_component_caboose( - component, - firmware_slot, - CABOOSE_KEY_GIT_COMMIT, - ) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, - })?; - let board = sp - .read_component_caboose(component, firmware_slot, CABOOSE_KEY_BOARD) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, - })?; - let name = sp - .read_component_caboose(component, firmware_slot, CABOOSE_KEY_NAME) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, - })?; - let version = sp - .read_component_caboose(component, firmware_slot, CABOOSE_KEY_VERSION) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, - })?; + async fn sp_component_serial_console_detach( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let apictx = rqctx.context(); - let git_commit = from_utf8(&CABOOSE_KEY_GIT_COMMIT, git_commit)?; - let board = from_utf8(&CABOOSE_KEY_BOARD, board)?; - let name = from_utf8(&CABOOSE_KEY_NAME, name)?; - let version = from_utf8(&CABOOSE_KEY_VERSION, version)?; + // TODO-cleanup: "component" support for the serial console is half baked; + // we don't use it at all to detach. + let PathSpComponent { sp, component: _ } = path.into_inner(); + let sp_id = sp.into(); - let caboose = SpComponentCaboose { git_commit, board, name, version }; + let sp = apictx.mgmt_switch.sp(sp_id)?; + sp.serial_console_detach().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; - Ok(HttpResponseOk(caboose)) -} + Ok(HttpResponseUpdatedNoContent {}) + } -/// Clear status of a component -/// -/// For components that maintain event counters (e.g., the sidecar `monorail`), -/// this will reset the event counters to zero. -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/clear-status", -}] -async fn sp_component_clear_status( - rqctx: RequestContext>, - path: Path, -) -> Result { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - sp.component_clear_status(component).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseUpdatedNoContent {}) -} + async fn sp_component_reset( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let apictx = rqctx.context(); + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + + sp.reset_component_prepare(component) + // We always want to run with the watchdog when resetting as + // disabling the watchdog should be considered a debug only feature + .and_then(|()| sp.reset_component_trigger(component, false)) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + + Ok(HttpResponseUpdatedNoContent {}) + } -/// Get the currently-active slot for an SP component -/// -/// Note that the meaning of "current" in "currently-active" may vary depending -/// on the component: for example, it may mean "the actively-running slot" or -/// "the slot that will become active the next time the component is booted". -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/active-slot", -}] -async fn sp_component_active_slot_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - let slot = sp.component_active_slot(component).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(SpComponentFirmwareSlot { slot })) -} + async fn sp_component_update( + rqctx: RequestContext>, + path: Path, + query_params: Query, + body: UntypedBody, + ) -> Result { + let apictx = rqctx.context(); + + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + let ComponentUpdateIdSlot { id, firmware_slot } = + query_params.into_inner(); + + // TODO-performance: this makes a full copy of the uploaded data + let image = body.as_bytes().to_vec(); + + sp.start_update(component, id, firmware_slot, image) + .await + .map_err(|err| SpCommsError::UpdateFailed { sp: sp_id, err })?; + + Ok(HttpResponseUpdatedNoContent {}) + } -#[derive(Deserialize, JsonSchema)] -pub struct SetComponentActiveSlotParams { - /// Persist this choice of active slot. - pub persist: bool, -} + async fn sp_component_update_status( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); -/// Set the currently-active slot for an SP component -/// -/// Note that the meaning of "current" in "currently-active" may vary depending -/// on the component: for example, it may mean "the actively-running slot" or -/// "the slot that will become active the next time the component is booted". -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/active-slot", -}] -async fn sp_component_active_slot_set( - rqctx: RequestContext>, - path: Path, - query_params: Query, - body: TypedBody, -) -> Result { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - let slot = body.into_inner().slot; - let persist = query_params.into_inner().persist; - - sp.set_component_active_slot(component, slot, persist).await.map_err( - |err| SpCommsError::SpCommunicationFailed { sp: sp_id, err }, - )?; - - Ok(HttpResponseUpdatedNoContent {}) -} + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; -/// Upgrade into a websocket connection attached to the given SP component's -/// serial console. -// This is a websocket endpoint; normally we'd expect to use `dropshot::channel` -// with `protocol = WEBSOCKETS` instead of `dropshot::endpoint`, but -// `dropshot::channel` doesn't allow us to return an error _before_ upgrading -// the connection, and we want to bail out ASAP if the SP doesn't allow us to -// attach. Therefore, we use `dropshot::endpoint` with the special argument type -// `WebsocketUpgrade`: this inserts the correct marker for progenitor to know -// this is a websocket endpoint, and it allows us to call -// `WebsocketUpgrade::handle()` (the method `dropshot::channel` would call for -// us to upgrade the connection) by hand after our error checking. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/serial-console/attach", -}] -async fn sp_component_serial_console_attach( - rqctx: RequestContext>, - path: Path, - websocket: WebsocketUpgrade, -) -> WebsocketEndpointResult { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let component = component_from_str(&component)?; - - // Ensure we can attach to this SP's serial console. - let console = apictx - .mgmt_switch - .sp(sp_id)? - .serial_console_attach(component) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, + let status = sp.update_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } })?; - let log = apictx.log.new(slog::o!("sp" => format!("{sp:?}"))); + Ok(HttpResponseOk(status.into())) + } - // We've successfully attached to the SP's serial console: upgrade the - // websocket and run our side of that connection. - websocket.handle(move |conn| { - crate::serial_console::run(sp_id, console, conn, log) - }) -} + async fn sp_component_update_abort( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let apictx = rqctx.context(); + + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let component = component_from_str(&component)?; + + let UpdateAbortBody { id } = body.into_inner(); + sp.update_abort(component, id).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -/// Detach the websocket connection attached to the given SP component's serial -/// console, if such a connection exists. -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/serial-console/detach", -}] -async fn sp_component_serial_console_detach( - rqctx: RequestContext>, - path: Path, -) -> Result { - let apictx = rqctx.context(); - - // TODO-cleanup: "component" support for the serial console is half baked; - // we don't use it at all to detach. - let PathSpComponent { sp, component: _ } = path.into_inner(); - let sp_id = sp.into(); - - let sp = apictx.mgmt_switch.sp(sp_id)?; - sp.serial_console_detach().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseUpdatedNoContent {}) -} + Ok(HttpResponseUpdatedNoContent {}) + } -#[derive(Deserialize, JsonSchema)] -pub struct ComponentUpdateIdSlot { - /// An identifier for this update. - /// - /// This ID applies to this single instance of the API call; it is not an - /// ID of `image` itself. Multiple API calls with the same `image` should - /// use different IDs. - pub id: Uuid, - /// The update slot to apply this image to. Supply 0 if the component only - /// has one update slot. - pub firmware_slot: u16, -} + async fn sp_rot_cmpa_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + + let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); + + // Ensure the caller knows they're asking for the RoT + if component_from_str(&component)? != SpComponent::ROT { + return Err(HttpError::for_bad_request( + Some("RequestUnsupportedForComponent".to_string()), + "Only the RoT has a CFPA".into(), + )); + } + + let sp = apictx.mgmt_switch.sp(sp_id)?; + let data = sp.read_rot_cmpa().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -#[derive(Deserialize, JsonSchema)] -pub struct ComponentCabooseSlot { - /// The firmware slot to for which we want to request caboose information. - pub firmware_slot: u16, -} + let base64_data = + base64::engine::general_purpose::STANDARD.encode(data); -#[derive(Deserialize, JsonSchema)] -pub struct UpdateAbortBody { - /// The ID of the update to abort. - /// - /// If the SP is currently receiving an update with this ID, it will be - /// aborted. - /// - /// If the SP is currently receiving an update with a different ID, the - /// abort request will fail. - /// - /// If the SP is not currently receiving any update, the request to abort - /// should succeed but will not have actually done anything. - pub id: Uuid, -} + Ok(HttpResponseOk(RotCmpa { base64_data })) + } -/// Reset an SP component (possibly the SP itself). -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/reset", -}] -async fn sp_component_reset( - rqctx: RequestContext>, - path: Path, -) -> Result { - let apictx = rqctx.context(); - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - sp.reset_component_prepare(component) - // We always want to run with the watchdog when resetting as - // disabling the watchdog should be considered a debug only feature - .and_then(|()| sp.reset_component_trigger(component, false)) - .await + async fn sp_rot_cfpa_get( + rqctx: RequestContext>, + path: Path, + params: TypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + + let PathSpComponent { sp, component } = path.into_inner(); + let GetCfpaParams { slot } = params.into_inner(); + let sp_id = sp.into(); + + // Ensure the caller knows they're asking for the RoT + if component_from_str(&component)? != SpComponent::ROT { + return Err(HttpError::for_bad_request( + Some("RequestUnsupportedForComponent".to_string()), + "Only the RoT has a CFPA".into(), + )); + } + + let sp = apictx.mgmt_switch.sp(sp_id)?; + let data = match slot { + RotCfpaSlot::Active => sp.read_rot_active_cfpa().await, + RotCfpaSlot::Inactive => sp.read_rot_inactive_cfpa().await, + RotCfpaSlot::Scratch => sp.read_rot_scratch_cfpa().await, + } .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err, })?; - Ok(HttpResponseUpdatedNoContent {}) -} - -/// Update an SP component -/// -/// Update a component of an SP according to its specific update mechanism. -/// This interface is generic for all component types, but resolves to a -/// mechanism specific to the given component type. This may fail for a variety -/// of reasons including the update bundle being invalid or improperly -/// specified or due to an error originating from the SP itself. -/// -/// Note that not all components may be updated; components without known -/// update mechanisms will return an error without any inspection of the -/// update bundle. -/// -/// Updating the SP itself is done via the component name `sp`. -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/update", -}] -async fn sp_component_update( - rqctx: RequestContext>, - path: Path, - query_params: Query, - body: UntypedBody, -) -> Result { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - let ComponentUpdateIdSlot { id, firmware_slot } = query_params.into_inner(); - - // TODO-performance: this makes a full copy of the uploaded data - let image = body.as_bytes().to_vec(); - - sp.start_update(component, id, firmware_slot, image) - .await - .map_err(|err| SpCommsError::UpdateFailed { sp: sp_id, err })?; - - Ok(HttpResponseUpdatedNoContent {}) -} + let base64_data = + base64::engine::general_purpose::STANDARD.encode(data); -/// Get the status of an update being applied to an SP component -/// -/// Getting the status of an update to the SP itself is done via the component -/// name `sp`. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/update-status", -}] -async fn sp_component_update_status( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - let status = sp.update_status(component).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(status.into())) -} - -/// Abort any in-progress update an SP component -/// -/// Aborting an update to the SP itself is done via the component name `sp`. -/// -/// On a successful return, the update corresponding to the given UUID will no -/// longer be in progress (either aborted or applied). -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/component/{component}/update-abort", -}] -async fn sp_component_update_abort( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let component = component_from_str(&component)?; - - let UpdateAbortBody { id } = body.into_inner(); - sp.update_abort(component, id).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseUpdatedNoContent {}) -} - -/// Read the CMPA from a root of trust. -/// -/// This endpoint is only valid for the `rot` component. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/cmpa", -}] -async fn sp_rot_cmpa_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let sp_id = sp.into(); - - // Ensure the caller knows they're asking for the RoT - if component_from_str(&component)? != SpComponent::ROT { - return Err(HttpError::for_bad_request( - Some("RequestUnsupportedForComponent".to_string()), - "Only the RoT has a CFPA".into(), - )); + Ok(HttpResponseOk(RotCfpa { base64_data, slot })) } - let sp = apictx.mgmt_switch.sp(sp_id)?; - let data = sp.read_rot_cmpa().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; + async fn sp_rot_boot_info( + rqctx: RequestContext>, + path: Path, + params: TypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + + let PathSpComponent { sp, component } = path.into_inner(); + let GetRotBootInfoParams { version } = params.into_inner(); + let sp_id = sp.into(); + + // Ensure the caller knows they're asking for the RoT + if component_from_str(&component)? != SpComponent::ROT { + return Err(HttpError::for_bad_request( + Some("RequestUnsupportedForComponent".to_string()), + "rot_boot_info only makes sent for a RoT".into(), + )); + } + + let sp = apictx.mgmt_switch.sp(sp_id)?; + let state = sp.rot_state(version).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; - let base64_data = base64::engine::general_purpose::STANDARD.encode(data); + Ok(HttpResponseOk(state.into())) + } - Ok(HttpResponseOk(RotCmpa { base64_data })) -} + async fn ignition_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let apictx = rqctx.context(); + let mgmt_switch = &apictx.mgmt_switch; + + let out = mgmt_switch + .bulk_ignition_state() + .await? + .map(|(id, state)| SpIgnitionInfo { + id: id.into(), + details: state.into(), + }) + .collect(); + + Ok(HttpResponseOk(out)) + } -/// Read the requested CFPA slot from a root of trust. -/// -/// This endpoint is only valid for the `rot` component. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/cfpa", -}] -async fn sp_rot_cfpa_get( - rqctx: RequestContext>, - path: Path, - params: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let GetCfpaParams { slot } = params.into_inner(); - let sp_id = sp.into(); - - // Ensure the caller knows they're asking for the RoT - if component_from_str(&component)? != SpComponent::ROT { - return Err(HttpError::for_bad_request( - Some("RequestUnsupportedForComponent".to_string()), - "Only the RoT has a CFPA".into(), - )); + async fn ignition_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let mgmt_switch = &apictx.mgmt_switch; + + let sp_id = path.into_inner().sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; + + let state = mgmt_switch + .ignition_controller() + .ignition_state(ignition_target) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + + let info = SpIgnitionInfo { id: sp_id.into(), details: state.into() }; + Ok(HttpResponseOk(info)) } - let sp = apictx.mgmt_switch.sp(sp_id)?; - let data = match slot { - RotCfpaSlot::Active => sp.read_rot_active_cfpa().await, - RotCfpaSlot::Inactive => sp.read_rot_inactive_cfpa().await, - RotCfpaSlot::Scratch => sp.read_rot_scratch_cfpa().await, + async fn ignition_command( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let apictx = rqctx.context(); + let mgmt_switch = &apictx.mgmt_switch; + let PathSpIgnitionCommand { sp, command } = path.into_inner(); + let sp_id = sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; + + mgmt_switch + .ignition_controller() + .ignition_command(ignition_target, command.into()) + .await + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; + + Ok(HttpResponseUpdatedNoContent {}) } - .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; - let base64_data = base64::engine::general_purpose::STANDARD.encode(data); + async fn sp_power_state_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; - Ok(HttpResponseOk(RotCfpa { base64_data, slot })) -} + let power_state = sp.power_state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; -/// Read the RoT boot state from a root of trust -/// -/// This endpoint is only valid for the `rot` component. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/component/{component}/rot-boot-info", -}] -async fn sp_rot_boot_info( - rqctx: RequestContext>, - path: Path, - params: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - let PathSpComponent { sp, component } = path.into_inner(); - let GetRotBootInfoParams { version } = params.into_inner(); - let sp_id = sp.into(); - - // Ensure the caller knows they're asking for the RoT - if component_from_str(&component)? != SpComponent::ROT { - return Err(HttpError::for_bad_request( - Some("RequestUnsupportedForComponent".to_string()), - "rot_boot_info only makes sent for a RoT".into(), - )); + Ok(HttpResponseOk(power_state.into())) } - let sp = apictx.mgmt_switch.sp(sp_id)?; - let state = sp.rot_state(version).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; + async fn sp_power_state_set( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let power_state = body.into_inner(); + + sp.set_power_state(power_state.into()).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; - Ok(HttpResponseOk(state.into())) -} + Ok(HttpResponseUpdatedNoContent {}) + } -/// List SPs via Ignition -/// -/// Retreive information for all SPs via the Ignition controller. This is lower -/// latency and has fewer possible failure modes than querying the SP over the -/// management network. -#[endpoint { - method = GET, - path = "/ignition", -}] -async fn ignition_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let apictx = rqctx.context(); - let mgmt_switch = &apictx.mgmt_switch; - - let out = mgmt_switch - .bulk_ignition_state() - .await? - .map(|(id, state)| SpIgnitionInfo { - id: id.into(), - details: state.into(), - }) - .collect(); + async fn sp_installinator_image_id_set( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + use ipcc::Key; - Ok(HttpResponseOk(out)) -} + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + + let image_id = ipcc::InstallinatorImageId::from(body.into_inner()); -/// Get SP info via Ignition -/// -/// Retreive information for an SP via the Ignition controller. This is lower -/// latency and has fewer possible failure modes than querying the SP over the -/// management network. -#[endpoint { - method = GET, - path = "/ignition/{type}/{slot}", -}] -async fn ignition_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let mgmt_switch = &apictx.mgmt_switch; - - let sp_id = path.into_inner().sp.into(); - let ignition_target = mgmt_switch.ignition_target(sp_id)?; - - let state = mgmt_switch - .ignition_controller() - .ignition_state(ignition_target) + sp.set_ipcc_key_lookup_value( + Key::InstallinatorImageId as u8, + image_id.serialize(), + ) .await .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err, })?; - let info = SpIgnitionInfo { id: sp_id.into(), details: state.into() }; - Ok(HttpResponseOk(info)) -} + Ok(HttpResponseUpdatedNoContent {}) + } -/// Send an ignition command targeting a specific SP. -/// -/// This endpoint can be used to transition a target between A2 and A3 (via -/// power-on / power-off) or reset it. -/// -/// The management network traffic caused by requests to this endpoint is -/// between this MGS instance and its local ignition controller, _not_ the SP -/// targeted by the command. -#[endpoint { - method = POST, - path = "/ignition/{type}/{slot}/{command}", -}] -async fn ignition_command( - rqctx: RequestContext>, - path: Path, -) -> Result { - let apictx = rqctx.context(); - let mgmt_switch = &apictx.mgmt_switch; - let PathSpIgnitionCommand { sp, command } = path.into_inner(); - let sp_id = sp.into(); - let ignition_target = mgmt_switch.ignition_target(sp_id)?; - - mgmt_switch - .ignition_controller() - .ignition_command(ignition_target, command.into()) + async fn sp_installinator_image_id_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + use ipcc::Key; + + let apictx = rqctx.context(); + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + + // We clear the image ID by setting it to a 0-length vec. + sp.set_ipcc_key_lookup_value( + Key::InstallinatorImageId as u8, + Vec::new(), + ) .await .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err, })?; - Ok(HttpResponseUpdatedNoContent {}) -} + Ok(HttpResponseUpdatedNoContent {}) + } -/// Get the current power state of a sled via its SP. -/// -/// Note that if the sled is in A3, the SP is powered off and will not be able -/// to respond; use the ignition control endpoints for those cases. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/power-state", -}] -async fn sp_power_state_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - - let power_state = sp.power_state().await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseOk(power_state.into())) -} + async fn sp_host_phase2_progress_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + + let Some(progress) = sp.most_recent_host_phase2_request().await else { + return Ok(HttpResponseOk(HostPhase2Progress::None)); + }; + + // Our `host_phase2_provider` is using an in-memory cache, so the only way + // we can fail to get the total size is if we no longer have the image that + // this SP most recently requested. We'll treat that as "no progress + // information", since it almost certainly means our progress info on this + // SP is very stale. + let Ok(total_size) = + apictx.host_phase2_provider.total_size(progress.hash).await + else { + return Ok(HttpResponseOk(HostPhase2Progress::None)); + }; + + let image_id = HostPhase2RecoveryImageId { + sha256_hash: ArtifactHash(progress.hash), + }; + + // `progress` tells us the offset the SP requested and the amount of data we + // sent starting at that offset; report the end of that chunk to our caller. + let offset = progress.offset.saturating_add(progress.data_sent); + + Ok(HttpResponseOk(HostPhase2Progress::Available { + image_id, + offset, + total_size, + age: progress.received.elapsed(), + })) + } -/// Set the current power state of a sled via its SP. -/// -/// Note that if the sled is in A3, the SP is powered off and will not be able -/// to respond; use the ignition control endpoints for those cases. -#[endpoint { - method = POST, - path = "/sp/{type}/{slot}/power-state", -}] -async fn sp_power_state_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - let power_state = body.into_inner(); - - sp.set_power_state(power_state.into()).await.map_err(|err| { - SpCommsError::SpCommunicationFailed { sp: sp_id, err } - })?; - - Ok(HttpResponseUpdatedNoContent {}) -} + async fn sp_host_phase2_progress_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let apictx = rqctx.context(); + let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; -/// Set the installinator image ID the sled should use for recovery. -/// -/// This value can be read by the host via IPCC; see the `ipcc` crate. -#[endpoint { - method = PUT, - path = "/sp/{type}/{slot}/ipcc/installinator-image-id", -}] -async fn sp_installinator_image_id_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - use ipcc::Key; - - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - - let image_id = ipcc::InstallinatorImageId::from(body.into_inner()); - - sp.set_ipcc_key_lookup_value( - Key::InstallinatorImageId as u8, - image_id.serialize(), - ) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; - - Ok(HttpResponseUpdatedNoContent {}) -} + sp.clear_most_recent_host_phase2_request().await; -/// Clear any previously-set installinator image ID on the target sled. -#[endpoint { - method = DELETE, - path = "/sp/{type}/{slot}/ipcc/installinator-image-id", -}] -async fn sp_installinator_image_id_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - use ipcc::Key; - - let apictx = rqctx.context(); - let sp_id = path.into_inner().sp.into(); - let sp = apictx.mgmt_switch.sp(sp_id)?; - - // We clear the image ID by setting it to a 0-length vec. - sp.set_ipcc_key_lookup_value(Key::InstallinatorImageId as u8, Vec::new()) - .await - .map_err(|err| SpCommsError::SpCommunicationFailed { - sp: sp_id, - err, - })?; + Ok(HttpResponseUpdatedNoContent {}) + } - Ok(HttpResponseUpdatedNoContent {}) -} + async fn recovery_host_phase2_upload( + rqctx: RequestContext>, + body: UntypedBody, + ) -> Result, HttpError> { + let apictx = rqctx.context(); + + // TODO: this makes a full copy of the host image, potentially unnecessarily + // if it's malformed. + let image = body.as_bytes().to_vec(); + + let sha256_hash = + apictx.host_phase2_provider.insert(image).await.map_err(|err| { + // Any cache-insertion failure indicates a malformed image; map them + // to bad requests. + HttpError::for_bad_request( + Some("BadHostPhase2Image".to_string()), + err.to_string(), + ) + })?; + let sha256_hash = ArtifactHash(sha256_hash); + + Ok(HttpResponseOk(HostPhase2RecoveryImageId { sha256_hash })) + } -/// Get the most recent host phase2 request we've seen from the target SP. -/// -/// This method can be used as an indirect progress report for how far along a -/// host is when it is booting via the MGS -> SP -> UART recovery path. This -/// path is used to install the trampoline image containing installinator to -/// recover a sled. -#[endpoint { - method = GET, - path = "/sp/{type}/{slot}/host-phase2-progress", -}] -async fn sp_host_phase2_progress_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; - - let Some(progress) = sp.most_recent_host_phase2_request().await else { - return Ok(HttpResponseOk(HostPhase2Progress::None)); - }; - - // Our `host_phase2_provider` is using an in-memory cache, so the only way - // we can fail to get the total size is if we no longer have the image that - // this SP most recently requested. We'll treat that as "no progress - // information", since it almost certainly means our progress info on this - // SP is very stale. - let Ok(total_size) = - apictx.host_phase2_provider.total_size(progress.hash).await - else { - return Ok(HttpResponseOk(HostPhase2Progress::None)); - }; - - let image_id = - HostPhase2RecoveryImageId { sha256_hash: ArtifactHash(progress.hash) }; - - // `progress` tells us the offset the SP requested and the amount of data we - // sent starting at that offset; report the end of that chunk to our caller. - let offset = progress.offset.saturating_add(progress.data_sent); - - Ok(HttpResponseOk(HostPhase2Progress::Available { - image_id, - offset, - total_size, - age: progress.received.elapsed(), - })) -} + async fn sp_local_switch_id( + rqctx: RequestContext>, + ) -> Result, HttpError> { + let apictx = rqctx.context(); -/// Clear the most recent host phase2 request we've seen from the target SP. -#[endpoint { - method = DELETE, - path = "/sp/{type}/{slot}/host-phase2-progress", -}] -async fn sp_host_phase2_progress_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; - - sp.clear_most_recent_host_phase2_request().await; - - Ok(HttpResponseUpdatedNoContent {}) -} + let id = apictx.mgmt_switch.local_switch()?; -/// Upload a host phase2 image that can be served to recovering hosts via the -/// host/SP control uart. -/// -/// MGS caches this image in memory and is limited to a small, fixed number of -/// images (potentially 1). Uploading a new image may evict the -/// least-recently-requested image if our cache is already full. -#[endpoint { - method = POST, - path = "/recovery/host-phase2", -}] -async fn recovery_host_phase2_upload( - rqctx: RequestContext>, - body: UntypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - // TODO: this makes a full copy of the host image, potentially unnecessarily - // if it's malformed. - let image = body.as_bytes().to_vec(); - - let sha256_hash = - apictx.host_phase2_provider.insert(image).await.map_err(|err| { - // Any cache-insertion failure indicates a malformed image; map them - // to bad requests. - HttpError::for_bad_request( - Some("BadHostPhase2Image".to_string()), - err.to_string(), - ) - })?; - let sha256_hash = ArtifactHash(sha256_hash); + Ok(HttpResponseOk(id.into())) + } - Ok(HttpResponseOk(HostPhase2RecoveryImageId { sha256_hash })) -} + async fn sp_all_ids( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let apictx = rqctx.context(); -/// Get the identifier for the switch this MGS instance is connected to. -/// -/// Note that most MGS endpoints behave identically regardless of which scrimlet -/// the MGS instance is running on; this one, however, is intentionally -/// different. This endpoint is _probably_ only useful for clients communicating -/// with MGS over localhost (i.e., other services in the switch zone) who need -/// to know which sidecar they are connected to. -#[endpoint { - method = GET, - path = "/local/switch-id", -}] -async fn sp_local_switch_id( - rqctx: RequestContext>, -) -> Result, HttpError> { - let apictx = rqctx.context(); - - let id = apictx.mgmt_switch.local_switch()?; - - Ok(HttpResponseOk(id.into())) -} + let all_ids = + apictx.mgmt_switch.all_sps()?.map(|(id, _)| id.into()).collect(); -/// Get the complete list of SP identifiers this MGS instance is configured to -/// find and communicate with. -/// -/// Note that unlike most MGS endpoints, this endpoint does not send any -/// communication on the management network. -#[endpoint { - method = GET, - path = "/local/all-sp-ids", -}] -async fn sp_all_ids( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let apictx = rqctx.context(); - - let all_ids = - apictx.mgmt_switch.all_sps()?.map(|(id, _)| id.into()).collect(); - - Ok(HttpResponseOk(all_ids)) + Ok(HttpResponseOk(all_ids)) + } } -// TODO -// The gateway service will get asynchronous notifications both from directly -// SPs over the management network and indirectly from Ignition via the Sidecar -// SP. -// TODO The Ignition controller will send an interrupt to its local SP. Will -// that SP then notify both gateway services or just its local gateway service? -// Both Ignition controller should both do the same thing at about the same -// time so is there a real benefit to them both sending messages to both -// gateways? This would cause a single message to effectively be replicated 4x -// (Nexus would need to dedup these). +// wrap `SpComponent::try_from(&str)` into a usable form for dropshot endpoints +fn component_from_str(s: &str) -> Result { + SpComponent::try_from(s).map_err(|_| { + HttpError::for_bad_request( + Some("InvalidSpComponent".to_string()), + "invalid SP component name".to_string(), + ) + }) +} -type GatewayApiDescription = ApiDescription>; +// The _from_comms functions are here rather than `From` impls in gateway-types +// so that gateway-types avoids a dependency on gateway-sp-comms. + +fn sp_state_from_comms( + sp_state: VersionedSpState, + rot_state: Result, +) -> SpState { + // We need to keep this backwards compatible. If we get an error from reading `rot_state` + // it could be because the RoT/SP isn't updated or because we have failed for some + // other reason. If we're on V1/V2 SP info and we fail, just fall back to using the + // RoT info in that struct since any error will also be communicated there. + match (sp_state, rot_state) { + (VersionedSpState::V1(s), Err(_)) => SpState::from(s), + (VersionedSpState::V1(s), Ok(r)) => { + SpState::from((s, RotState::from(r))) + } + (VersionedSpState::V2(s), Err(_)) => SpState::from(s), + (VersionedSpState::V2(s), Ok(r)) => { + SpState::from((s, RotState::from(r))) + } + (VersionedSpState::V3(s), Ok(r)) => { + SpState::from((s, RotState::from(r))) + } + (VersionedSpState::V3(s), Err(err)) => SpState::from(( + s, + RotState::CommunicationFailed { message: err.to_string() }, + )), + } +} -/// Returns a description of the gateway API -pub fn api() -> GatewayApiDescription { - fn register_endpoints( - api: &mut GatewayApiDescription, - ) -> Result<(), ApiDescriptionRegisterError> { - api.register(sp_get)?; - api.register(sp_startup_options_get)?; - api.register(sp_startup_options_set)?; - api.register(sp_component_reset)?; - api.register(sp_power_state_get)?; - api.register(sp_power_state_set)?; - api.register(sp_installinator_image_id_set)?; - api.register(sp_installinator_image_id_delete)?; - api.register(sp_sensor_read_value)?; - api.register(sp_component_list)?; - api.register(sp_component_get)?; - api.register(sp_component_caboose_get)?; - api.register(sp_component_clear_status)?; - api.register(sp_component_active_slot_get)?; - api.register(sp_component_active_slot_set)?; - api.register(sp_component_serial_console_attach)?; - api.register(sp_component_serial_console_detach)?; - api.register(sp_component_update)?; - api.register(sp_component_update_status)?; - api.register(sp_component_update_abort)?; - api.register(sp_rot_cmpa_get)?; - api.register(sp_rot_cfpa_get)?; - api.register(sp_rot_boot_info)?; - api.register(sp_host_phase2_progress_get)?; - api.register(sp_host_phase2_progress_delete)?; - api.register(ignition_list)?; - api.register(ignition_get)?; - api.register(ignition_command)?; - api.register(recovery_host_phase2_upload)?; - api.register(sp_local_switch_id)?; - api.register(sp_all_ids)?; - Ok(()) +fn sp_component_list_from_comms( + inv: gateway_sp_comms::SpInventory, +) -> SpComponentList { + SpComponentList { + components: inv + .devices + .into_iter() + .map(sp_component_info_from_comms) + .collect(), } +} - let mut api = GatewayApiDescription::new(); - if let Err(err) = register_endpoints(&mut api) { - panic!("failed to register entrypoints: {}", err); +fn sp_component_info_from_comms( + dev: gateway_sp_comms::SpDevice, +) -> SpComponentInfo { + SpComponentInfo { + component: dev.component.as_str().unwrap_or("???").to_string(), + device: dev.device, + serial_number: None, // TODO populate when SP provides it + description: dev.description, + capabilities: dev.capabilities.bits(), + presence: dev.presence.into(), } - api } diff --git a/gateway/src/http_entrypoints/conversions.rs b/gateway/src/http_entrypoints/conversions.rs deleted file mode 100644 index c7fcb29922..0000000000 --- a/gateway/src/http_entrypoints/conversions.rs +++ /dev/null @@ -1,596 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -// Copyright 2022 Oxide Computer Company - -//! Conversions between externally-defined types and HTTP / JsonSchema types. - -use super::HostStartupOptions; -use super::IgnitionCommand; -use super::ImageVersion; -use super::InstallinatorImageId; -use super::PowerState; -use super::RotImageDetails; -use super::RotImageError; -use super::RotSlot; -use super::RotState; -use super::SpComponentInfo; -use super::SpComponentList; -use super::SpComponentPresence; -use super::SpIdentifier; -use super::SpIgnition; -use super::SpIgnitionSystemType; -use super::SpSensorReading; -use super::SpSensorReadingResult; -use super::SpState; -use super::SpType; -use super::SpUpdateStatus; -use super::UpdatePreparationProgress; -use dropshot::HttpError; -use gateway_messages::RotBootInfo; -use gateway_messages::SpComponent; -use gateway_messages::StartupOptions; -use gateway_messages::UpdateStatus; -use std::str; - -// wrap `SpComponent::try_from(&str)` into a usable form for dropshot endpoints -pub(super) fn component_from_str(s: &str) -> Result { - SpComponent::try_from(s).map_err(|_| { - HttpError::for_bad_request( - Some("InvalidSpComponent".to_string()), - "invalid SP component name".to_string(), - ) - }) -} - -impl From for SpSensorReading { - fn from(value: gateway_messages::SensorReading) -> Self { - Self { - timestamp: value.timestamp, - result: match value.value { - Ok(value) => SpSensorReadingResult::Success { value }, - Err(err) => err.into(), - }, - } - } -} - -impl From for SpSensorReadingResult { - fn from(value: gateway_messages::SensorDataMissing) -> Self { - use gateway_messages::SensorDataMissing; - match value { - SensorDataMissing::DeviceOff => Self::DeviceOff, - SensorDataMissing::DeviceError => Self::DeviceError, - SensorDataMissing::DeviceNotPresent => Self::DeviceNotPresent, - SensorDataMissing::DeviceUnavailable => Self::DeviceUnavailable, - SensorDataMissing::DeviceTimeout => Self::DeviceTimeout, - } - } -} - -impl From for SpUpdateStatus { - fn from(status: UpdateStatus) -> Self { - match status { - UpdateStatus::None => Self::None, - UpdateStatus::Preparing(status) => Self::Preparing { - id: status.id.into(), - progress: status.progress.map(Into::into), - }, - UpdateStatus::SpUpdateAuxFlashChckScan { - id, total_size, .. - } => Self::InProgress { - id: id.into(), - bytes_received: 0, - total_bytes: total_size, - }, - UpdateStatus::InProgress(status) => Self::InProgress { - id: status.id.into(), - bytes_received: status.bytes_received, - total_bytes: status.total_size, - }, - UpdateStatus::Complete(id) => Self::Complete { id: id.into() }, - UpdateStatus::Aborted(id) => Self::Aborted { id: id.into() }, - UpdateStatus::Failed { id, code } => { - Self::Failed { id: id.into(), code } - } - UpdateStatus::RotError { id, error } => { - Self::RotError { id: id.into(), message: format!("{error:?}") } - } - } - } -} - -impl From - for UpdatePreparationProgress -{ - fn from(progress: gateway_messages::UpdatePreparationProgress) -> Self { - Self { current: progress.current, total: progress.total } - } -} - -impl From for PowerState { - fn from(power_state: gateway_messages::PowerState) -> Self { - match power_state { - gateway_messages::PowerState::A0 => Self::A0, - gateway_messages::PowerState::A1 => Self::A1, - gateway_messages::PowerState::A2 => Self::A2, - } - } -} - -impl From for gateway_messages::PowerState { - fn from(power_state: PowerState) -> gateway_messages::PowerState { - match power_state { - PowerState::A0 => gateway_messages::PowerState::A0, - PowerState::A1 => gateway_messages::PowerState::A1, - PowerState::A2 => gateway_messages::PowerState::A2, - } - } -} - -impl From for ImageVersion { - fn from(v: gateway_messages::ImageVersion) -> Self { - Self { epoch: v.epoch, version: v.version } - } -} - -impl From for RotImageError { - fn from(error: gateway_messages::ImageError) -> Self { - match error { - gateway_messages::ImageError::Unchecked => RotImageError::Unchecked, - gateway_messages::ImageError::FirstPageErased => { - RotImageError::FirstPageErased - } - gateway_messages::ImageError::PartiallyProgrammed => { - RotImageError::PartiallyProgrammed - } - gateway_messages::ImageError::InvalidLength => { - RotImageError::InvalidLength - } - gateway_messages::ImageError::HeaderNotProgrammed => { - RotImageError::HeaderNotProgrammed - } - gateway_messages::ImageError::BootloaderTooSmall => { - RotImageError::BootloaderTooSmall - } - gateway_messages::ImageError::BadMagic => RotImageError::BadMagic, - gateway_messages::ImageError::HeaderImageSize => { - RotImageError::HeaderImageSize - } - gateway_messages::ImageError::UnalignedLength => { - RotImageError::UnalignedLength - } - gateway_messages::ImageError::UnsupportedType => { - RotImageError::UnsupportedType - } - gateway_messages::ImageError::ResetVectorNotThumb2 => { - RotImageError::ResetVectorNotThumb2 - } - gateway_messages::ImageError::ResetVector => { - RotImageError::ResetVector - } - gateway_messages::ImageError::Signature => RotImageError::Signature, - } - } -} -// We expect serial and model numbers to be ASCII and 0-padded: find the first 0 -// byte and convert to a string. If that fails, hexlify the entire slice. -fn stringify_byte_string(bytes: &[u8]) -> String { - let first_zero = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - - str::from_utf8(&bytes[..first_zero]) - .map(|s| s.to_string()) - .unwrap_or_else(|_err| hex::encode(bytes)) -} - -impl From<(gateway_messages::SpStateV1, RotState)> for SpState { - fn from(all: (gateway_messages::SpStateV1, RotState)) -> Self { - let (state, rot) = all; - Self { - serial_number: stringify_byte_string(&state.serial_number), - model: stringify_byte_string(&state.model), - revision: state.revision, - hubris_archive_id: hex::encode(state.hubris_archive_id), - base_mac_address: state.base_mac_address, - power_state: PowerState::from(state.power_state), - rot, - } - } -} - -impl From for SpState { - fn from(state: gateway_messages::SpStateV1) -> Self { - Self::from((state, RotState::from(state.rot))) - } -} - -impl From<(gateway_messages::SpStateV2, RotState)> for SpState { - fn from(all: (gateway_messages::SpStateV2, RotState)) -> Self { - let (state, rot) = all; - Self { - serial_number: stringify_byte_string(&state.serial_number), - model: stringify_byte_string(&state.model), - revision: state.revision, - hubris_archive_id: hex::encode(state.hubris_archive_id), - base_mac_address: state.base_mac_address, - power_state: PowerState::from(state.power_state), - rot, - } - } -} - -impl From for SpState { - fn from(state: gateway_messages::SpStateV2) -> Self { - Self::from((state, RotState::from(state.rot))) - } -} - -impl From<(gateway_messages::SpStateV3, RotState)> for SpState { - fn from(all: (gateway_messages::SpStateV3, RotState)) -> Self { - let (state, rot) = all; - Self { - serial_number: stringify_byte_string(&state.serial_number), - model: stringify_byte_string(&state.model), - revision: state.revision, - hubris_archive_id: hex::encode(state.hubris_archive_id), - base_mac_address: state.base_mac_address, - power_state: PowerState::from(state.power_state), - rot, - } - } -} - -impl - From<( - gateway_sp_comms::VersionedSpState, - Result< - gateway_messages::RotBootInfo, - gateway_sp_comms::error::CommunicationError, - >, - )> for SpState -{ - fn from( - all: ( - gateway_sp_comms::VersionedSpState, - Result< - gateway_messages::RotBootInfo, - gateway_sp_comms::error::CommunicationError, - >, - ), - ) -> Self { - // We need to keep this backwards compatible. If we get an error from reading `rot_state` - // it could be because the RoT/SP isn't updated or because we have failed for some - // other reason. If we're on V1/V2 SP info and we fail, just fall back to using the - // RoT info in that struct since any error will also be communicated there. - match (all.0, all.1) { - (gateway_sp_comms::VersionedSpState::V1(s), Err(_)) => { - Self::from(s) - } - (gateway_sp_comms::VersionedSpState::V1(s), Ok(r)) => { - Self::from((s, RotState::from(r))) - } - (gateway_sp_comms::VersionedSpState::V2(s), Err(_)) => { - Self::from(s) - } - (gateway_sp_comms::VersionedSpState::V2(s), Ok(r)) => { - Self::from((s, RotState::from(r))) - } - (gateway_sp_comms::VersionedSpState::V3(s), Ok(r)) => { - Self::from((s, RotState::from(r))) - } - (gateway_sp_comms::VersionedSpState::V3(s), Err(err)) => { - Self::from(( - s, - RotState::CommunicationFailed { message: err.to_string() }, - )) - } - } - } -} - -impl From> - for RotState -{ - fn from( - result: Result, - ) -> Self { - match result { - Ok(state) => Self::from(state), - Err(err) => Self::CommunicationFailed { message: err.to_string() }, - } - } -} - -impl From> - for RotState -{ - fn from( - result: Result< - gateway_messages::RotStateV2, - gateway_messages::RotError, - >, - ) -> Self { - match result { - Ok(state) => Self::from(state), - Err(err) => Self::CommunicationFailed { message: err.to_string() }, - } - } -} - -impl RotState { - fn fwid_to_string(fwid: gateway_messages::Fwid) -> String { - match fwid { - gateway_messages::Fwid::Sha3_256(digest) => hex::encode(digest), - } - } -} - -impl From for RotState { - fn from(state: gateway_messages::RotStateV3) -> Self { - Self::V3 { - active: state.active.into(), - persistent_boot_preference: state.persistent_boot_preference.into(), - pending_persistent_boot_preference: state - .pending_persistent_boot_preference - .map(Into::into), - transient_boot_preference: state - .transient_boot_preference - .map(Into::into), - slot_a_fwid: Self::fwid_to_string(state.slot_a_fwid), - slot_b_fwid: Self::fwid_to_string(state.slot_b_fwid), - - stage0_fwid: Self::fwid_to_string(state.stage0_fwid), - stage0next_fwid: Self::fwid_to_string(state.stage0next_fwid), - - slot_a_error: state.slot_a_status.err().map(From::from), - slot_b_error: state.slot_b_status.err().map(From::from), - - stage0_error: state.stage0_status.err().map(From::from), - stage0next_error: state.stage0next_status.err().map(From::from), - } - } -} - -impl From for RotState { - fn from(state: gateway_messages::RotStateV2) -> Self { - Self::V2 { - active: state.active.into(), - persistent_boot_preference: state.persistent_boot_preference.into(), - pending_persistent_boot_preference: state - .pending_persistent_boot_preference - .map(Into::into), - transient_boot_preference: state - .transient_boot_preference - .map(Into::into), - slot_a_sha3_256_digest: state - .slot_a_sha3_256_digest - .map(hex::encode), - slot_b_sha3_256_digest: state - .slot_b_sha3_256_digest - .map(hex::encode), - } - } -} - -impl From for RotState { - fn from(state: gateway_messages::RotState) -> Self { - let boot_state = state.rot_updates.boot_state; - Self::V2 { - active: boot_state.active.into(), - slot_a_sha3_256_digest: boot_state - .slot_a - .map(|details| hex::encode(details.digest)), - slot_b_sha3_256_digest: boot_state - .slot_b - .map(|details| hex::encode(details.digest)), - // RotState(V1) didn't have the following fields, so we make - // it up as best we can. This RoT version is pre-shipping - // and should only exist on (not updated recently) test - // systems. - persistent_boot_preference: boot_state.active.into(), - pending_persistent_boot_preference: None, - transient_boot_preference: None, - } - } -} - -impl From for RotState { - fn from(value: gateway_messages::RotBootInfo) -> Self { - match value { - RotBootInfo::V1(s) => Self::from(s), - RotBootInfo::V2(s) => Self::from(s), - RotBootInfo::V3(s) => Self::from(s), - } - } -} - -impl From for RotSlot { - fn from(slot: gateway_messages::RotSlotId) -> Self { - match slot { - gateway_messages::RotSlotId::A => Self::A, - gateway_messages::RotSlotId::B => Self::B, - } - } -} - -impl From for RotImageDetails { - fn from(details: gateway_messages::RotImageDetails) -> Self { - Self { - digest: hex::encode(details.digest), - version: details.version.into(), - } - } -} - -impl From for SpIgnition { - fn from(state: gateway_messages::IgnitionState) -> Self { - use gateway_messages::ignition::SystemPowerState; - - if let Some(target_state) = state.target { - Self::Present { - id: target_state.system_type.into(), - power: matches!( - target_state.power_state, - SystemPowerState::On | SystemPowerState::PoweringOn - ), - ctrl_detect_0: target_state.controller0_present, - ctrl_detect_1: target_state.controller1_present, - flt_a3: target_state.faults.power_a3, - flt_a2: target_state.faults.power_a2, - flt_rot: target_state.faults.rot, - flt_sp: target_state.faults.sp, - } - } else { - Self::Absent - } - } -} - -impl From for SpIgnitionSystemType { - fn from(st: gateway_messages::ignition::SystemType) -> Self { - use gateway_messages::ignition::SystemType; - match st { - SystemType::Gimlet => Self::Gimlet, - SystemType::Sidecar => Self::Sidecar, - SystemType::Psc => Self::Psc, - SystemType::Unknown(id) => Self::Unknown { id }, - } - } -} - -impl From for gateway_messages::IgnitionCommand { - fn from(cmd: IgnitionCommand) -> Self { - match cmd { - IgnitionCommand::PowerOn => { - gateway_messages::IgnitionCommand::PowerOn - } - IgnitionCommand::PowerOff => { - gateway_messages::IgnitionCommand::PowerOff - } - IgnitionCommand::PowerReset => { - gateway_messages::IgnitionCommand::PowerReset - } - } - } -} - -impl From for crate::management_switch::SpType { - fn from(typ: SpType) -> Self { - match typ { - SpType::Sled => Self::Sled, - SpType::Power => Self::Power, - SpType::Switch => Self::Switch, - } - } -} - -impl From for SpType { - fn from(typ: crate::management_switch::SpType) -> Self { - match typ { - crate::management_switch::SpType::Sled => Self::Sled, - crate::management_switch::SpType::Power => Self::Power, - crate::management_switch::SpType::Switch => Self::Switch, - } - } -} - -impl From for crate::management_switch::SpIdentifier { - fn from(id: SpIdentifier) -> Self { - Self { - typ: id.typ.into(), - // id.slot may come from an untrusted source, but usize >= 32 bits - // on any platform that will run this code, so unwrap is fine - slot: usize::try_from(id.slot).unwrap(), - } - } -} - -impl From for SpIdentifier { - fn from(id: crate::management_switch::SpIdentifier) -> Self { - Self { - typ: id.typ.into(), - // id.slot comes from a trusted source (crate::management_switch) - // and will not exceed u32::MAX - slot: u32::try_from(id.slot).unwrap(), - } - } -} - -impl From for SpComponentPresence { - fn from(p: gateway_messages::DevicePresence) -> Self { - match p { - gateway_messages::DevicePresence::Present => Self::Present, - gateway_messages::DevicePresence::NotPresent => Self::NotPresent, - gateway_messages::DevicePresence::Failed => Self::Failed, - gateway_messages::DevicePresence::Unavailable => Self::Unavailable, - gateway_messages::DevicePresence::Timeout => Self::Timeout, - gateway_messages::DevicePresence::Error => Self::Error, - } - } -} - -impl From for SpComponentInfo { - fn from(d: gateway_sp_comms::SpDevice) -> Self { - Self { - component: d.component.as_str().unwrap_or("???").to_string(), - device: d.device, - serial_number: None, // TODO populate when SP provides it - description: d.description, - capabilities: d.capabilities.bits(), - presence: d.presence.into(), - } - } -} - -impl From for SpComponentList { - fn from(inv: gateway_sp_comms::SpInventory) -> Self { - Self { components: inv.devices.into_iter().map(Into::into).collect() } - } -} - -impl From for StartupOptions { - fn from(mgs_opt: HostStartupOptions) -> Self { - let mut opt = StartupOptions::empty(); - opt.set( - StartupOptions::PHASE2_RECOVERY_MODE, - mgs_opt.phase2_recovery_mode, - ); - opt.set(StartupOptions::STARTUP_KBM, mgs_opt.kbm); - opt.set(StartupOptions::STARTUP_BOOTRD, mgs_opt.bootrd); - opt.set(StartupOptions::STARTUP_PROM, mgs_opt.prom); - opt.set(StartupOptions::STARTUP_KMDB, mgs_opt.kmdb); - opt.set(StartupOptions::STARTUP_KMDB_BOOT, mgs_opt.kmdb_boot); - opt.set(StartupOptions::STARTUP_BOOT_RAMDISK, mgs_opt.boot_ramdisk); - opt.set(StartupOptions::STARTUP_BOOT_NET, mgs_opt.boot_net); - opt.set(StartupOptions::STARTUP_VERBOSE, mgs_opt.verbose); - opt - } -} - -impl From for HostStartupOptions { - fn from(opt: StartupOptions) -> Self { - Self { - phase2_recovery_mode: opt - .contains(StartupOptions::PHASE2_RECOVERY_MODE), - kbm: opt.contains(StartupOptions::STARTUP_KBM), - bootrd: opt.contains(StartupOptions::STARTUP_BOOTRD), - prom: opt.contains(StartupOptions::STARTUP_PROM), - kmdb: opt.contains(StartupOptions::STARTUP_KMDB), - kmdb_boot: opt.contains(StartupOptions::STARTUP_KMDB_BOOT), - boot_ramdisk: opt.contains(StartupOptions::STARTUP_BOOT_RAMDISK), - boot_net: opt.contains(StartupOptions::STARTUP_BOOT_NET), - verbose: opt.contains(StartupOptions::STARTUP_VERBOSE), - } - } -} - -impl From for ipcc::InstallinatorImageId { - fn from(id: InstallinatorImageId) -> Self { - Self { - update_id: id.update_id, - host_phase_2: id.host_phase_2, - control_plane: id.control_plane, - } - } -} diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index be8c84d7db..e1eed05334 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -44,18 +44,6 @@ use std::net::SocketAddrV6; use std::sync::Arc; use uuid::Uuid; -/// Run the OpenAPI generator for the API; which emits the OpenAPI spec -/// to stdout. -pub fn run_openapi() -> Result<(), String> { - http_entrypoints::api() - .openapi("Oxide Management Gateway Service API", "0.0.1") - .description("API for interacting with the Oxide control plane's gateway service") - .contact_url("https://oxide.computer") - .contact_email("api@oxide.computer") - .write(&mut std::io::stdout()) - .map_err(|e| e.to_string()) -} - pub struct MgsArguments { pub id: Uuid, pub addresses: Vec, diff --git a/gateway/src/management_switch.rs b/gateway/src/management_switch.rs index 0571dc051e..a93c44d62c 100644 --- a/gateway/src/management_switch.rs +++ b/gateway/src/management_switch.rs @@ -69,6 +69,28 @@ impl SpIdentifier { } } +impl From for SpIdentifier { + fn from(id: gateway_types::component::SpIdentifier) -> Self { + Self { + typ: id.typ.into(), + // id.slot may come from an untrusted source, but usize >= 32 bits + // on any platform that will run this code, so unwrap is fine + slot: usize::try_from(id.slot).unwrap(), + } + } +} + +impl From for gateway_types::component::SpIdentifier { + fn from(id: SpIdentifier) -> Self { + Self { + typ: id.typ.into(), + // id.slot comes from a trusted source (crate::management_switch) + // and will not exceed u32::MAX + slot: u32::try_from(id.slot).unwrap(), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum SpType { @@ -77,6 +99,26 @@ pub enum SpType { Power, } +impl From for SpType { + fn from(typ: gateway_types::component::SpType) -> Self { + match typ { + gateway_types::component::SpType::Sled => Self::Sled, + gateway_types::component::SpType::Power => Self::Power, + gateway_types::component::SpType::Switch => Self::Switch, + } + } +} + +impl From for gateway_types::component::SpType { + fn from(typ: SpType) -> Self { + match typ { + SpType::Sled => Self::Sled, + SpType::Power => Self::Power, + SpType::Switch => Self::Switch, + } + } +} + // We derive `Serialize` to be able to send `SwitchPort`s to usdt probes, but // critically we do _not_ implement `Deserialize` - the only way to construct a // `SwitchPort` should be to receive one from a `ManagementSwitch`. diff --git a/gateway/tests/integration_tests/commands.rs b/gateway/tests/integration_tests/commands.rs deleted file mode 100644 index 5b1b32199c..0000000000 --- a/gateway/tests/integration_tests/commands.rs +++ /dev/null @@ -1,39 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use std::path::PathBuf; - -use expectorate::assert_contents; -use omicron_test_utils::dev::test_cmds::{ - assert_exit_code, path_to_executable, run_command, EXIT_SUCCESS, -}; -use openapiv3::OpenAPI; -use subprocess::Exec; - -// name of mgs executable -const CMD_MGS: &str = env!("CARGO_BIN_EXE_mgs"); - -fn path_to_mgs() -> PathBuf { - path_to_executable(CMD_MGS) -} - -#[test] -fn test_mgs_openapi_sled() { - let exec = Exec::cmd(path_to_mgs()).arg("openapi"); - let (exit_status, stdout_text, stderr_text) = run_command(exec); - assert_exit_code(exit_status, EXIT_SUCCESS, &stderr_text); - assert_contents("tests/output/cmd-mgs-openapi-stderr", &stderr_text); - - let spec: OpenAPI = serde_json::from_str(&stdout_text) - .expect("stdout was not valid OpenAPI"); - - // Check for lint errors. - let errors = openapi_lint::validate(&spec); - assert!(errors.is_empty(), "{}", errors.join("\n\n")); - - // Confirm that the output hasn't changed. It's expected that we'll change - // this file as the API evolves, but pay attention to the diffs to ensure - // that the changes match your expectations. - assert_contents("../openapi/gateway.json", &stdout_text); -} diff --git a/gateway/tests/integration_tests/component_list.rs b/gateway/tests/integration_tests/component_list.rs index c2c47a64a4..ec876c0783 100644 --- a/gateway/tests/integration_tests/component_list.rs +++ b/gateway/tests/integration_tests/component_list.rs @@ -10,10 +10,10 @@ use gateway_messages::SpComponent; use gateway_messages::SpPort; use gateway_test_utils::current_simulator_state; use gateway_test_utils::setup; -use omicron_gateway::http_entrypoints::SpComponentInfo; -use omicron_gateway::http_entrypoints::SpComponentList; -use omicron_gateway::http_entrypoints::SpComponentPresence; -use omicron_gateway::http_entrypoints::SpType; +use gateway_types::component::SpComponentInfo; +use gateway_types::component::SpComponentList; +use gateway_types::component::SpComponentPresence; +use gateway_types::component::SpType; #[tokio::test] async fn component_list() { diff --git a/gateway/tests/integration_tests/location_discovery.rs b/gateway/tests/integration_tests/location_discovery.rs index 61da006a06..1e571d343b 100644 --- a/gateway/tests/integration_tests/location_discovery.rs +++ b/gateway/tests/integration_tests/location_discovery.rs @@ -7,8 +7,8 @@ use dropshot::test_util; use gateway_messages::SpPort; use gateway_test_utils::setup; -use omicron_gateway::http_entrypoints::SpState; -use omicron_gateway::http_entrypoints::SpType; +use gateway_types::component::SpState; +use gateway_types::component::SpType; use omicron_gateway::SpIdentifier; #[tokio::test] diff --git a/gateway/tests/integration_tests/mod.rs b/gateway/tests/integration_tests/mod.rs index 1c6ca0d037..3fafad6058 100644 --- a/gateway/tests/integration_tests/mod.rs +++ b/gateway/tests/integration_tests/mod.rs @@ -4,7 +4,6 @@ // Copyright 2022 Oxide Computer Company -mod commands; mod component_list; mod location_discovery; mod serial_console; diff --git a/gateway/tests/integration_tests/serial_console.rs b/gateway/tests/integration_tests/serial_console.rs index 11cb9674a7..c2f6743723 100644 --- a/gateway/tests/integration_tests/serial_console.rs +++ b/gateway/tests/integration_tests/serial_console.rs @@ -10,10 +10,10 @@ use gateway_messages::SpPort; use gateway_test_utils::current_simulator_state; use gateway_test_utils::setup; use gateway_test_utils::sim_sp_serial_console; +use gateway_types::component::SpType; use http::uri::Scheme; use http::StatusCode; use http::Uri; -use omicron_gateway::http_entrypoints::SpType; use tokio_tungstenite::tungstenite; use tokio_tungstenite::tungstenite::protocol::Message; diff --git a/gateway/tests/output/cmd-mgs-openapi-stderr b/gateway/tests/output/cmd-mgs-openapi-stderr deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openapi/gateway.json b/openapi/gateway.json index 8bd71e7c99..d61c5d1766 100644 --- a/openapi/gateway.json +++ b/openapi/gateway.json @@ -828,8 +828,8 @@ }, "/sp/{type}/{slot}/component/{component}/serial-console/attach": { "get": { - "summary": "Upgrade into a websocket connection attached to the given SP component's", - "description": "serial console.", + "summary": "Upgrade into a websocket connection attached to the given SP", + "description": "component's serial console.", "operationId": "sp_component_serial_console_attach", "parameters": [ { @@ -875,8 +875,8 @@ }, "/sp/{type}/{slot}/component/{component}/serial-console/detach": { "post": { - "summary": "Detach the websocket connection attached to the given SP component's serial", - "description": "console, if such a connection exists.", + "summary": "Detach the websocket connection attached to the given SP component's", + "description": "serial console, if such a connection exists.", "operationId": "sp_component_serial_console_detach", "parameters": [ { diff --git a/sp-sim/Cargo.toml b/sp-sim/Cargo.toml index 7270db1a67..92969472a1 100644 --- a/sp-sim/Cargo.toml +++ b/sp-sim/Cargo.toml @@ -13,8 +13,8 @@ async-trait.workspace = true clap.workspace = true dropshot.workspace = true futures.workspace = true -omicron-gateway.workspace = true gateway-messages.workspace = true +gateway-types.workspace = true hex = { workspace = true, features = [ "serde" ] } omicron-common.workspace = true serde.workspace = true diff --git a/sp-sim/src/gimlet.rs b/sp-sim/src/gimlet.rs index ac465cb217..93f34f76c1 100644 --- a/sp-sim/src/gimlet.rs +++ b/sp-sim/src/gimlet.rs @@ -36,6 +36,7 @@ use gateway_messages::SpStateV2; use gateway_messages::{version, MessageKind}; use gateway_messages::{ComponentDetails, Message, MgsError, StartupOptions}; use gateway_messages::{DiscoverResponse, IgnitionState, PowerState}; +use gateway_types::component::SpState; use slog::{debug, error, info, warn, Logger}; use std::cell::Cell; use std::collections::HashMap; @@ -104,8 +105,8 @@ impl Drop for Gimlet { #[async_trait] impl SimulatedSp for Gimlet { - async fn state(&self) -> omicron_gateway::http_entrypoints::SpState { - omicron_gateway::http_entrypoints::SpState::from( + async fn state(&self) -> SpState { + SpState::from( self.handler.as_ref().unwrap().lock().await.sp_state_impl(), ) } diff --git a/sp-sim/src/lib.rs b/sp-sim/src/lib.rs index 868d7ded2c..0f340ed642 100644 --- a/sp-sim/src/lib.rs +++ b/sp-sim/src/lib.rs @@ -13,6 +13,7 @@ pub use anyhow::Result; use async_trait::async_trait; pub use config::Config; use gateway_messages::SpPort; +use gateway_types::component::SpState; pub use gimlet::Gimlet; pub use gimlet::SimSpHandledRequest; pub use gimlet::SIM_GIMLET_BOARD; @@ -36,7 +37,7 @@ pub enum Responsiveness { #[async_trait] pub trait SimulatedSp { /// Serial number. - async fn state(&self) -> omicron_gateway::http_entrypoints::SpState; + async fn state(&self) -> SpState; /// Listening UDP address of the given port of this simulated SP, if it was /// configured to listen. diff --git a/sp-sim/src/sidecar.rs b/sp-sim/src/sidecar.rs index a6bc49e609..c2fb2467d8 100644 --- a/sp-sim/src/sidecar.rs +++ b/sp-sim/src/sidecar.rs @@ -44,6 +44,7 @@ use gateway_messages::SpError; use gateway_messages::SpPort; use gateway_messages::SpStateV2; use gateway_messages::StartupOptions; +use gateway_types::component::SpState; use slog::debug; use slog::info; use slog::warn; @@ -81,8 +82,8 @@ impl Drop for Sidecar { #[async_trait] impl SimulatedSp for Sidecar { - async fn state(&self) -> omicron_gateway::http_entrypoints::SpState { - omicron_gateway::http_entrypoints::SpState::from( + async fn state(&self) -> SpState { + SpState::from( self.handler.as_ref().unwrap().lock().await.sp_state_impl(), ) }