From 16a81d740f310723e8de5b5c4783eb5c4c39027a Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Wed, 8 Jul 2026 19:12:47 +0000 Subject: [PATCH 1/2] Add versioned API for rack commissioning (RFD 710) This was a claude oneshot to copy over the existing wicketd APIs that are used in rkdeploy and then version them. It seems to have done this correctly. Note that it uses types from `wicket_common` that are unversioned. If these change in the commissioned API, we will have to copy them to V1 and update the users. Claude knows how to do this. Doing it now would be overkill and would likely lead to a lot of `From` impls. Since we are just starting off, let's keep things simple for now and not change the internal types yet. --- Cargo.lock | 41 + Cargo.toml | 9 + common/src/address.rs | 3 + dev-tools/dropshot-apis/Cargo.toml | 1 + dev-tools/dropshot-apis/src/main.rs | 19 + .../wicketd-commission-1.0.0-caad21.json | 6467 +++++++++++++++++ .../wicketd-commission-latest.json | 1 + sled-agent/src/services.rs | 6 + smf/wicketd/manifest.xml | 3 +- wicketd-commission-api/Cargo.toml | 16 + wicketd-commission-api/src/lib.rs | 217 + wicketd-commission-types-versions/Cargo.toml | 21 + .../src/initial/artifacts.rs | 44 + .../src/initial/bootstrap_sleds.rs | 40 + .../src/initial/inventory.rs | 23 + .../src/initial/location.rs | 25 + .../src/initial/mod.rs | 12 + .../src/initial/rss_config.rs | 47 + .../src/initial/update.rs | 28 + .../src/latest.rs | 29 + wicketd-commission-types-versions/src/lib.rs | 34 + wicketd-commission-types/Cargo.toml | 12 + wicketd-commission-types/src/artifacts.rs | 5 + .../src/bootstrap_sleds.rs | 5 + wicketd-commission-types/src/inventory.rs | 5 + wicketd-commission-types/src/lib.rs | 20 + wicketd-commission-types/src/location.rs | 5 + wicketd-commission-types/src/rss_config.rs | 5 + wicketd-commission-types/src/update.rs | 5 + wicketd/Cargo.toml | 2 + wicketd/src/bin/wicketd.rs | 7 + wicketd/src/commission_http_entrypoints.rs | 685 ++ wicketd/src/http_entrypoints.rs | 45 +- wicketd/src/lib.rs | 84 +- wicketd/tests/integration_tests/setup.rs | 1 + 35 files changed, 7929 insertions(+), 43 deletions(-) create mode 100644 openapi/wicketd-commission/wicketd-commission-1.0.0-caad21.json create mode 120000 openapi/wicketd-commission/wicketd-commission-latest.json create mode 100644 wicketd-commission-api/Cargo.toml create mode 100644 wicketd-commission-api/src/lib.rs create mode 100644 wicketd-commission-types-versions/Cargo.toml create mode 100644 wicketd-commission-types-versions/src/initial/artifacts.rs create mode 100644 wicketd-commission-types-versions/src/initial/bootstrap_sleds.rs create mode 100644 wicketd-commission-types-versions/src/initial/inventory.rs create mode 100644 wicketd-commission-types-versions/src/initial/location.rs create mode 100644 wicketd-commission-types-versions/src/initial/mod.rs create mode 100644 wicketd-commission-types-versions/src/initial/rss_config.rs create mode 100644 wicketd-commission-types-versions/src/initial/update.rs create mode 100644 wicketd-commission-types-versions/src/latest.rs create mode 100644 wicketd-commission-types-versions/src/lib.rs create mode 100644 wicketd-commission-types/Cargo.toml create mode 100644 wicketd-commission-types/src/artifacts.rs create mode 100644 wicketd-commission-types/src/bootstrap_sleds.rs create mode 100644 wicketd-commission-types/src/inventory.rs create mode 100644 wicketd-commission-types/src/lib.rs create mode 100644 wicketd-commission-types/src/location.rs create mode 100644 wicketd-commission-types/src/rss_config.rs create mode 100644 wicketd-commission-types/src/update.rs create mode 100644 wicketd/src/commission_http_entrypoints.rs diff --git a/Cargo.lock b/Cargo.lock index 1d6f5f72823..0fdfe08aa86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8932,6 +8932,7 @@ dependencies = [ "serde_json", "sled-agent-api", "wicketd-api", + "wicketd-commission-api", ] [[package]] @@ -17410,6 +17411,8 @@ dependencies = [ "wicket-common", "wicketd-api", "wicketd-client", + "wicketd-commission-api", + "wicketd-commission-types", ] [[package]] @@ -17457,6 +17460,44 @@ dependencies = [ "wicket-common", ] +[[package]] +name = "wicketd-commission-api" +version = "0.1.0" +dependencies = [ + "bootstrap-agent-lockstep-client", + "dropshot", + "dropshot-api-manager-types", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "wicket-common", + "wicketd-commission-types-versions", +] + +[[package]] +name = "wicketd-commission-types" +version = "0.1.0" +dependencies = [ + "omicron-workspace-hack", + "wicketd-commission-types-versions", +] + +[[package]] +name = "wicketd-commission-types-versions" +version = "0.1.0" +dependencies = [ + "bootstrap-agent-lockstep-client", + "omicron-common", + "omicron-passwords", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "schemars 0.8.22", + "semver 1.0.28", + "serde", + "sled-hardware-types", + "tufaceous-artifact", + "wicket-common", +] + [[package]] name = "widestring" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 8405e5e7a66..1e98f964480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,6 +185,9 @@ members = [ "wicket", "wicketd", "wicketd-api", + "wicketd-commission-api", + "wicketd-commission-types", + "wicketd-commission-types-versions", "workspace-hack", "zone-setup" ] @@ -378,6 +381,9 @@ default-members = [ "wicket", "wicketd", "wicketd-api", + "wicketd-commission-api", + "wicketd-commission-types", + "wicketd-commission-types-versions", "workspace-hack", "zone-setup", ] @@ -891,6 +897,9 @@ wicket = { path = "wicket" } wicket-common = { path = "wicket-common" } wicketd-api = { path = "wicketd-api" } wicketd-client = { path = "clients/wicketd-client" } +wicketd-commission-api = { path = "wicketd-commission-api" } +wicketd-commission-types = { path = "wicketd-commission-types" } +wicketd-commission-types-versions = { path = "wicketd-commission-types-versions" } x509-cert = { version = "0.2.5", default-features = false } xshell = "0.2.7" zerocopy = "0.8.26" diff --git a/common/src/address.rs b/common/src/address.rs index 480777dcbac..ead7d9d8d22 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -261,6 +261,9 @@ pub const NEXUS_TECHPORT_EXTERNAL_PORT: u16 = 12228; /// interface(s). pub const WICKETD_NEXUS_PROXY_PORT: u16 = 12229; +/// The port on which `wicketd` serves its "commission" API +pub const WICKETD_COMMISSION_PORT: u16 = 12230; + /// The port on which NTP runs pub const NTP_PORT: u16 = 123; diff --git a/dev-tools/dropshot-apis/Cargo.toml b/dev-tools/dropshot-apis/Cargo.toml index 3418a2b7f08..77f0618aec5 100644 --- a/dev-tools/dropshot-apis/Cargo.toml +++ b/dev-tools/dropshot-apis/Cargo.toml @@ -30,6 +30,7 @@ serde.workspace = true serde_json.workspace = true sled-agent-api.workspace = true wicketd-api.workspace = true +wicketd-commission-api.workspace = true [lints] workspace = true diff --git a/dev-tools/dropshot-apis/src/main.rs b/dev-tools/dropshot-apis/src/main.rs index 5935c05be45..81fc377f79b 100644 --- a/dev-tools/dropshot-apis/src/main.rs +++ b/dev-tools/dropshot-apis/src/main.rs @@ -30,6 +30,7 @@ use repo_depot_api::*; use serde::{Deserialize, Serialize}; use sled_agent_api::*; use wicketd_api::*; +use wicketd_commission_api::*; fn environment() -> anyhow::Result { // The workspace root is two levels up from this crate's directory. @@ -339,6 +340,24 @@ fn all_apis() -> anyhow::Result { api_description: wicketd_api_mod::stub_api_description, ident: "wicketd", }), + ManagedApi::from(ManagedApiConfig { + title: "Wicketd Commission API", + versions: Versions::new_versioned( + wicketd_commission_api::supported_versions(), + ), + metadata: ManagedApiMetadata { + description: Some( + "Subset of the wicketd API used by external \ + commissioning tools", + ), + contact_url: Some("https://oxide.computer"), + contact_email: Some("api@oxide.computer"), + extra: to_value(ApiBoundary::Internal), + }, + api_description: + wicketd_commission_api_mod::stub_api_description, + ident: "wicketd-commission", + }), ]; let apis = ManagedApis::new(apis) diff --git a/openapi/wicketd-commission/wicketd-commission-1.0.0-caad21.json b/openapi/wicketd-commission/wicketd-commission-1.0.0-caad21.json new file mode 100644 index 00000000000..2841e35664a --- /dev/null +++ b/openapi/wicketd-commission/wicketd-commission-1.0.0-caad21.json @@ -0,0 +1,6467 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Wicketd Commission API", + "description": "Subset of the wicketd API used by external commissioning tools", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "1.0.0" + }, + "paths": { + "/artifacts-and-event-reports": { + "get": { + "summary": "An endpoint used to report all available artifacts and event reports.", + "description": "The order of the returned artifacts is unspecified, and may change between calls even if the total set of artifacts has not.", + "operationId": "get_artifacts_and_event_reports", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetArtifactsAndEventReportsResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bootstrap-sleds": { + "get": { + "summary": "Get wicketd's current view of all sleds visible on the bootstrap network.", + "operationId": "get_bootstrap_sleds", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootstrapSledIps" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/clear-update-state": { + "post": { + "summary": "Resets update state for a sled.", + "description": "Use this to clear update state after a failed update.", + "operationId": "post_clear_update_state", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearUpdateStateParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearUpdateStateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/inventory": { + "get": { + "summary": "A status endpoint used to report high level information known to", + "description": "wicketd.\n\nThis endpoint can be polled to see if there have been state changes in the system that are useful to report to wicket.\n\nWicket, and possibly other callers, will retrieve the changed information, with follow up calls.", + "operationId": "get_inventory", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetInventoryParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetInventoryResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/location": { + "get": { + "summary": "Report the identity of the sled and switch we're currently running on /", + "description": "connected to.", + "operationId": "get_location", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLocationResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rack-setup": { + "get": { + "summary": "Query current state of rack setup.", + "operationId": "get_rack_setup_state", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackOperationStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Run rack setup.", + "description": "Will return an error if not all of the rack setup configuration has been populated.", + "operationId": "post_run_rack_setup", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RackInitUuid" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rack-setup/config": { + "get": { + "summary": "Get the current status of the user-provided (or system-default-provided, in", + "description": "some cases) RSS configuration.", + "operationId": "get_rss_config", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentRssUserConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update (a subset of) the current RSS configuration.", + "description": "Sensitive values (certificates and password hash) are not set through this endpoint.", + "operationId": "put_rss_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutRssUserConfigInsensitive" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Reset all RSS configuration to their default values.", + "operationId": "delete_rss_config", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rack-setup/config/cert": { + "post": { + "summary": "Add an external certificate.", + "description": "This must be paired with its private key. They may be posted in either order, but one cannot post two certs in a row (or two keys in a row).", + "operationId": "post_rss_config_cert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateUploadResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rack-setup/config/key": { + "post": { + "summary": "Add the private key of an external certificate.", + "description": "This must be paired with its certificate. They may be posted in either order, but one cannot post two keys in a row (or two certs in a row).", + "operationId": "post_rss_config_key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateUploadResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rack-setup/config/recovery-user-password-hash": { + "put": { + "summary": "Update the RSS config recovery silo user password hash.", + "operationId": "put_rss_config_recovery_user_password_hash", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutRssRecoveryUserPasswordHash" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/repository": { + "put": { + "summary": "Upload a TUF repository to the server.", + "description": "At any given time, wicketd will keep at most one TUF repository in memory. Any previously-uploaded repositories will be discarded.", + "operationId": "put_repository", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/update": { + "post": { + "summary": "An endpoint to start updating one or more sleds, switches and PSCs.", + "operationId": "post_start_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartUpdateParams" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AllowedSourceIps": { + "description": "Description of source IPs allowed to reach rack services.", + "oneOf": [ + { + "description": "Allow traffic from any external IP address.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "any" + ] + } + }, + "required": [ + "allow" + ] + }, + { + "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "list" + ] + }, + "ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "allow", + "ips" + ] + } + ] + }, + "ApplicationDescriptor": { + "description": "An Application Descriptor describes the supported datapath configurations.\n\nThis is a CMIS-specific concept. It's used for modules to advertise how it can be used by the host. Each application describes the host-side electrical interface; the media-side interface; the number of lanes required; etc.\n\nHost-side software can select one of these applications to instruct the module to use a specific set of lanes, with the interface on either side of the module.", + "type": "object", + "properties": { + "host_id": { + "description": "The electrical interface with the host side.", + "type": "string" + }, + "host_lane_assignment_options": { + "description": "The lanes on the host-side supporting this application.\n\nThis is a bit mask with a 1 identifying the lowest lane in a consecutive group of lanes to which the application can be assigned. This must be used with the `host_lane_count`. For example a value of `0b0000_0001` with a host lane count of 4 indicates that the first 4 lanes may be used in this application.\n\nAn application may support starting from multiple lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "host_lane_count": { + "description": "The number of host-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_id": { + "description": "The interface, optical or copper, with the media side.", + "allOf": [ + { + "$ref": "#/components/schemas/MediaInterfaceId" + } + ] + }, + "media_lane_assignment_options": { + "description": "The lanes on the media-side supporting this application.\n\nSee `host_lane_assignment_options` for details.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_lane_count": { + "description": "The number of media-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "host_id", + "host_lane_assignment_options", + "host_lane_count", + "media_id", + "media_lane_assignment_options", + "media_lane_count" + ] + }, + "ArtifactHashId": { + "description": "A hash-based identifier for an artifact or deployment unit: the kind and hash.", + "type": "object", + "properties": { + "hash": { + "description": "The hash of the artifact.", + "type": "string", + "format": "hex string (32 bytes)" + }, + "kind": { + "description": "The kind of artifact this is.", + "type": "string" + } + }, + "required": [ + "hash", + "kind" + ] + }, + "ArtifactId": { + "description": "An identifier for an artifact.", + "type": "object", + "properties": { + "kind": { + "description": "The kind of artifact this is.", + "type": "string" + }, + "name": { + "description": "The artifact's name.", + "type": "string" + }, + "version": { + "description": "The artifact's version.", + "type": "string" + } + }, + "required": [ + "kind", + "name", + "version" + ] + }, + "Aux1Monitor": { + "description": "The first auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The monitored property is custom, i.e., part-specific.", + "type": "object", + "properties": { + "custom": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "custom" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux2Monitor": { + "description": "The second auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux3Monitor": { + "description": "The third auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "Measured voltage of an additional power supply (Volts).", + "type": "object", + "properties": { + "additional_supply_voltage": { + "type": "number", + "format": "float" + } + }, + "required": [ + "additional_supply_voltage" + ], + "additionalProperties": false + } + ] + }, + "AuxMonitors": { + "description": "Auxlliary monitored values for CMIS modules.", + "type": "object", + "properties": { + "aux1": { + "nullable": true, + "description": "Auxlliary monitor 1, either a custom value or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux1Monitor" + } + ] + }, + "aux2": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux2Monitor" + } + ] + }, + "aux3": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or additional supply voltage.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux3Monitor" + } + ] + }, + "custom": { + "nullable": true, + "description": "A custom monitor. The value here is entirely vendor- and part-specific, so the part's data sheet must be consulted. The value may be either a signed or unsigned 16-bit integer, and so is included as raw bytes.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + "Baseboard": { + "description": "Describes properties that should uniquely identify a Gimlet.", + "oneOf": [ + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "identifier", + "model", + "revision", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "pc" + ] + } + }, + "required": [ + "identifier", + "model", + "type" + ] + } + ] + }, + "BgpAuthKeyId": { + "description": "The key identifier for authentication to use with a BGP peer.", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "BgpAuthKeyInfo": { + "description": "Describes insensitive information about a BGP authentication key.\n\nThis information is considered okay to display in the UI.", + "oneOf": [ + { + "description": "TCP-MD5 authentication.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "tcp_md5" + ] + }, + "sha256": { + "description": "A SHA-256 digest of the key.", + "type": "string", + "format": "hex string (32 bytes)" + } + }, + "required": [ + "kind", + "sha256" + ] + } + ] + }, + "BgpAuthKeyStatus": { + "description": "The status of a BGP authentication key.\n\nThis is part of a wicketd response, but is returned here because our tooling turns BTreeMaps into HashMaps. So we use a `replace` directive instead.", + "oneOf": [ + { + "description": "The key was specified but hasn't been set yet.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "unset" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "The key has been set.", + "type": "object", + "properties": { + "info": { + "description": "Information about the key.", + "allOf": [ + { + "$ref": "#/components/schemas/BgpAuthKeyInfo" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "set" + ] + } + }, + "required": [ + "info", + "status" + ] + } + ] + }, + "BgpConfig": { + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number for the BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "checker": { + "nullable": true, + "description": "Checker to apply to incoming messages.", + "default": null, + "type": "string" + }, + "max_paths": { + "description": "Maximum number of paths to use when multiple \"best paths\" exist", + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/MaxPathConfig" + } + ] + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + }, + "shaper": { + "nullable": true, + "description": "Shaper to apply to outgoing messages.", + "default": null, + "type": "string" + } + }, + "required": [ + "asn", + "originate" + ] + }, + "BootstrapSledDescription": { + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "bootstrap_ip": { + "nullable": true, + "description": "The sled's bootstrap address, if the host is on and we've discovered it on the bootstrap network.", + "type": "string", + "format": "ipv6" + }, + "id": { + "$ref": "#/components/schemas/SpIdentifier" + } + }, + "required": [ + "baseboard", + "id" + ] + }, + "BootstrapSledIp": { + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "ip": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "baseboard", + "ip" + ] + }, + "BootstrapSledIps": { + "type": "object", + "properties": { + "sleds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BootstrapSledIp" + } + } + }, + "required": [ + "sleds" + ] + }, + "CertificateUploadResponse": { + "oneOf": [ + { + "description": "The key has been uploaded, but we're waiting on its corresponding certificate chain.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "waiting_on_cert" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "The cert chain has been uploaded, but we're waiting on its corresponding private key.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "waiting_on_key" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "A cert chain and its key have been accepted.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cert_key_accepted" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "A cert chain and its key are valid, but have already been uploaded.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cert_key_duplicate_ignored" + ] + } + }, + "required": [ + "status" + ] + } + ] + }, + "ClearUpdateStateOptions": { + "type": "object", + "properties": { + "test_error": { + "nullable": true, + "description": "If passed in, fails the clear update state operation with a simulated error.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTestError" + } + ] + } + } + }, + "ClearUpdateStateParams": { + "type": "object", + "properties": { + "options": { + "description": "Options for clearing update state", + "allOf": [ + { + "$ref": "#/components/schemas/ClearUpdateStateOptions" + } + ] + }, + "targets": { + "description": "The SP identifiers to clear the update state for. Must be non-empty.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + } + }, + "required": [ + "options", + "targets" + ] + }, + "ClearUpdateStateResponse": { + "type": "object", + "properties": { + "cleared": { + "description": "The SPs for which update data was cleared.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + }, + "no_update_data": { + "description": "The SPs that had no update state to clear.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + } + }, + "required": [ + "cleared", + "no_update_data" + ] + }, + "CmisDatapath": { + "description": "A datapath in a CMIS module.\n\nIn contrast to SFF-8636, CMIS makes first-class the concept of a datapath: a set of lanes and all the associated machinery involved in the transfer of data. This includes:\n\n- The \"application descriptor\" which is the host and media interfaces, and the lanes on each side used to transfer data; - The state of the datapath in a well-defined finite state machine (see CMIS 5.0 section 6.3.3); - The flags indicating how the datapath components are operating, such as receiving an input Rx signal or whether the transmitter is disabled.", + "type": "object", + "properties": { + "application": { + "description": "The application descriptor for this datapath.", + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationDescriptor" + } + ] + }, + "lane_status": { + "description": "The status bits for each lane in the datapath.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisLaneStatus" + } + } + }, + "required": [ + "application", + "lane_status" + ] + }, + "CmisLaneStatus": { + "description": "The status of a single CMIS lane.\n\nIf any particular control or status value is unsupported by a module, it is `None`.", + "type": "object", + "properties": { + "rx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Rx auto-squelch.\n\nThe module can implement automatic squelching of the Rx output, if the media-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "rx_lol": { + "nullable": true, + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "nullable": true, + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "rx_output_enabled": { + "nullable": true, + "description": "Whether the Rx output is enabled.\n\nThe host may control this to disable the electrical output from the module to the host.", + "type": "boolean" + }, + "rx_output_polarity": { + "nullable": true, + "description": "The Rx output polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side output signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "rx_output_status": { + "description": "Status of host-side Rx output.\n\nThis indicates whether the Rx output is sending a valid signal to the host. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + }, + "state": { + "description": "The datapath state of this lane.\n\nSee CMIS 5.0 section 8.9.1 for details.", + "type": "string" + }, + "tx_adaptive_eq_fail": { + "nullable": true, + "description": "A failure in the Tx adaptive input equalization.", + "type": "boolean" + }, + "tx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Tx auto-squelch.\n\nThe module can implement automatic squelching of the Tx output, if the host-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "tx_failure": { + "nullable": true, + "description": "General Tx failure flag.\n\nThis indicates that an internal and unspecified malfunction has occurred on the Tx lane.", + "type": "boolean" + }, + "tx_force_squelch": { + "nullable": true, + "description": "Whether the host-side has force-squelched the Tx output.\n\nThis indicates that the host can _force_ squelching the output if the signal is not valid.", + "type": "boolean" + }, + "tx_input_polarity": { + "nullable": true, + "description": "The Tx input polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side input signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "tx_lol": { + "nullable": true, + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "nullable": true, + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + }, + "tx_output_enabled": { + "nullable": true, + "description": "Whether the Tx output is enabled.", + "type": "boolean" + }, + "tx_output_status": { + "description": "Status of media-side Tx output.\n\nThis indicates whether the Rx output is sending a valid signal to the media itself. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + } + }, + "required": [ + "rx_output_status", + "state", + "tx_output_status" + ] + }, + "CurrentRssUserConfig": { + "type": "object", + "properties": { + "insensitive": { + "$ref": "#/components/schemas/CurrentRssUserConfigInsensitive" + }, + "sensitive": { + "$ref": "#/components/schemas/CurrentRssUserConfigSensitive" + } + }, + "required": [ + "insensitive", + "sensitive" + ] + }, + "CurrentRssUserConfigInsensitive": { + "description": "The subset of `RackInitializeRequest` that the user fills in as clear text (e.g., via an uploaded config file).", + "type": "object", + "properties": { + "allowed_source_ips": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + }, + "bootstrap_sleds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BootstrapSledDescription" + }, + "uniqueItems": true + }, + "dns_servers": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_dns_ips": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_dns_zone_name": { + "type": "string" + }, + "external_jumbo_frames_opt_in_enabled": { + "description": "Enable the fleet-wide jumbo-frames opt-in. Operators can also toggle this at runtime via the Nexus API after handoff.", + "default": false, + "type": "boolean" + }, + "internal_services_ip_pool_ranges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpRange" + } + }, + "ntp_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/UserSpecifiedRackNetworkConfig" + } + ] + } + }, + "required": [ + "bootstrap_sleds", + "dns_servers", + "external_dns_ips", + "external_dns_zone_name", + "internal_services_ip_pool_ranges", + "ntp_servers" + ] + }, + "CurrentRssUserConfigSensitive": { + "type": "object", + "properties": { + "bgp_auth_keys": { + "$ref": "#/components/schemas/GetBgpAuthKeyInfoResponse" + }, + "num_external_certificates": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "recovery_silo_password_set": { + "type": "boolean" + } + }, + "required": [ + "bgp_auth_keys", + "num_external_certificates", + "recovery_silo_password_set" + ] + }, + "Datapath": { + "description": "Information about a transceiver's datapath.\n\nThis includes state related to the low-level eletrical and optical path through which bits flow. This includes flags like loss-of-signal / loss-of-lock; transmitter enablement state; and equalization parameters.", + "oneOf": [ + { + "description": "A number of datapaths in a CMIS module.\n\nCMIS modules may have a large number of supported configurations of their various lanes, each called an \"application\". These are described by the `ApplicationDescriptor` type, which mirrors CMIS 5.0 table 8-18. Each descriptor is identified by an \"Application Selector Code\", which is just its index in the section of the memory map describing them.\n\nEach lane can be used in zero or more applications, however, it may exist in at most one application at a time. These active applications, of which there may be more than one, are keyed by their codes in the contained mapping.", + "type": "object", + "properties": { + "cmis": { + "type": "object", + "properties": { + "connector": { + "description": "The type of free-side connector", + "type": "string" + }, + "datapaths": { + "description": "Mapping from \"application selector\" ID to its datapath information.\n\nThe datapath inclues the lanes used; host electrical interface; media interface; and a lot more about the state of the path.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisDatapath" + } + }, + "supported_lanes": { + "description": "A bit mask with a 1 in bit `i` if the `i`th lane is supported.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "connector", + "datapaths", + "supported_lanes" + ] + } + }, + "required": [ + "cmis" + ], + "additionalProperties": false + }, + { + "description": "Datapath state about each lane in an SFF-8636 module.", + "type": "object", + "properties": { + "sff8636": { + "type": "object", + "properties": { + "connector": { + "description": "The type of a media-side connector.\n\nThese values come from SFF-8024 Rev 4.10 Table 4-3.", + "type": "string" + }, + "lanes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sff8636Datapath" + }, + "minItems": 4, + "maxItems": 4 + }, + "specification": { + "$ref": "#/components/schemas/SffComplianceCode" + } + }, + "required": [ + "connector", + "lanes", + "specification" + ] + } + }, + "required": [ + "sff8636" + ], + "additionalProperties": false + } + ] + }, + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "EventReportForWicketdEngineSpec": { + "description": "A report produced from an [`EventBuffer`](crate::EventBuffer).\n\nRemote reports can be passed into a [`StepContext`](crate::StepContext), in which case they show up as nested events.", + "type": "object", + "properties": { + "last_seen": { + "nullable": true, + "description": "The last event seen.\n\n`last_seen` can be used to retrieve deltas of events.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "progress_events": { + "description": "A list of progress events, or whether we're currently waiting for a progress event.\n\nCurrently, this produces one progress event for each top-level and nested event in progress.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProgressEventForWicketdEngineSpec" + } + }, + "root_execution_id": { + "nullable": true, + "description": "The root execution ID for this report.\n\nEach report has a root execution ID, which ties together all step and progress events. This is always filled out if the list of step events is non-empty.", + "type": "string", + "format": "uuid" + }, + "step_events": { + "description": "A list of step events.\n\nStep events include success and failure events.", + "type": "array", + "items": { + "$ref": "#/components/schemas/StepEventForWicketdEngineSpec" + } + } + }, + "required": [ + "progress_events", + "step_events" + ] + }, + "ExtendedStatus": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "GetArtifactsAndEventReportsResponse": { + "description": "The response to a `get_artifacts` call: the system version, and the list of all artifacts currently held by wicketd.", + "type": "object", + "properties": { + "artifacts": { + "description": "Map of artifacts we ingested from the most-recently-uploaded TUF repository to a list of artifacts we're serving over the bootstrap network. In some cases the list of artifacts being served will have length 1 (when we're serving the artifact directly); in other cases the artifact in the TUF repo contains multiple nested artifacts inside it (e.g., RoT artifacts contain both A and B images), and we serve the list of extracted artifacts but not the original combination.\n\nConceptually, this is a `BTreeMap>`, but JSON requires string keys for maps, so we give back a vec of pairs instead.", + "type": "array", + "items": { + "$ref": "#/components/schemas/InstallableArtifacts" + } + }, + "event_reports": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/EventReportForWicketdEngineSpec" + } + } + }, + "system_version": { + "nullable": true, + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + }, + "required": [ + "artifacts", + "event_reports" + ] + }, + "GetBgpAuthKeyInfoResponse": { + "description": "Returns information about BGP keys for rack network setup.\n\nThis is part of a wicketd response, but is returned here because our tooling turns BTreeMaps into HashMaps. So we use a `replace` directive instead.", + "type": "object", + "properties": { + "data": { + "description": "Information about the requested keys.\n\nNone indicates that the key ID has not been set yet. An error indicates that the key was not specified in the rack setup config.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BgpAuthKeyStatus" + } + } + }, + "required": [ + "data" + ] + }, + "GetInventoryParams": { + "type": "object", + "properties": { + "force_refresh": { + "description": "Refresh the state of these SPs from MGS prior to returning (instead of returning cached data).", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + } + } + }, + "required": [ + "force_refresh" + ] + }, + "GetInventoryResponse": { + "description": "The response to a `get_inventory` call: the inventory known to wicketd, or a notification that data is unavailable.", + "oneOf": [ + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "inventory": { + "$ref": "#/components/schemas/RackV1Inventory" + } + }, + "required": [ + "inventory" + ] + }, + "type": { + "type": "string", + "enum": [ + "response" + ] + } + }, + "required": [ + "data", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unavailable" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "GetLocationResponse": { + "description": "All the fields of this response are optional, because it's possible we don't know any of them (yet) if MGS has not yet finished discovering its location or (ever) if we're running in a dev environment that doesn't support MGS-location / baseboard mapping.", + "type": "object", + "properties": { + "sled_baseboard": { + "nullable": true, + "description": "The baseboard of our sled (where wicketd is running).", + "allOf": [ + { + "$ref": "#/components/schemas/Baseboard" + } + ] + }, + "sled_id": { + "nullable": true, + "description": "The identity of our sled (where wicketd is running).", + "allOf": [ + { + "$ref": "#/components/schemas/SpIdentifier" + } + ] + }, + "switch_baseboard": { + "nullable": true, + "description": "The baseboard of the switch our sled is physically connected to.", + "allOf": [ + { + "$ref": "#/components/schemas/Baseboard" + } + ] + }, + "switch_id": { + "nullable": true, + "description": "The identity of the switch our sled is physically connected to.", + "allOf": [ + { + "$ref": "#/components/schemas/SpIdentifier" + } + ] + } + } + }, + "InstallableArtifacts": { + "type": "object", + "properties": { + "artifact_id": { + "$ref": "#/components/schemas/ArtifactId" + }, + "installable": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArtifactHashId" + } + }, + "sign": { + "nullable": true, + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "artifact_id", + "installable" + ] + }, + "IpNet": { + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::IpNet", + "version": "0.1.0" + }, + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + } + ] + }, + "IpRange": { + "oneOf": [ + { + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Range" + } + ] + }, + { + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Range" + } + ] + } + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Range": { + "description": "A non-decreasing IPv4 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv4" + }, + "last": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "first", + "last" + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Range": { + "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", + "type": "object", + "properties": { + "first": { + "type": "string", + "format": "ipv6" + }, + "last": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "first", + "last" + ] + }, + "LanePolarity": { + "description": "The polarity of a transceiver lane.", + "type": "string", + "enum": [ + "normal", + "flipped" + ] + }, + "LinkFec": { + "description": "The forward error correction mode of a link.", + "oneOf": [ + { + "description": "Firecode forward error correction.", + "type": "string", + "enum": [ + "firecode" + ] + }, + { + "description": "No forward error correction.", + "type": "string", + "enum": [ + "none" + ] + }, + { + "description": "Reed-Solomon forward error correction.", + "type": "string", + "enum": [ + "rs" + ] + } + ] + }, + "LinkSpeed": { + "description": "The speed of a link.", + "oneOf": [ + { + "description": "Zero gigabits per second.", + "type": "string", + "enum": [ + "speed0_g" + ] + }, + { + "description": "1 gigabit per second.", + "type": "string", + "enum": [ + "speed1_g" + ] + }, + { + "description": "10 gigabits per second.", + "type": "string", + "enum": [ + "speed10_g" + ] + }, + { + "description": "25 gigabits per second.", + "type": "string", + "enum": [ + "speed25_g" + ] + }, + { + "description": "40 gigabits per second.", + "type": "string", + "enum": [ + "speed40_g" + ] + }, + { + "description": "50 gigabits per second.", + "type": "string", + "enum": [ + "speed50_g" + ] + }, + { + "description": "100 gigabits per second.", + "type": "string", + "enum": [ + "speed100_g" + ] + }, + { + "description": "200 gigabits per second.", + "type": "string", + "enum": [ + "speed200_g" + ] + }, + { + "description": "400 gigabits per second.", + "type": "string", + "enum": [ + "speed400_g" + ] + } + ] + }, + "LldpAdminStatus": { + "description": "To what extent should this port participate in LLDP", + "type": "string", + "enum": [ + "enabled", + "disabled", + "rx_only", + "tx_only" + ] + }, + "LldpPortConfig": { + "description": "Per-port LLDP configuration settings. Only the \"status\" setting is mandatory. All other fields have natural defaults or may be inherited from the switch.", + "type": "object", + "properties": { + "chassis_id": { + "nullable": true, + "description": "Chassis ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + }, + "management_addrs": { + "nullable": true, + "description": "Management IP addresses to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "port_description": { + "nullable": true, + "description": "Port description to advertise. If this is not set, no description will be advertised.", + "type": "string" + }, + "port_id": { + "nullable": true, + "description": "Port ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be set to the port name. e.g., qsfp0/0.", + "type": "string" + }, + "status": { + "description": "To what extent should this port participate in LLDP", + "allOf": [ + { + "$ref": "#/components/schemas/LldpAdminStatus" + } + ] + }, + "system_description": { + "nullable": true, + "description": "System description to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "System name to advertise. If this is not set, it will be inherited from the switch-level settings.", + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "ManualPortConfig": { + "description": "User-specified version of `PortConfig`.\n\nAll of `PortConfig` is user-specified. But we expect the port name to be a key, rather than a field as in `PortConfig`. So this has all of the fields other than the port name.", + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSpecifiedUplinkAddressConfig" + } + }, + "autoneg": { + "type": "boolean" + }, + "bgp_peers": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSpecifiedBgpPeerConfig" + } + }, + "lldp": { + "nullable": true, + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + }, + "tx_eq": { + "nullable": true, + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + }, + "uplink_port_fec": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "uplink_port_speed": { + "$ref": "#/components/schemas/LinkSpeed" + } + }, + "required": [ + "addresses", + "autoneg", + "routes", + "uplink_port_speed" + ], + "additionalProperties": false + }, + "MaxPathConfig": { + "type": "integer", + "format": "uint8", + "minimum": 1, + "maximum": 32 + }, + "MediaInterfaceId": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for multi-mode fiber media.\n\nSee SFF-8024 Table 4-6.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mmf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for single-mode fiber.\n\nSee SFF-8024 Table 4-7.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "smf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for passive copper cables.\n\nSee SFF-8024 Table 4-8.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "passive_copper" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for active cable assemblies.\n\nSee SFF-8024 Table 4-9.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "active_cable" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for BASE-T.\n\nSee SFF-8024 Table 4-10.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "base_t" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, + "MgsV1Inventory": { + "description": "The current state of the v1 Rack as known to MGS", + "type": "object", + "properties": { + "sps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpInventory" + } + } + }, + "required": [ + "sps" + ] + }, + "MgsV1InventorySnapshot": { + "description": "The state of the v1 Rack as known to MGS at a given time.", + "type": "object", + "properties": { + "inventory": { + "$ref": "#/components/schemas/MgsV1Inventory" + }, + "last_seen": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "inventory", + "last_seen" + ] + }, + "Monitors": { + "description": "Free-side device monitoring information.\n\nNote that all values are optional, as some specifications do not require that modules implement monitoring of those values.", + "type": "object", + "properties": { + "aux_monitors": { + "nullable": true, + "description": "Auxiliary monitoring values.\n\nThese are only available on CMIS-compatible transceivers, e.g., QSFP-DD.", + "allOf": [ + { + "$ref": "#/components/schemas/AuxMonitors" + } + ] + }, + "receiver_power": { + "nullable": true, + "description": "The measured input optical power (milliwatts);\n\nNote that due to a limitation in the SFF-8636 specification, it's possible for receiver power to be zero. See [`ReceiverPower`] for details.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceiverPower" + } + }, + "supply_voltage": { + "nullable": true, + "description": "The measured input supply voltage (Volts).", + "type": "number", + "format": "float" + }, + "temperature": { + "nullable": true, + "description": "The measured cage temperature (degrees C);", + "type": "number", + "format": "float" + }, + "transmitter_bias_current": { + "nullable": true, + "description": "The output laser bias current (milliamps).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, + "transmitter_power": { + "nullable": true, + "description": "The measured output optical power (milliwatts).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + } + }, + "Name": { + "title": "A name unique within the parent collection", + "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", + "type": "string", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", + "minLength": 1, + "maxLength": 63 + }, + "NewPasswordHash": { + "title": "A password hash in PHC string format", + "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", + "type": "string" + }, + "Oui": { + "description": "An Organization Unique Identifier.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 3, + "maxItems": 3 + }, + "OutputStatus": { + "type": "string", + "enum": [ + "valid", + "invalid" + ] + }, + "PowerMode": { + "description": "The power mode of a module.", + "type": "object", + "properties": { + "software_override": { + "nullable": true, + "description": "Whether the module is configured for software override of power control.\n\nIf the module is in `PowerState::Off`, this can't be determined, and `None` is returned.", + "type": "boolean" + }, + "state": { + "description": "The actual power state.", + "allOf": [ + { + "$ref": "#/components/schemas/PowerState3" + } + ] + } + }, + "required": [ + "state" + ] + }, + "PowerState": { + "description": "See RFD 81.\n\nThis enum only lists power states the SP is able to control; higher power states are controlled by ignition.\n\n
JSON schema\n\n```json { \"description\": \"See RFD 81.\\n\\nThis enum only lists power states the SP is able to control; higher power states are controlled by ignition.\", \"type\": \"string\", \"enum\": [ \"A0\", \"A1\", \"A2\" ] } ```
", + "type": "string", + "enum": [ + "A0", + "A1", + "A2" + ] + }, + "PowerState2": { + "description": "See RFD 81.\n\nThis enum only lists power states the SP is able to control; higher power states are controlled by ignition.", + "type": "string", + "enum": [ + "A0", + "A1", + "A2" + ] + }, + "PowerState3": { + "description": "An allowed power state for the module.", + "oneOf": [ + { + "description": "A module is entirely powered off, using the EFuse.", + "type": "string", + "enum": [ + "off" + ] + }, + { + "description": "Power is enabled to the module, but module remains in low-power mode.\n\nIn this state, modules will not establish a link or transmit traffic, but they may be managed and queried for information through their memory maps.", + "type": "string", + "enum": [ + "low" + ] + }, + { + "description": "The module is in high-power mode.\n\nNote that additional configuration may be required to correctly configure the module, such as described in SFF-8636 rev 2.10a table 6-10, and that the _host side_ is responsible for ensuring that the relevant configuration is applied.", + "type": "string", + "enum": [ + "high" + ] + } + ] + }, + "ProgressCounter": { + "description": "Current progress.\n\nBoth `current` and `total` are abstract counters. These counters are often a number of bytes. There is no guarantee that the counter won't go back in subsequent events; that can happen e.g. if a fetch happens from multiple peers within a single attempt.", + "type": "object", + "properties": { + "current": { + "description": "The current progress.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "total": { + "nullable": true, + "description": "The total progress.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "units": { + "description": "Progress units.", + "type": "string" + } + }, + "required": [ + "current", + "units" + ] + }, + "ProgressEventForGenericSpec": { + "type": "object", + "properties": { + "data": { + "description": "The kind of event this is.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressEventKindForGenericSpec" + } + ] + }, + "execution_id": { + "description": "The execution ID.", + "type": "string", + "format": "uuid" + }, + "spec": { + "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used with `Self::from_generic` to deserialize generic metadata.", + "type": "string" + }, + "total_elapsed": { + "description": "Total time elapsed since the start of execution.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "data", + "execution_id", + "spec", + "total_elapsed" + ] + }, + "ProgressEventForWicketdEngineSpec": { + "type": "object", + "properties": { + "data": { + "description": "The kind of event this is.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressEventKindForWicketdEngineSpec" + } + ] + }, + "execution_id": { + "description": "The execution ID.", + "type": "string", + "format": "uuid" + }, + "spec": { + "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used with `Self::from_generic` to deserialize generic metadata.", + "type": "string" + }, + "total_elapsed": { + "description": "Total time elapsed since the start of execution.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "data", + "execution_id", + "spec", + "total_elapsed" + ] + }, + "ProgressEventKindForGenericSpec": { + "oneOf": [ + { + "description": "The update engine is waiting for a progress message.\n\nThe update engine sends this message immediately after a [`StepEvent`] corresponding to a new step.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "Total time elapsed since the start of the attempt.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "waiting_for_progress" + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "step", + "step_elapsed" + ] + }, + { + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "Total time elapsed since the start of the attempt.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "progress" + ] + }, + "metadata": { + "description": "Metadata that was returned with progress." + }, + "progress": { + "nullable": true, + "description": "Current progress.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressCounter" + } + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "metadata", + "step", + "step_elapsed" + ] + }, + { + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "event": { + "description": "The event that occurred.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressEventForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "nested" + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "event", + "kind", + "step", + "step_elapsed" + ] + }, + { + "description": "Future variants that might be unknown.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "ProgressEventKindForWicketdEngineSpec": { + "oneOf": [ + { + "description": "The update engine is waiting for a progress message.\n\nThe update engine sends this message immediately after a [`StepEvent`] corresponding to a new step.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "Total time elapsed since the start of the attempt.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "waiting_for_progress" + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "step", + "step_elapsed" + ] + }, + { + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "Total time elapsed since the start of the attempt.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "progress" + ] + }, + "metadata": { + "description": "Metadata that was returned with progress." + }, + "progress": { + "nullable": true, + "description": "Current progress.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressCounter" + } + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "metadata", + "step", + "step_elapsed" + ] + }, + { + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number currently being executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "event": { + "description": "The event that occurred.", + "allOf": [ + { + "$ref": "#/components/schemas/ProgressEventForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "nested" + ] + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "event", + "kind", + "step", + "step_elapsed" + ] + }, + { + "description": "Future variants that might be unknown.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "PutRssRecoveryUserPasswordHash": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/components/schemas/NewPasswordHash" + } + }, + "required": [ + "hash" + ] + }, + "PutRssUserConfigInsensitive": { + "description": "The portion of `CurrentRssUserConfig` that can be posted in one shot; it is provided by the wicket user uploading a TOML file, currently.\n\nThis is the \"write\" version of [`CurrentRssUserConfigInsensitive`], with some different fields.", + "type": "object", + "properties": { + "allowed_source_ips": { + "$ref": "#/components/schemas/AllowedSourceIps" + }, + "bootstrap_sleds": { + "description": "List of slot numbers only.\n\n`wicketd` will map this back to sleds with the correct `SpIdentifier` based on the `bootstrap_sleds` it provides in `CurrentRssUserConfigInsensitive`.", + "type": "array", + "items": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "uniqueItems": true + }, + "dns_servers": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_dns_ips": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "external_dns_zone_name": { + "type": "string" + }, + "external_jumbo_frames_opt_in_enabled": { + "description": "Enable the fleet-wide jumbo-frames opt-in.", + "default": false, + "type": "boolean" + }, + "internal_services_ip_pool_ranges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpRange" + } + }, + "ntp_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "$ref": "#/components/schemas/UserSpecifiedRackNetworkConfig" + } + }, + "required": [ + "allowed_source_ips", + "bootstrap_sleds", + "dns_servers", + "external_dns_ips", + "external_dns_zone_name", + "internal_services_ip_pool_ranges", + "ntp_servers", + "rack_network_config" + ], + "additionalProperties": false + }, + "RackInitUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackInitUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "RackOperationStatus": { + "description": "Current status of any rack-level operation being performed by this bootstrap agent.\n\n
JSON schema\n\n```json { \"description\": \"Current status of any rack-level operation being performed by this bootstrap agent.\", \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"id\", \"status\", \"step\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackInitUuid\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"initializing\" ] }, \"step\": { \"$ref\": \"#/components/schemas/RssStep\" } } }, { \"description\": \"`id` will be none if the rack was already initialized on startup.\", \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"id\": { \"oneOf\": [ { \"type\": \"null\" }, { \"allOf\": [ { \"$ref\": \"#/components/schemas/RackInitUuid\" } ] } ] }, \"status\": { \"type\": \"string\", \"enum\": [ \"initialized\" ] } } }, { \"type\": \"object\", \"required\": [ \"id\", \"message\", \"status\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackInitUuid\" }, \"message\": { \"type\": \"string\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"initialization_failed\" ] } } }, { \"type\": \"object\", \"required\": [ \"id\", \"status\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackInitUuid\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"initialization_panicked\" ] } } }, { \"type\": \"object\", \"required\": [ \"id\", \"status\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackResetUuid\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"resetting\" ] } } }, { \"description\": \"`reset_id` will be None if the rack is in an uninitialized-on-startup, or Some if it is in an uninitialized state due to a reset operation completing.\", \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"reset_id\": { \"oneOf\": [ { \"type\": \"null\" }, { \"allOf\": [ { \"$ref\": \"#/components/schemas/RackResetUuid\" } ] } ] }, \"status\": { \"type\": \"string\", \"enum\": [ \"uninitialized\" ] } } }, { \"type\": \"object\", \"required\": [ \"id\", \"message\", \"status\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackResetUuid\" }, \"message\": { \"type\": \"string\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"reset_failed\" ] } } }, { \"type\": \"object\", \"required\": [ \"id\", \"status\" ], \"properties\": { \"id\": { \"$ref\": \"#/components/schemas/RackResetUuid\" }, \"status\": { \"type\": \"string\", \"enum\": [ \"reset_panicked\" ] } } } ] } ```
", + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "status": { + "type": "string", + "enum": [ + "initializing" + ] + }, + "step": { + "$ref": "#/components/schemas/RssStep" + } + }, + "required": [ + "id", + "status", + "step" + ] + }, + { + "description": "`id` will be none if the rack was already initialized on startup.", + "type": "object", + "properties": { + "id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackInitUuid" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "initialized" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "initialization_failed" + ] + } + }, + "required": [ + "id", + "message", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackInitUuid" + }, + "status": { + "type": "string", + "enum": [ + "initialization_panicked" + ] + } + }, + "required": [ + "id", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "status": { + "type": "string", + "enum": [ + "resetting" + ] + } + }, + "required": [ + "id", + "status" + ] + }, + { + "description": "`reset_id` will be None if the rack is in an uninitialized-on-startup, or Some if it is in an uninitialized state due to a reset operation completing.", + "type": "object", + "properties": { + "reset_id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackResetUuid" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "uninitialized" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "reset_failed" + ] + } + }, + "required": [ + "id", + "message", + "status" + ] + }, + { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RackResetUuid" + }, + "status": { + "type": "string", + "enum": [ + "reset_panicked" + ] + } + }, + "required": [ + "id", + "status" + ] + } + ] + }, + "RackResetUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackResetUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "RackV1Inventory": { + "description": "The current state of the v1 Rack as known to wicketd", + "type": "object", + "properties": { + "mgs": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/MgsV1InventorySnapshot" + } + ] + }, + "transceivers": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TransceiverInventorySnapshot" + } + ] + } + } + }, + "ReceiverPower": { + "description": "Measured receiver optical power.\n\nThe SFF specifications allow for devices to monitor input optical power in several ways. It may either be an average power, over some unspecified time, or a peak-to-peak power. The latter is often abbreviated OMA, for Optical Modulation Amplitude. Again the time interval for peak-to-peak measurments are not specified.\n\nDetails -------\n\nThe SFF-8636 specification has an unfortunate limitation. There is no separate advertisement for whether a module supports measurements of receiver power. Instead, the _kind_ of measurement is advertised. The _same bit value_ could mean that either a peak-to-peak measurement is supported, or the measurements are not supported at all. Thus values of `PeakToPeak(0.0)` may mean that power measurements are not supported.", + "oneOf": [ + { + "description": "The measurement is represents average optical power, in mW.", + "type": "object", + "properties": { + "average": { + "type": "number", + "format": "float" + } + }, + "required": [ + "average" + ], + "additionalProperties": false + }, + { + "description": "The measurement represents a peak-to-peak, in mW.", + "type": "object", + "properties": { + "peak_to_peak": { + "type": "number", + "format": "float" + } + }, + "required": [ + "peak_to_peak" + ], + "additionalProperties": false + } + ] + }, + "RotImageError": { + "type": "string", + "enum": [ + "unchecked", + "first_page_erased", + "partially_programmed", + "invalid_length", + "header_not_programmed", + "bootloader_too_small", + "bad_magic", + "header_image_size", + "unaligned_length", + "unsupported_type", + "reset_vector_not_thumb2", + "reset_vector", + "signature" + ] + }, + "RotInventory": { + "description": "RoT-related data that isn't already supplied in [`SpState`].", + "type": "object", + "properties": { + "active": { + "$ref": "#/components/schemas/RotSlot" + }, + "caboose_a": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_b": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_stage0": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_stage0next": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + } + }, + "required": [ + "active" + ] + }, + "RotSlot": { + "oneOf": [ + { + "type": "object", + "properties": { + "slot": { + "type": "string", + "enum": [ + "a" + ] + } + }, + "required": [ + "slot" + ] + }, + { + "type": "object", + "properties": { + "slot": { + "type": "string", + "enum": [ + "b" + ] + } + }, + "required": [ + "slot" + ] + } + ] + }, + "RotState": { + "oneOf": [ + { + "type": "object", + "properties": { + "active": { + "$ref": "#/components/schemas/RotSlot" + }, + "pending_persistent_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] + }, + "persistent_boot_preference": { + "$ref": "#/components/schemas/RotSlot" + }, + "slot_a_sha3_256_digest": { + "nullable": true, + "type": "string" + }, + "slot_b_sha3_256_digest": { + "nullable": true, + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "v2" + ] + }, + "transient_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] + } + }, + "required": [ + "active", + "persistent_boot_preference", + "state" + ] + }, + { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "communication_failed" + ] + } + }, + "required": [ + "message", + "state" + ] + }, + { + "type": "object", + "properties": { + "active": { + "$ref": "#/components/schemas/RotSlot" + }, + "pending_persistent_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] + }, + "persistent_boot_preference": { + "$ref": "#/components/schemas/RotSlot" + }, + "slot_a_error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotImageError" + } + ] + }, + "slot_a_fwid": { + "type": "string" + }, + "slot_b_error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotImageError" + } + ] + }, + "slot_b_fwid": { + "type": "string" + }, + "stage0_error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotImageError" + } + ] + }, + "stage0_fwid": { + "type": "string" + }, + "stage0next_error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotImageError" + } + ] + }, + "stage0next_fwid": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "v3" + ] + }, + "transient_boot_preference": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] + } + }, + "required": [ + "active", + "persistent_boot_preference", + "slot_a_fwid", + "slot_b_fwid", + "stage0_fwid", + "stage0next_fwid", + "state" + ] + } + ] + }, + "RouteConfig": { + "type": "object", + "properties": { + "destination": { + "description": "The destination of the route.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "nexthop": { + "description": "The nexthop/gateway address.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "The RIB priority (i.e. Admin Distance) associated with this route.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id associated with this route.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "destination", + "nexthop" + ] + }, + "RouterLifetimeConfig": { + "description": "Router lifetime in seconds for unnumbered BGP peers", + "type": "integer", + "format": "uint16", + "minimum": 0, + "maximum": 9000 + }, + "RssStep": { + "description": "Steps we go through during initial rack setup. Keep this list in order that they happen.\n\n
JSON schema\n\n```json { \"description\": \"Steps we go through during initial rack setup. Keep this list in order that they happen.\", \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"requested\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"starting\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"load_existing_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"create_sled_plan\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_trust_quorum\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"initial_network_config_update\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"sled_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"final_network_config_update\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"configure_dns\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"init_ntp\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_time_sync\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"wait_for_database\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"cluster_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"zones_init\" ] } } }, { \"type\": \"object\", \"required\": [ \"status\" ], \"properties\": { \"status\": { \"type\": \"string\", \"enum\": [ \"nexus_handoff\" ] } } } ] } ```
", + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "requested" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "starting" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "load_existing_plan" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "create_sled_plan" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_trust_quorum" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "initial_network_config_update" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "sled_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "final_network_config_update" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_dns" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "configure_dns" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "init_ntp" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "wait_for_time_sync" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "wait_for_database" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cluster_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "zones_init" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "nexus_handoff" + ] + } + }, + "required": [ + "status" + ] + } + ] + }, + "Sff8636Datapath": { + "description": "The datapath of an SFF-8636 module.\n\nThis describes the state of a single lane in an SFF module. It includes information about input and output signals, faults, and controls.", + "type": "object", + "properties": { + "rx_cdr_enabled": { + "description": "Media-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "rx_lol": { + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "tx_adaptive_eq_fault": { + "description": "Flag indicating a fault in adaptive transmit equalization.", + "type": "boolean" + }, + "tx_cdr_enabled": { + "description": "Host-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "tx_enabled": { + "description": "Software control of output transmitter.", + "type": "boolean" + }, + "tx_fault": { + "description": "Flag indicating a fault in the transmitter and/or laser.", + "type": "boolean" + }, + "tx_lol": { + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + } + }, + "required": [ + "rx_cdr_enabled", + "rx_lol", + "rx_los", + "tx_adaptive_eq_fault", + "tx_cdr_enabled", + "tx_enabled", + "tx_fault", + "tx_lol", + "tx_los" + ] + }, + "SffComplianceCode": { + "description": "The compliance code for an SFF-8636 module.\n\nThese values record a specification compliance code, from SFF-8636 Table 6-17, or an extended specification compliance code, from SFF-8024 Table 4-4.", + "oneOf": [ + { + "type": "object", + "properties": { + "code": { + "description": "Extended electrical or optical interface codes", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "extended" + ] + } + }, + "required": [ + "code", + "type" + ] + }, + { + "type": "object", + "properties": { + "code": { + "description": "The Ethernet specification implemented by a module.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "ethernet" + ] + } + }, + "required": [ + "code", + "type" + ] + } + ] + }, + "SpComponentCaboose": { + "description": "`SpComponentCaboose`\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"board\", \"git_commit\", \"name\", \"version\" ], \"properties\": { \"board\": { \"type\": \"string\" }, \"epoch\": { \"type\": [ \"string\", \"null\" ] }, \"git_commit\": { \"type\": \"string\" }, \"name\": { \"type\": \"string\" }, \"sign\": { \"type\": [ \"string\", \"null\" ] }, \"version\": { \"type\": \"string\" } } } ```
", + "type": "object", + "properties": { + "board": { + "type": "string" + }, + "epoch": { + "nullable": true, + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sign": { + "nullable": true, + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "board", + "git_commit", + "name", + "version" + ] + }, + "SpComponentInfo": { + "description": "Overview of a single SP component.\n\n
JSON schema\n\n```json { \"description\": \"Overview of a single SP component.\", \"type\": \"object\", \"required\": [ \"capabilities\", \"component\", \"description\", \"device\", \"presence\" ], \"properties\": { \"capabilities\": { \"description\": \"`capabilities` is a bitmask; interpret it via [`gateway_messages::DeviceCapabilities`].\", \"type\": \"integer\", \"format\": \"uint32\", \"minimum\": 0.0 }, \"component\": { \"description\": \"The unique identifier for this component.\", \"type\": \"string\" }, \"description\": { \"description\": \"A human-readable description of the component.\", \"type\": \"string\" }, \"device\": { \"description\": \"The name of the physical device.\", \"type\": \"string\" }, \"presence\": { \"description\": \"Whether or not the component is present, to the best of the SP's ability to judge.\", \"allOf\": [ { \"$ref\": \"#/components/schemas/SpComponentPresence\" } ] }, \"serial_number\": { \"description\": \"The component's serial number, if it has one.\", \"type\": [ \"string\", \"null\" ] } } } ```
", + "type": "object", + "properties": { + "capabilities": { + "description": "`capabilities` is a bitmask; interpret it via [`gateway_messages::DeviceCapabilities`].", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "component": { + "description": "The unique identifier for this component.", + "type": "string" + }, + "description": { + "description": "A human-readable description of the component.", + "type": "string" + }, + "device": { + "description": "The name of the physical device.", + "type": "string" + }, + "presence": { + "description": "Whether or not the component is present, to the best of the SP's ability to judge.", + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentPresence" + } + ] + }, + "serial_number": { + "nullable": true, + "description": "The component's serial number, if it has one.", + "type": "string" + } + }, + "required": [ + "capabilities", + "component", + "description", + "device", + "presence" + ] + }, + "SpComponentPresence": { + "description": "Description of the presence or absence of a component.\n\nThe 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).\n\n
JSON schema\n\n```json { \"description\": \"Description of the presence or absence of a component.\\n\\nThe 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).\", \"oneOf\": [ { \"description\": \"The component is present.\", \"type\": \"string\", \"enum\": [ \"present\" ] }, { \"description\": \"The component is not present.\", \"type\": \"string\", \"enum\": [ \"not_present\" ] }, { \"description\": \"The component is present but in a failed or faulty state.\", \"type\": \"string\", \"enum\": [ \"failed\" ] }, { \"description\": \"The SP is unable to determine the presence of the component.\", \"type\": \"string\", \"enum\": [ \"unavailable\" ] }, { \"description\": \"The SP's attempt to determine the presence of the component timed out.\", \"type\": \"string\", \"enum\": [ \"timeout\" ] }, { \"description\": \"The SP's attempt to determine the presence of the component failed.\", \"type\": \"string\", \"enum\": [ \"error\" ] } ] } ```
", + "oneOf": [ + { + "description": "The component is present.", + "type": "string", + "enum": [ + "present" + ] + }, + { + "description": "The component is not present.", + "type": "string", + "enum": [ + "not_present" + ] + }, + { + "description": "The component is present but in a failed or faulty state.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "The SP is unable to determine the presence of the component.", + "type": "string", + "enum": [ + "unavailable" + ] + }, + { + "description": "The SP's attempt to determine the presence of the component timed out.", + "type": "string", + "enum": [ + "timeout" + ] + }, + { + "description": "The SP's attempt to determine the presence of the component failed.", + "type": "string", + "enum": [ + "error" + ] + } + ] + }, + "SpIdentifier": { + "description": "`SpIdentifier`\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"slot\", \"type\" ], \"properties\": { \"slot\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 }, \"type\": { \"$ref\": \"#/components/schemas/SpType\" } } } ```
", + "type": "object", + "properties": { + "slot": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "type": { + "$ref": "#/components/schemas/SpType" + } + }, + "required": [ + "slot", + "type" + ] + }, + "SpIgnition": { + "description": "State of an ignition target.", + "oneOf": [ + { + "type": "object", + "properties": { + "present": { + "type": "string", + "enum": [ + "no" + ] + } + }, + "required": [ + "present" + ] + }, + { + "type": "object", + "properties": { + "ctrl_detect_0": { + "type": "boolean" + }, + "ctrl_detect_1": { + "type": "boolean" + }, + "flt_a2": { + "description": "Fault from the A2 power domain", + "type": "boolean" + }, + "flt_a3": { + "description": "Fault from the A3 power domain", + "type": "boolean" + }, + "flt_rot": { + "description": "Fault from the RoT", + "type": "boolean" + }, + "flt_sp": { + "description": "Fault from the SP", + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/SpIgnitionSystemType" + }, + "power": { + "type": "boolean" + }, + "present": { + "type": "string", + "enum": [ + "yes" + ] + } + }, + "required": [ + "ctrl_detect_0", + "ctrl_detect_1", + "flt_a2", + "flt_a3", + "flt_rot", + "flt_sp", + "id", + "power", + "present" + ] + } + ] + }, + "SpIgnitionSystemType": { + "oneOf": [ + { + "type": "object", + "properties": { + "system_type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "system_type" + ] + }, + { + "type": "object", + "properties": { + "system_type": { + "type": "string", + "enum": [ + "sidecar" + ] + } + }, + "required": [ + "system_type" + ] + }, + { + "type": "object", + "properties": { + "system_type": { + "type": "string", + "enum": [ + "psc" + ] + } + }, + "required": [ + "system_type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "system_type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "id", + "system_type" + ] + }, + { + "type": "object", + "properties": { + "system_type": { + "type": "string", + "enum": [ + "cosmo" + ] + } + }, + "required": [ + "system_type" + ] + } + ] + }, + "SpInventory": { + "description": "SP-related data", + "type": "object", + "properties": { + "caboose_active": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "caboose_inactive": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpComponentCaboose" + } + ] + }, + "components": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/SpComponentInfo" + } + }, + "id": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "ignition": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpIgnition" + } + ] + }, + "rot": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotInventory" + } + ] + }, + "state": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SpState" + } + ] + } + }, + "required": [ + "id" + ] + }, + "SpState": { + "type": "object", + "properties": { + "base_mac_address": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 6, + "maxItems": 6 + }, + "hubris_archive_id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "power_state": { + "$ref": "#/components/schemas/PowerState2" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "rot": { + "$ref": "#/components/schemas/RotState" + }, + "serial_number": { + "type": "string" + } + }, + "required": [ + "base_mac_address", + "hubris_archive_id", + "model", + "power_state", + "revision", + "rot", + "serial_number" + ] + }, + "SpType": { + "type": "string", + "enum": [ + "sled", + "power", + "switch" + ] + }, + "StartUpdateOptions": { + "type": "object", + "properties": { + "skip_rot_bootloader_version_check": { + "description": "If true, skip the check on the current RoT version and always update it regardless of whether the update appears to be neeeded.", + "type": "boolean" + }, + "skip_rot_version_check": { + "description": "If true, skip the check on the current RoT version and always update it regardless of whether the update appears to be neeeded.", + "type": "boolean" + }, + "skip_sp_version_check": { + "description": "If true, skip the check on the current SP version and always update it regardless of whether the update appears to be neeeded.", + "type": "boolean" + }, + "test_error": { + "nullable": true, + "description": "If passed in, fails the update with a simulated error.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTestError" + } + ] + }, + "test_simulate_rot_bootloader_result": { + "nullable": true, + "description": "If passed in, simulates a result for the RoT Bootloader update.\n\nThis is used for testing.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateSimulatedResult" + } + ] + }, + "test_simulate_rot_result": { + "nullable": true, + "description": "If passed in, simulates a result for the RoT update.\n\nThis is used for testing.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateSimulatedResult" + } + ] + }, + "test_simulate_sp_result": { + "nullable": true, + "description": "If passed in, simulates a result for the SP update.\n\nThis is used for testing.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateSimulatedResult" + } + ] + }, + "test_step_seconds": { + "nullable": true, + "description": "If passed in, creates a test step that lasts these many seconds long.\n\nThis is used for testing.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "skip_rot_bootloader_version_check", + "skip_rot_version_check", + "skip_sp_version_check" + ] + }, + "StartUpdateParams": { + "type": "object", + "properties": { + "options": { + "description": "Options for the update.", + "allOf": [ + { + "$ref": "#/components/schemas/StartUpdateOptions" + } + ] + }, + "targets": { + "description": "The SP identifiers to start the update with. Must be non-empty.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + } + }, + "required": [ + "options", + "targets" + ] + }, + "StepComponentSummaryForGenericSpec": { + "type": "object", + "properties": { + "component": { + "description": "The component." + }, + "total_component_steps": { + "description": "The number of steps present in this component.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "component", + "total_component_steps" + ] + }, + "StepComponentSummaryForWicketdEngineSpec": { + "type": "object", + "properties": { + "component": { + "description": "The component.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateComponent" + } + ] + }, + "total_component_steps": { + "description": "The number of steps present in this component.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "component", + "total_component_steps" + ] + }, + "StepEventForGenericSpec": { + "type": "object", + "properties": { + "data": { + "description": "The kind of event this is.", + "allOf": [ + { + "$ref": "#/components/schemas/StepEventKindForGenericSpec" + } + ] + }, + "event_index": { + "description": "A monotonically increasing index for this `StepEvent`.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "execution_id": { + "description": "The execution ID.", + "type": "string", + "format": "uuid" + }, + "spec": { + "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used along with `Self::from_generic` to identify which specification to deserialize generic metadata against. For example:\n\n```rust,ignore if event.spec == \"MySpec\" { // event is likely generated from a MySpec engine. let event = Event::::from_generic(event)?; // ... } ```", + "type": "string" + }, + "total_elapsed": { + "description": "Total time elapsed since the start of execution.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "data", + "event_index", + "execution_id", + "spec", + "total_elapsed" + ] + }, + "StepEventForWicketdEngineSpec": { + "type": "object", + "properties": { + "data": { + "description": "The kind of event this is.", + "allOf": [ + { + "$ref": "#/components/schemas/StepEventKindForWicketdEngineSpec" + } + ] + }, + "event_index": { + "description": "A monotonically increasing index for this `StepEvent`.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "execution_id": { + "description": "The execution ID.", + "type": "string", + "format": "uuid" + }, + "spec": { + "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used along with `Self::from_generic` to identify which specification to deserialize generic metadata against. For example:\n\n```rust,ignore if event.spec == \"MySpec\" { // event is likely generated from a MySpec engine. let event = Event::::from_generic(event)?; // ... } ```", + "type": "string" + }, + "total_elapsed": { + "description": "Total time elapsed since the start of execution.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "data", + "event_index", + "execution_id", + "spec", + "total_elapsed" + ] + }, + "StepEventKindForGenericSpec": { + "oneOf": [ + { + "description": "No steps were defined, and the executor exited without doing anything.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "no_steps_defined" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "Execution was started.\n\nThis is an initial event -- it is always expected to be the first event received from the event stream.", + "type": "object", + "properties": { + "components": { + "description": "A list of components, along with the number of items each component has.", + "type": "array", + "items": { + "$ref": "#/components/schemas/StepComponentSummaryForGenericSpec" + } + }, + "first_step": { + "description": "Information about the first step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_started" + ] + }, + "steps": { + "description": "The list of steps that will be executed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/StepInfoForGenericSpec" + } + } + }, + "required": [ + "components", + "first_step", + "kind", + "steps" + ] + }, + { + "description": "Progress was reset along an attempt, and this attempt is going down a different path.", + "type": "object", + "properties": { + "attempt": { + "description": "The current attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The amount of time this attempt has taken so far.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "progress_reset" + ] + }, + "message": { + "description": "A message assocaited with the reset.", + "type": "string" + }, + "metadata": { + "description": "Progress-related metadata associated with this attempt." + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "message", + "metadata", + "step", + "step_elapsed" + ] + }, + { + "description": "An attempt failed and this step is being retried.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The amount of time the previous attempt took.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "attempt_retry" + ] + }, + "message": { + "description": "A message associated with the retry.", + "type": "string" + }, + "next_attempt": { + "description": "The attempt number for the next attempt.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt_elapsed", + "kind", + "message", + "next_attempt", + "step", + "step_elapsed" + ] + }, + { + "description": "A step is complete and the next step has been started.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number that completed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "step_completed" + ] + }, + "next_step": { + "description": "The next step that is being started.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "outcome": { + "description": "The outcome of the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepOutcomeForGenericSpec" + } + ] + }, + "step": { + "description": "Information about the step that just completed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "next_step", + "outcome", + "step", + "step_elapsed" + ] + }, + { + "description": "Execution is complete.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_completed" + ] + }, + "last_attempt": { + "description": "The attempt number that completed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "last_outcome": { + "description": "The outcome of the last step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepOutcomeForGenericSpec" + } + ] + }, + "last_step": { + "description": "Information about the last step that completed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt_elapsed", + "kind", + "last_attempt", + "last_outcome", + "last_step", + "step_elapsed" + ] + }, + { + "description": "Execution failed.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "causes": { + "description": "A chain of causes associated with the failure.", + "type": "array", + "items": { + "type": "string" + } + }, + "failed_step": { + "description": "Information about the step that failed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_failed" + ] + }, + "message": { + "description": "A message associated with the failure.", + "type": "string" + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "total_attempts": { + "description": "The total number of attempts that were performed before the step failed.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "attempt_elapsed", + "causes", + "failed_step", + "kind", + "message", + "step_elapsed", + "total_attempts" + ] + }, + { + "description": "Execution aborted by an external user.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "aborted_step": { + "description": "Information about the step that was running at the time execution was aborted.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "attempt": { + "description": "The attempt that was running at the time the step was aborted.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_aborted" + ] + }, + "message": { + "description": "A message associated with the abort.", + "type": "string" + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "aborted_step", + "attempt", + "attempt_elapsed", + "kind", + "message", + "step_elapsed" + ] + }, + { + "description": "A nested step event occurred.", + "type": "object", + "properties": { + "attempt": { + "description": "The current attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "event": { + "description": "The event that occurred.", + "allOf": [ + { + "$ref": "#/components/schemas/StepEventForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "nested" + ] + }, + "step": { + "description": "Information about the step that's occurring.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForGenericSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "event", + "kind", + "step", + "step_elapsed" + ] + }, + { + "description": "Future variants that might be unknown.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "StepEventKindForWicketdEngineSpec": { + "oneOf": [ + { + "description": "No steps were defined, and the executor exited without doing anything.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "no_steps_defined" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "Execution was started.\n\nThis is an initial event -- it is always expected to be the first event received from the event stream.", + "type": "object", + "properties": { + "components": { + "description": "A list of components, along with the number of items each component has.", + "type": "array", + "items": { + "$ref": "#/components/schemas/StepComponentSummaryForWicketdEngineSpec" + } + }, + "first_step": { + "description": "Information about the first step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_started" + ] + }, + "steps": { + "description": "The list of steps that will be executed.", + "type": "array", + "items": { + "$ref": "#/components/schemas/StepInfoForWicketdEngineSpec" + } + } + }, + "required": [ + "components", + "first_step", + "kind", + "steps" + ] + }, + { + "description": "Progress was reset along an attempt, and this attempt is going down a different path.", + "type": "object", + "properties": { + "attempt": { + "description": "The current attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The amount of time this attempt has taken so far.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "progress_reset" + ] + }, + "message": { + "description": "A message assocaited with the reset.", + "type": "string" + }, + "metadata": { + "description": "Progress-related metadata associated with this attempt." + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "message", + "metadata", + "step", + "step_elapsed" + ] + }, + { + "description": "An attempt failed and this step is being retried.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The amount of time the previous attempt took.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "attempt_retry" + ] + }, + "message": { + "description": "A message associated with the retry.", + "type": "string" + }, + "next_attempt": { + "description": "The attempt number for the next attempt.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "step": { + "description": "Information about the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt_elapsed", + "kind", + "message", + "next_attempt", + "step", + "step_elapsed" + ] + }, + { + "description": "A step is complete and the next step has been started.", + "type": "object", + "properties": { + "attempt": { + "description": "The attempt number that completed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "step_completed" + ] + }, + "next_step": { + "description": "The next step that is being started.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "outcome": { + "description": "The outcome of the step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepOutcomeForWicketdEngineSpec" + } + ] + }, + "step": { + "description": "Information about the step that just completed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "kind", + "next_step", + "outcome", + "step", + "step_elapsed" + ] + }, + { + "description": "Execution is complete.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_completed" + ] + }, + "last_attempt": { + "description": "The attempt number that completed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "last_outcome": { + "description": "The outcome of the last step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepOutcomeForWicketdEngineSpec" + } + ] + }, + "last_step": { + "description": "Information about the last step that completed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt_elapsed", + "kind", + "last_attempt", + "last_outcome", + "last_step", + "step_elapsed" + ] + }, + { + "description": "Execution failed.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "causes": { + "description": "A chain of causes associated with the failure.", + "type": "array", + "items": { + "type": "string" + } + }, + "failed_step": { + "description": "Information about the step that failed.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_failed" + ] + }, + "message": { + "description": "A message associated with the failure.", + "type": "string" + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "total_attempts": { + "description": "The total number of attempts that were performed before the step failed.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "attempt_elapsed", + "causes", + "failed_step", + "kind", + "message", + "step_elapsed", + "total_attempts" + ] + }, + { + "description": "Execution aborted by an external user.\n\nThis is a terminal event: it is guaranteed that no more events will be seen after this one.", + "type": "object", + "properties": { + "aborted_step": { + "description": "Information about the step that was running at the time execution was aborted.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "attempt": { + "description": "The attempt that was running at the time the step was aborted.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "execution_aborted" + ] + }, + "message": { + "description": "A message associated with the abort.", + "type": "string" + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "aborted_step", + "attempt", + "attempt_elapsed", + "kind", + "message", + "step_elapsed" + ] + }, + { + "description": "A nested step event occurred.", + "type": "object", + "properties": { + "attempt": { + "description": "The current attempt number.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "attempt_elapsed": { + "description": "The time it took for this attempt to complete.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "event": { + "description": "The event that occurred.", + "allOf": [ + { + "$ref": "#/components/schemas/StepEventForGenericSpec" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "nested" + ] + }, + "step": { + "description": "Information about the step that's occurring.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoWithMetadataForWicketdEngineSpec" + } + ] + }, + "step_elapsed": { + "description": "Total time elapsed since the start of the step. Includes prior attempts.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + } + }, + "required": [ + "attempt", + "attempt_elapsed", + "event", + "kind", + "step", + "step_elapsed" + ] + }, + { + "description": "Future variants that might be unknown.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "kind" + ] + } + ] + }, + "StepInfoForGenericSpec": { + "description": "Serializable information about a step.", + "type": "object", + "properties": { + "component": { + "description": "The component that this step is part of." + }, + "component_index": { + "description": "The index of the step within the component.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "description": { + "description": "The description for this step.", + "type": "string" + }, + "id": { + "description": "An identifier for this step." + }, + "index": { + "description": "The index of the step within all steps to be executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "total_component_steps": { + "description": "The total number of steps in this component.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "component", + "component_index", + "description", + "id", + "index", + "total_component_steps" + ] + }, + "StepInfoForWicketdEngineSpec": { + "description": "Serializable information about a step.", + "type": "object", + "properties": { + "component": { + "description": "The component that this step is part of.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateComponent" + } + ] + }, + "component_index": { + "description": "The index of the step within the component.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "description": { + "description": "The description for this step.", + "type": "string" + }, + "id": { + "description": "An identifier for this step.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateStepId" + } + ] + }, + "index": { + "description": "The index of the step within all steps to be executed.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "total_component_steps": { + "description": "The total number of steps in this component.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "component", + "component_index", + "description", + "id", + "index", + "total_component_steps" + ] + }, + "StepInfoWithMetadataForGenericSpec": { + "description": "Serializable information about a step.", + "type": "object", + "properties": { + "info": { + "description": "Information about this step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoForGenericSpec" + } + ] + }, + "metadata": { + "nullable": true, + "description": "Additional metadata associated with this step." + } + }, + "required": [ + "info" + ] + }, + "StepInfoWithMetadataForWicketdEngineSpec": { + "description": "Serializable information about a step.", + "type": "object", + "properties": { + "info": { + "description": "Information about this step.", + "allOf": [ + { + "$ref": "#/components/schemas/StepInfoForWicketdEngineSpec" + } + ] + }, + "metadata": { + "nullable": true, + "description": "Additional metadata associated with this step." + } + }, + "required": [ + "info" + ] + }, + "StepOutcomeForGenericSpec": { + "oneOf": [ + { + "description": "The step completed successfully.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "success" + ] + }, + "message": { + "nullable": true, + "description": "An optional message associated with this step.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional completion metadata associated with the step." + } + }, + "required": [ + "kind" + ] + }, + { + "description": "The step completed with a warning.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "warning" + ] + }, + "message": { + "description": "A warning message.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional completion metadata associated with the step." + } + }, + "required": [ + "kind", + "message" + ] + }, + { + "description": "The step was skipped with a message.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "skipped" + ] + }, + "message": { + "description": "Message associated with the skip.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional metadata associated with the skip." + } + }, + "required": [ + "kind", + "message" + ] + } + ] + }, + "StepOutcomeForWicketdEngineSpec": { + "oneOf": [ + { + "description": "The step completed successfully.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "success" + ] + }, + "message": { + "nullable": true, + "description": "An optional message associated with this step.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional completion metadata associated with the step." + } + }, + "required": [ + "kind" + ] + }, + { + "description": "The step completed with a warning.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "warning" + ] + }, + "message": { + "description": "A warning message.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional completion metadata associated with the step." + } + }, + "required": [ + "kind", + "message" + ] + }, + { + "description": "The step was skipped with a message.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "skipped" + ] + }, + "message": { + "description": "Message associated with the skip.", + "type": "string" + }, + "metadata": { + "nullable": true, + "description": "Optional metadata associated with the skip." + } + }, + "required": [ + "kind", + "message" + ] + } + ] + }, + "Transceiver": { + "description": "A transceiver in the switch's front ports.", + "type": "object", + "properties": { + "datapath": { + "description": "Status of the transceiver's machinery for carrying data, the \"datapath\".", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/Datapath" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/Datapath" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "monitors": { + "description": "Environmental monitoring data, such as temperature or optical power.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/Monitors" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/Monitors" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "port": { + "description": "The port in which the transceiver sits.", + "type": "string" + }, + "power": { + "description": "Information about the power state of the transceiver.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/PowerMode" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/PowerMode" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "status": { + "description": "The general status of the transceiver, such as presence and faults.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/ExtendedStatus" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/ExtendedStatus" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "vendor": { + "description": "Details about the vendor, part number, and serial number.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/VendorInfo" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/VendorInfo" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "datapath", + "monitors", + "port", + "power", + "status", + "vendor" + ] + }, + "TransceiverInventorySnapshot": { + "description": "A snapshot of the transceiver inventory at a given time.", + "type": "object", + "properties": { + "inventory": { + "description": "The transceivers in each switch.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transceiver" + } + } + }, + "last_seen": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "inventory", + "last_seen" + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UpdateComponent": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "rot_bootloader" + ] + } + }, + "required": [ + "component" + ] + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "rot" + ] + } + }, + "required": [ + "component" + ] + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "sp" + ] + } + }, + "required": [ + "component" + ] + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "host" + ] + } + }, + "required": [ + "component" + ] + } + ] + }, + "UpdateSimulatedResult": { + "description": "A simulated result for a component update.\n\nUsed by [`StartUpdateOptions`].", + "type": "string", + "enum": [ + "success", + "warning", + "skipped", + "failure" + ] + }, + "UpdateStepId": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "test_step" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "set_host_power_state" + ] + }, + "state": { + "$ref": "#/components/schemas/PowerState" + } + }, + "required": [ + "id", + "state" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "interrogate_rot_bootloader" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "interrogate_rot" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "interrogate_sp" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "sp_component_update" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "setting_installinator_image_id" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "clearing_installinator_image_id" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "setting_host_startup_options" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "waiting_for_trampoline_phase2_upload" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "fetch_host_type" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "check_host_type" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "downloading_installinator" + ] + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "running_installinator" + ] + } + }, + "required": [ + "id" + ] + } + ] + }, + "UpdateTestError": { + "oneOf": [ + { + "description": "Simulate an error where the operation fails to complete.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "fail" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "Simulate an issue where the operation times out.", + "type": "object", + "properties": { + "content": { + "type": "object", + "properties": { + "secs": { + "description": "The number of seconds to time out after.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "secs" + ] + }, + "kind": { + "type": "string", + "enum": [ + "timeout" + ] + } + }, + "required": [ + "content", + "kind" + ] + } + ] + }, + "UserSpecifiedBgpPeerConfig": { + "description": "User-specified version of [`BgpPeerConfig`].\n\nThis is similar to [`BgpPeerConfig`], except it doesn't have the sensitive `md5_auth_key` parameter, instead requiring that the user provide the key separately.", + "type": "object", + "properties": { + "addr": { + "description": "Address of the peer.", + "allOf": [ + { + "$ref": "#/components/schemas/UserSpecifiedRouterPeerAddr" + } + ] + }, + "allowed_export": { + "description": "Apply export policy to this peer with an allow list.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/UserSpecifiedImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Apply import policy to this peer with an allow list.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/UserSpecifiedImportExportPolicy" + } + ] + }, + "asn": { + "description": "The autonomous sysetm number of the router the peer belongs to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "auth_key_id": { + "nullable": true, + "description": "The key identifier for authentication to use with the peer.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/BgpAuthKeyId" + } + ] + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "default": [], + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "nullable": true, + "description": "The interval in seconds between peer connection retry attempts. Defaults to 3 seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "nullable": true, + "description": "How long to delay sending open messages to a peer in seconds. Defaults to 0.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "default": false, + "type": "boolean" + }, + "hold_time": { + "nullable": true, + "description": "How long to keep a session alive without a keepalive in seconds. Defaults to 6 seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_time": { + "nullable": true, + "description": "How long to keep a peer in idle after a state machine reset in seconds. Defaults to 3 seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "nullable": true, + "description": "The interval to send keepalive messages at, in seconds. Defaults to 2 seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Switch port the peer is reachable on.", + "type": "string" + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "router_lifetime": { + "description": "Router lifetime in seconds for unnumbered BGP peers.", + "default": 0, + "allOf": [ + { + "$ref": "#/components/schemas/RouterLifetimeConfig" + } + ] + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a BGP peer session.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "asn", + "port" + ], + "additionalProperties": false + }, + "UserSpecifiedImportExportPolicy": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + }, + "UserSpecifiedPortConfig": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManualPortConfig" + }, + { + "type": "object" + } + ] + }, + "UserSpecifiedRackNetworkConfig": { + "description": "User-specified parts of `RackNetworkConfig`.", + "type": "object", + "properties": { + "bgp": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "infra_ip_first": { + "type": "string", + "format": "ip" + }, + "infra_ip_last": { + "type": "string", + "format": "ip" + }, + "rack_subnet_address": { + "nullable": true, + "type": "string", + "format": "ipv6" + }, + "switch0": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/UserSpecifiedPortConfig" + } + }, + "switch1": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/UserSpecifiedPortConfig" + } + } + }, + "required": [ + "bgp", + "infra_ip_first", + "infra_ip_last", + "switch0", + "switch1" + ], + "additionalProperties": false + }, + "UserSpecifiedRouterPeerAddr": { + "type": "string" + }, + "UserSpecifiedUplinkAddressConfig": { + "description": "User-specified version of [`sled_agent_types::early_networking::UplinkAddressConfig`].\n\nThis allows us to have a nicer TOML representation of [`UplinkAddress`].", + "type": "object", + "properties": { + "address": { + "description": "The address to be used on the uplink.", + "type": "string" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ], + "additionalProperties": false + }, + "Vendor": { + "description": "Vendor-specific information about a transceiver module.", + "type": "object", + "properties": { + "date": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "oui": { + "$ref": "#/components/schemas/Oui" + }, + "part": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "name", + "oui", + "part", + "revision", + "serial" + ] + }, + "VendorInfo": { + "description": "The vendor information for a transceiver module.", + "type": "object", + "properties": { + "identifier": { + "description": "The SFF-8024 identifier.", + "type": "string" + }, + "vendor": { + "description": "The vendor information.", + "allOf": [ + { + "$ref": "#/components/schemas/Vendor" + } + ] + } + }, + "required": [ + "identifier", + "vendor" + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/wicketd-commission/wicketd-commission-latest.json b/openapi/wicketd-commission/wicketd-commission-latest.json new file mode 120000 index 00000000000..6425b591610 --- /dev/null +++ b/openapi/wicketd-commission/wicketd-commission-latest.json @@ -0,0 +1 @@ +wicketd-commission-1.0.0-caad21.json \ No newline at end of file diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index 7829036953a..4d343d5847d 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -68,6 +68,7 @@ use omicron_common::address::NTP_ADMIN_PORT; use omicron_common::address::RACK_PREFIX; use omicron_common::address::SLED_PREFIX; use omicron_common::address::TFPORTD_PORT; +use omicron_common::address::WICKETD_COMMISSION_PORT; use omicron_common::address::WICKETD_NEXUS_PROXY_PORT; use omicron_common::address::WICKETD_PORT; use omicron_common::address::{BOOTSTRAP_ARTIFACT_PORT, COCKROACH_ADMIN_PORT}; @@ -2675,6 +2676,11 @@ impl ServiceManager { "astring", &format!("[::1]:{WICKETD_PORT}"), ) + .add_property( + "commission-address", + "astring", + &format!("[::1]:{WICKETD_COMMISSION_PORT}"), + ) .add_property( "artifact-address", "astring", diff --git a/smf/wicketd/manifest.xml b/smf/wicketd/manifest.xml index 349d7b5a57f..d73e8b8991b 100644 --- a/smf/wicketd/manifest.xml +++ b/smf/wicketd/manifest.xml @@ -17,7 +17,7 @@ @@ -46,6 +46,7 @@ + diff --git a/wicketd-commission-api/Cargo.toml b/wicketd-commission-api/Cargo.toml new file mode 100644 index 00000000000..9d5307d4f36 --- /dev/null +++ b/wicketd-commission-api/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wicketd-commission-api" +version = "0.1.0" +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +bootstrap-agent-lockstep-client.workspace = true +dropshot.workspace = true +dropshot-api-manager-types.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +wicket-common.workspace = true +wicketd-commission-types-versions.workspace = true diff --git a/wicketd-commission-api/src/lib.rs b/wicketd-commission-api/src/lib.rs new file mode 100644 index 00000000000..f78e4dc10ae --- /dev/null +++ b/wicketd-commission-api/src/lib.rs @@ -0,0 +1,217 @@ +// 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/. + +//! The commission API: a subset of the wicketd API used by external tools +//! (such as rkdeploy) to drive rack commissioning. + +use bootstrap_agent_lockstep_client::types::RackOperationStatus; +use dropshot::HttpError; +use dropshot::HttpResponseOk; +use dropshot::HttpResponseUpdatedNoContent; +use dropshot::RequestContext; +use dropshot::StreamingBody; +use dropshot::TypedBody; +use dropshot_api_manager_types::api_versions; +use omicron_uuid_kinds::RackInitUuid; +use wicket_common::rack_setup::PutRssUserConfigInsensitive; +use wicketd_commission_types_versions::latest; + +api_versions!([(1, INITIAL)]); + +/// Full release repositories are currently (Dec 2024) 1.8 GiB and are likely to +/// continue growing. +const PUT_REPOSITORY_MAX_BYTES: usize = 4 * 1024 * 1024 * 1024; + +#[dropshot::api_description] +pub trait WicketdCommissionApi { + type Context; + + /// Get wicketd's current view of all sleds visible on the bootstrap network. + #[endpoint { + method = GET, + path = "/bootstrap-sleds", + versions = VERSION_INITIAL.., + }] + async fn get_bootstrap_sleds( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Get the current status of the user-provided (or system-default-provided, in + /// some cases) RSS configuration. + #[endpoint { + method = GET, + path = "/rack-setup/config", + versions = VERSION_INITIAL.., + }] + async fn get_rss_config( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Update (a subset of) the current RSS configuration. + /// + /// Sensitive values (certificates and password hash) are not set through + /// this endpoint. + #[endpoint { + method = PUT, + path = "/rack-setup/config", + versions = VERSION_INITIAL.., + }] + async fn put_rss_config( + rqctx: RequestContext, + body: TypedBody, + ) -> Result; + + /// Add an external certificate. + /// + /// This must be paired with its private key. They may be posted in either + /// order, but one cannot post two certs in a row (or two keys in a row). + #[endpoint { + method = POST, + path = "/rack-setup/config/cert", + versions = VERSION_INITIAL.., + }] + async fn post_rss_config_cert( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError>; + + /// Add the private key of an external certificate. + /// + /// This must be paired with its certificate. They may be posted in either + /// order, but one cannot post two keys in a row (or two certs in a row). + #[endpoint { + method = POST, + path = "/rack-setup/config/key", + versions = VERSION_INITIAL.., + }] + async fn post_rss_config_key( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError>; + + /// Update the RSS config recovery silo user password hash. + #[endpoint { + method = PUT, + path = "/rack-setup/config/recovery-user-password-hash", + versions = VERSION_INITIAL.., + }] + async fn put_rss_config_recovery_user_password_hash( + rqctx: RequestContext, + body: TypedBody, + ) -> Result; + + /// Reset all RSS configuration to their default values. + #[endpoint { + method = DELETE, + path = "/rack-setup/config", + versions = VERSION_INITIAL.., + }] + async fn delete_rss_config( + rqctx: RequestContext, + ) -> Result; + + /// Query current state of rack setup. + #[endpoint { + method = GET, + path = "/rack-setup", + versions = VERSION_INITIAL.., + }] + async fn get_rack_setup_state( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Run rack setup. + /// + /// Will return an error if not all of the rack setup configuration has + /// been populated. + #[endpoint { + method = POST, + path = "/rack-setup", + versions = VERSION_INITIAL.., + }] + async fn post_run_rack_setup( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// A status endpoint used to report high level information known to + /// wicketd. + /// + /// This endpoint can be polled to see if there have been state changes in + /// the system that are useful to report to wicket. + /// + /// Wicket, and possibly other callers, will retrieve the changed + /// information, with follow up calls. + #[endpoint { + method = GET, + path = "/inventory", + versions = VERSION_INITIAL.., + }] + async fn get_inventory( + rqctx: RequestContext, + body_params: TypedBody, + ) -> Result, HttpError>; + + /// Upload a TUF repository to the server. + /// + /// At any given time, wicketd will keep at most one TUF repository in + /// memory. Any previously-uploaded repositories will be discarded. + #[endpoint { + method = PUT, + path = "/repository", + request_body_max_bytes = PUT_REPOSITORY_MAX_BYTES, + versions = VERSION_INITIAL.., + }] + async fn put_repository( + rqctx: RequestContext, + body: StreamingBody, + ) -> Result; + + /// An endpoint used to report all available artifacts and event reports. + /// + /// The order of the returned artifacts is unspecified, and may change between + /// calls even if the total set of artifacts has not. + #[endpoint { + method = GET, + path = "/artifacts-and-event-reports", + versions = VERSION_INITIAL.., + }] + async fn get_artifacts_and_event_reports( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Report the identity of the sled and switch we're currently running on / + /// connected to. + #[endpoint { + method = GET, + path = "/location", + versions = VERSION_INITIAL.., + }] + async fn get_location( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// An endpoint to start updating one or more sleds, switches and PSCs. + #[endpoint { + method = POST, + path = "/update", + versions = VERSION_INITIAL.., + }] + async fn post_start_update( + rqctx: RequestContext, + params: TypedBody, + ) -> Result; + + /// Resets update state for a sled. + /// + /// Use this to clear update state after a failed update. + #[endpoint { + method = POST, + path = "/clear-update-state", + versions = VERSION_INITIAL.., + }] + async fn post_clear_update_state( + rqctx: RequestContext, + params: TypedBody, + ) -> Result, HttpError>; +} diff --git a/wicketd-commission-types-versions/Cargo.toml b/wicketd-commission-types-versions/Cargo.toml new file mode 100644 index 00000000000..ddea6b82b1b --- /dev/null +++ b/wicketd-commission-types-versions/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wicketd-commission-types-versions" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +bootstrap-agent-lockstep-client.workspace = true +omicron-common.workspace = true +omicron-passwords.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +semver.workspace = true +serde.workspace = true +sled-hardware-types.workspace = true +tufaceous-artifact.workspace = true +wicket-common.workspace = true diff --git a/wicketd-commission-types-versions/src/initial/artifacts.rs b/wicketd-commission-types-versions/src/initial/artifacts.rs new file mode 100644 index 00000000000..79d36ac7d3a --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/artifacts.rs @@ -0,0 +1,44 @@ +// 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::collections::BTreeMap; + +use omicron_common::update::ArtifactId; +use schemars::JsonSchema; +use semver::Version; +use serde::Serialize; +use tufaceous_artifact::ArtifactHashId; +use wicket_common::inventory::SpType; +use wicket_common::update_events::EventReport; + +#[derive(Clone, Debug, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct InstallableArtifacts { + pub artifact_id: ArtifactId, + pub installable: Vec, + pub sign: Option>, +} + +/// The response to a `get_artifacts` call: the system version, and the list of +/// all artifacts currently held by wicketd. +#[derive(Clone, Debug, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GetArtifactsAndEventReportsResponse { + pub system_version: Option, + + /// Map of artifacts we ingested from the most-recently-uploaded TUF + /// repository to a list of artifacts we're serving over the bootstrap + /// network. In some cases the list of artifacts being served will have + /// length 1 (when we're serving the artifact directly); in other cases the + /// artifact in the TUF repo contains multiple nested artifacts inside it + /// (e.g., RoT artifacts contain both A and B images), and we serve the list + /// of extracted artifacts but not the original combination. + /// + /// Conceptually, this is a `BTreeMap>`, but + /// JSON requires string keys for maps, so we give back a vec of pairs + /// instead. + pub artifacts: Vec, + + pub event_reports: BTreeMap>, +} diff --git a/wicketd-commission-types-versions/src/initial/bootstrap_sleds.rs b/wicketd-commission-types-versions/src/initial/bootstrap_sleds.rs new file mode 100644 index 00000000000..727e0a64b31 --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/bootstrap_sleds.rs @@ -0,0 +1,40 @@ +// 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::net::Ipv6Addr; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::Baseboard; + +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +pub struct BootstrapSledIp { + pub baseboard: Baseboard, + pub ip: Ipv6Addr, +} + +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +pub struct BootstrapSledIps { + pub sleds: Vec, +} diff --git a/wicketd-commission-types-versions/src/initial/inventory.rs b/wicketd-commission-types-versions/src/initial/inventory.rs new file mode 100644 index 00000000000..8adc397f126 --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/inventory.rs @@ -0,0 +1,23 @@ +// 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 wicket_common::inventory::{RackV1Inventory, SpIdentifier}; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct GetInventoryParams { + /// Refresh the state of these SPs from MGS prior to returning (instead of + /// returning cached data). + pub force_refresh: Vec, +} + +/// The response to a `get_inventory` call: the inventory known to wicketd, or a +/// notification that data is unavailable. +#[derive(Clone, Debug, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum GetInventoryResponse { + Response { inventory: RackV1Inventory }, + Unavailable, +} diff --git a/wicketd-commission-types-versions/src/initial/location.rs b/wicketd-commission-types-versions/src/initial/location.rs new file mode 100644 index 00000000000..b91f324e27f --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/location.rs @@ -0,0 +1,25 @@ +// 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::Serialize; +use sled_hardware_types::Baseboard; +use wicket_common::inventory::SpIdentifier; + +/// All the fields of this response are optional, because it's possible we don't +/// know any of them (yet) if MGS has not yet finished discovering its location +/// or (ever) if we're running in a dev environment that doesn't support +/// MGS-location / baseboard mapping. +#[derive(Clone, Debug, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GetLocationResponse { + /// The identity of our sled (where wicketd is running). + pub sled_id: Option, + /// The baseboard of our sled (where wicketd is running). + pub sled_baseboard: Option, + /// The baseboard of the switch our sled is physically connected to. + pub switch_baseboard: Option, + /// The identity of the switch our sled is physically connected to. + pub switch_id: Option, +} diff --git a/wicketd-commission-types-versions/src/initial/mod.rs b/wicketd-commission-types-versions/src/initial/mod.rs new file mode 100644 index 00000000000..17951db049a --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/mod.rs @@ -0,0 +1,12 @@ +// 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/. + +//! Version `INITIAL` of the Wicketd Commission API. + +pub mod artifacts; +pub mod bootstrap_sleds; +pub mod inventory; +pub mod location; +pub mod rss_config; +pub mod update; diff --git a/wicketd-commission-types-versions/src/initial/rss_config.rs b/wicketd-commission-types-versions/src/initial/rss_config.rs new file mode 100644 index 00000000000..9ff89c68f07 --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/rss_config.rs @@ -0,0 +1,47 @@ +// 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 wicket_common::rack_setup::CurrentRssUserConfigInsensitive; +use wicket_common::rack_setup::GetBgpAuthKeyInfoResponse; + +// This is a summary of the subset of `RackInitializeRequest` that is sensitive; +// we only report a summary instead of returning actual data. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct CurrentRssUserConfigSensitive { + pub num_external_certificates: usize, + pub recovery_silo_password_set: bool, + // We define GetBgpAuthKeyInfoResponse in wicket-common and use a + // progenitor replace directive for it, because we don't want typify to + // turn the BTreeMap into a HashMap. Use the same struct here to piggyback + // on that. + pub bgp_auth_keys: GetBgpAuthKeyInfoResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct CurrentRssUserConfig { + pub sensitive: CurrentRssUserConfigSensitive, + pub insensitive: CurrentRssUserConfigInsensitive, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum CertificateUploadResponse { + /// The key has been uploaded, but we're waiting on its corresponding + /// certificate chain. + WaitingOnCert, + /// The cert chain has been uploaded, but we're waiting on its corresponding + /// private key. + WaitingOnKey, + /// A cert chain and its key have been accepted. + CertKeyAccepted, + /// A cert chain and its key are valid, but have already been uploaded. + CertKeyDuplicateIgnored, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct PutRssRecoveryUserPasswordHash { + pub hash: omicron_passwords::NewPasswordHash, +} diff --git a/wicketd-commission-types-versions/src/initial/update.rs b/wicketd-commission-types-versions/src/initial/update.rs new file mode 100644 index 00000000000..d2010800bdc --- /dev/null +++ b/wicketd-commission-types-versions/src/initial/update.rs @@ -0,0 +1,28 @@ +// 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::collections::BTreeSet; + +use schemars::JsonSchema; +use serde::Deserialize; +use wicket_common::inventory::SpIdentifier; +use wicket_common::rack_update::{ClearUpdateStateOptions, StartUpdateOptions}; + +#[derive(Clone, Debug, JsonSchema, Deserialize)] +pub struct StartUpdateParams { + /// The SP identifiers to start the update with. Must be non-empty. + pub targets: BTreeSet, + + /// Options for the update. + pub options: StartUpdateOptions, +} + +#[derive(Clone, Debug, JsonSchema, Deserialize)] +pub struct ClearUpdateStateParams { + /// The SP identifiers to clear the update state for. Must be non-empty. + pub targets: BTreeSet, + + /// Options for clearing update state + pub options: ClearUpdateStateOptions, +} diff --git a/wicketd-commission-types-versions/src/latest.rs b/wicketd-commission-types-versions/src/latest.rs new file mode 100644 index 00000000000..0b6ba34ef29 --- /dev/null +++ b/wicketd-commission-types-versions/src/latest.rs @@ -0,0 +1,29 @@ +// 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/. + +//! Re-exports of the latest versions of all published types. + +pub mod artifacts { + pub use crate::v1::artifacts::*; +} + +pub mod bootstrap_sleds { + pub use crate::v1::bootstrap_sleds::*; +} + +pub mod inventory { + pub use crate::v1::inventory::*; +} + +pub mod location { + pub use crate::v1::location::*; +} + +pub mod rss_config { + pub use crate::v1::rss_config::*; +} + +pub mod update { + pub use crate::v1::update::*; +} diff --git a/wicketd-commission-types-versions/src/lib.rs b/wicketd-commission-types-versions/src/lib.rs new file mode 100644 index 00000000000..b966b9d153f --- /dev/null +++ b/wicketd-commission-types-versions/src/lib.rs @@ -0,0 +1,34 @@ +// 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/. + +//! Versioned types for the Wicketd Commission API. +//! +//! # Adding a new API version +//! +//! When adding a new API version N with added or changed types: +//! +//! 1. Create `/mod.rs`, where `` is the lowercase +//! form of the new version's identifier, as defined in the API trait's +//! `api_versions!` macro. +//! +//! 2. Add to the end of this list: +//! +//! ```rust,ignore +//! #[path = "/mod.rs"] +//! pub mod vN; +//! ``` +//! +//! 3. Add your types to the new module, mirroring the module structure from +//! earlier versions. +//! +//! 4. Update `latest.rs` with new and updated types from the new version. +//! +//! For more information, see the [detailed guide] and [RFD 619]. +//! +//! [detailed guide]: https://github.com/oxidecomputer/dropshot-api-manager/blob/main/guides/new-version.md +//! [RFD 619]: https://rfd.shared.oxide.computer/rfd/619 + +pub mod latest; +#[path = "initial/mod.rs"] +pub mod v1; diff --git a/wicketd-commission-types/Cargo.toml b/wicketd-commission-types/Cargo.toml new file mode 100644 index 00000000000..c3d86a433e5 --- /dev/null +++ b/wicketd-commission-types/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wicketd-commission-types" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +omicron-workspace-hack.workspace = true +wicketd-commission-types-versions.workspace = true diff --git a/wicketd-commission-types/src/artifacts.rs b/wicketd-commission-types/src/artifacts.rs new file mode 100644 index 00000000000..865ddc92fbe --- /dev/null +++ b/wicketd-commission-types/src/artifacts.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::artifacts::*; diff --git a/wicketd-commission-types/src/bootstrap_sleds.rs b/wicketd-commission-types/src/bootstrap_sleds.rs new file mode 100644 index 00000000000..5528a43c8db --- /dev/null +++ b/wicketd-commission-types/src/bootstrap_sleds.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::bootstrap_sleds::*; diff --git a/wicketd-commission-types/src/inventory.rs b/wicketd-commission-types/src/inventory.rs new file mode 100644 index 00000000000..fb76e0cfe17 --- /dev/null +++ b/wicketd-commission-types/src/inventory.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::inventory::*; diff --git a/wicketd-commission-types/src/lib.rs b/wicketd-commission-types/src/lib.rs new file mode 100644 index 00000000000..5f0790ee4fd --- /dev/null +++ b/wicketd-commission-types/src/lib.rs @@ -0,0 +1,20 @@ +// 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 the Wicketd Commission API. +//! +//! This crate re-exports the latest versions of all types from the +//! `wicketd-commission-types-versions` crate. These are floating identifiers +//! that should be used by business logic that doesn't need to care about API +//! versioning. +//! +//! The API crate (`wicketd-commission-api`) uses fixed identifiers from the +//! versions crate directly. + +pub mod artifacts; +pub mod bootstrap_sleds; +pub mod inventory; +pub mod location; +pub mod rss_config; +pub mod update; diff --git a/wicketd-commission-types/src/location.rs b/wicketd-commission-types/src/location.rs new file mode 100644 index 00000000000..968274c95c3 --- /dev/null +++ b/wicketd-commission-types/src/location.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::location::*; diff --git a/wicketd-commission-types/src/rss_config.rs b/wicketd-commission-types/src/rss_config.rs new file mode 100644 index 00000000000..828c4acaee6 --- /dev/null +++ b/wicketd-commission-types/src/rss_config.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::rss_config::*; diff --git a/wicketd-commission-types/src/update.rs b/wicketd-commission-types/src/update.rs new file mode 100644 index 00000000000..50ea764ae4f --- /dev/null +++ b/wicketd-commission-types/src/update.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::update::*; diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index e7976a18de4..382f75befbb 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -73,6 +73,8 @@ update-engine.workspace = true wicket-common.workspace = true wicketd-api.workspace = true wicketd-client.workspace = true +wicketd-commission-api.workspace = true +wicketd-commission-types.workspace = true omicron-workspace-hack.workspace = true semver.workspace = true slog-error-chain.workspace = true diff --git a/wicketd/src/bin/wicketd.rs b/wicketd/src/bin/wicketd.rs index 042df085f67..76e8a615a51 100644 --- a/wicketd/src/bin/wicketd.rs +++ b/wicketd/src/bin/wicketd.rs @@ -28,6 +28,11 @@ enum Args { #[clap(short, long, action)] address: SocketAddrV6, + /// The address on which the wicketd "commission" dropshot server should + /// listen. + #[clap(long, action)] + commission_address: SocketAddrV6, + /// The address on the bootstrap network to serve artifacts at #[clap(long, action)] artifact_address: SocketAddrV6, @@ -83,6 +88,7 @@ async fn do_run() -> Result<(), CmdError> { Args::Run { config_file_path, address, + commission_address, artifact_address, mgs_address, nexus_proxy_address, @@ -126,6 +132,7 @@ async fn do_run() -> Result<(), CmdError> { let args = wicketd::Args { address, + commission_address, artifact_address, mgs_address, nexus_proxy_address, diff --git a/wicketd/src/commission_http_entrypoints.rs b/wicketd/src/commission_http_entrypoints.rs new file mode 100644 index 00000000000..7fe55d9ccd7 --- /dev/null +++ b/wicketd/src/commission_http_entrypoints.rs @@ -0,0 +1,685 @@ +// 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 entrypoint functions for the wicketd "commission" API. +//! +//! This API is a stable version of the wicketd API that is used by external +//! commissioning tools (such as `rkdeploy`). It is versioned independently of +//! the main wicketd API. + +use crate::ServerContext; +use crate::context::CommonConfigContainer; +use crate::helpers::SpIdentifierDisplay; +use crate::helpers::sps_to_string; +use crate::mgs::GetInventoryError as GetMgsInventoryError; +use crate::mgs::GetInventoryResponse as GetMgsInventoryResponse; +use crate::mgs::MgsHandle; +use crate::mgs::ShutdownInProgress; +use crate::transceivers::GetTransceiversResponse; +use bootstrap_agent_lockstep_client::types::RackOperationStatus; +use dropshot::ApiDescription; +use dropshot::HttpError; +use dropshot::HttpResponseOk; +use dropshot::HttpResponseUpdatedNoContent; +use dropshot::RequestContext; +use dropshot::StreamingBody; +use dropshot::TypedBody; +use omicron_uuid_kinds::RackInitUuid; +use sled_hardware_types::Baseboard; +use slog_error_chain::InlineErrorChain; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::sync::Arc; +use wicket_common::WICKETD_TIMEOUT; +use wicket_common::inventory::MgsV1Inventory; +use wicket_common::inventory::MgsV1InventorySnapshot; +use wicket_common::inventory::RackV1Inventory; +use wicket_common::inventory::SpType; +use wicket_common::inventory::TransceiverInventorySnapshot; +use wicket_common::rack_setup::PutRssUserConfigInsensitive; +use wicket_common::rack_update::ClearUpdateStateResponse; +use wicketd_commission_api::WicketdCommissionApi; +use wicketd_commission_api::wicketd_commission_api_mod; +use wicketd_commission_types::artifacts::GetArtifactsAndEventReportsResponse; +use wicketd_commission_types::artifacts::InstallableArtifacts; +use wicketd_commission_types::bootstrap_sleds::BootstrapSledIp; +use wicketd_commission_types::bootstrap_sleds::BootstrapSledIps; +use wicketd_commission_types::inventory::GetInventoryParams; +use wicketd_commission_types::inventory::GetInventoryResponse; +use wicketd_commission_types::location::GetLocationResponse; +use wicketd_commission_types::rss_config::CertificateUploadResponse; +use wicketd_commission_types::rss_config::CurrentRssUserConfig; +use wicketd_commission_types::rss_config::CurrentRssUserConfigSensitive; +use wicketd_commission_types::rss_config::PutRssRecoveryUserPasswordHash; +use wicketd_commission_types::update::ClearUpdateStateParams; +use wicketd_commission_types::update::StartUpdateParams; + +type CommissionApiDescription = ApiDescription>; + +/// Return a description of the wicketd commission API for use in generating +/// an OpenAPI spec. +pub fn api() -> CommissionApiDescription { + wicketd_commission_api_mod::api_description::() + .expect("failed to register commission entrypoints") +} + +pub enum WicketdCommissionApiImpl {} + +impl WicketdCommissionApi for WicketdCommissionApiImpl { + type Context = Arc; + + async fn get_bootstrap_sleds( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + + let sleds = ctx + .bootstrap_peers + .sleds() + .into_iter() + .map(|(baseboard, ip)| BootstrapSledIp { baseboard, ip }) + .collect(); + + Ok(HttpResponseOk(BootstrapSledIps { sleds })) + } + + async fn get_rss_config( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + + // We can't run RSS if we don't have an inventory from MGS yet; we + // always need to fill in the bootstrap sleds first. + let inventory = mgs_inventory_or_unavail(&ctx.mgs_handle).await?; + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + let rss_config = config.rss_config_mut_or_conflict( + "cannot get RSS config when not preparing for RSS", + )?; + + let ddm_discovered_sleds = &ctx.bootstrap_peers.sleds(); + let config = + rss_config.get_latest(&inventory, &ddm_discovered_sleds, &ctx.log); + let converted: wicketd_api::CurrentRssUserConfig = config.into(); + + Ok(HttpResponseOk(CurrentRssUserConfig { + sensitive: CurrentRssUserConfigSensitive { + num_external_certificates: converted + .sensitive + .num_external_certificates, + recovery_silo_password_set: converted + .sensitive + .recovery_silo_password_set, + bgp_auth_keys: converted.sensitive.bgp_auth_keys, + }, + insensitive: converted.insensitive, + })) + } + + async fn put_rss_config( + rqctx: RequestContext, + body: TypedBody, + ) -> Result { + let ctx = rqctx.context(); + + let inventory = mgs_inventory_or_unavail(&ctx.mgs_handle).await?; + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + + // Overwrite any non-rss config + let rss_config = config.rss_config_mut_or_default(); + + let ddm_discovered_sleds = &ctx.bootstrap_peers.sleds(); + rss_config + .update( + body.into_inner(), + ctx.baseboard.as_ref(), + &inventory, + &ddm_discovered_sleds, + &ctx.log, + ) + .map_err(|err| HttpError::for_bad_request(None, err))?; + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn post_rss_config_cert( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + + let rss_config = config.rss_config_mut_or_conflict( + "cannot post certificates when not preparing for RSS", + )?; + + let response = rss_config + .push_cert(body.into_inner()) + .map_err(|err| HttpError::for_bad_request(None, err))?; + + Ok(HttpResponseOk(convert_cert_upload_response(response))) + } + + async fn post_rss_config_key( + rqctx: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + let rss_config = config.rss_config_mut_or_conflict( + "cannot post private keys when not preparing for RSS", + )?; + + let response = rss_config + .push_key(body.into_inner()) + .map_err(|err| HttpError::for_bad_request(None, err))?; + + Ok(HttpResponseOk(convert_cert_upload_response(response))) + } + + async fn put_rss_config_recovery_user_password_hash( + rqctx: RequestContext, + body: TypedBody, + ) -> Result { + let ctx = rqctx.context(); + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + + let rss_config = config.rss_config_mut_or_conflict( + "cannot put recovery user password when not preparing for RSS", + )?; + + rss_config.set_recovery_user_password_hash(body.into_inner().hash); + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn delete_rss_config( + rqctx: RequestContext, + ) -> Result { + let ctx = rqctx.context(); + + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + let rss_config = config.rss_config_mut_or_conflict( + "cannot delete RSS config when not preparing for RSS", + )?; + + *rss_config = Default::default(); + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn get_rack_setup_state( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + + let lockstep_addr = + ctx.bootstrap_agent_lockstep_addr().map_err(|err| { + HttpError::for_bad_request(None, format!("{err:#}")) + })?; + + let client = bootstrap_agent_lockstep_client::Client::new( + &format!("http://{}", lockstep_addr), + ctx.log.new(slog::o!("component" => "bootstrap lockstep client")), + ); + + let op_status = client + .rack_initialization_status() + .await + .map_err(|err| { + use bootstrap_agent_lockstep_client::Error as BaError; + match err { + BaError::CommunicationError(err) => { + let message = format!( + "Failed to send rack setup request: {}", + InlineErrorChain::new(&err) + ); + HttpError { + status_code: + dropshot::ErrorStatusCode::SERVICE_UNAVAILABLE, + error_code: None, + external_message: message.clone(), + internal_message: message, + headers: None, + } + } + other => HttpError::for_bad_request( + None, + format!("Rack setup request failed: {other}"), + ), + } + })? + .into_inner(); + + Ok(HttpResponseOk(op_status)) + } + + async fn post_run_rack_setup( + rqctx: RequestContext, + ) -> Result, HttpError> { + let ctx = rqctx.context(); + let log = &rqctx.log; + + let lockstep_addr = + ctx.bootstrap_agent_lockstep_addr().map_err(|err| { + HttpError::for_bad_request(None, format!("{err:#}")) + })?; + + let request = { + let mut config = ctx.rss_or_multirack_join_config.lock().unwrap(); + + let rss_config = config.rss_config_mut_or_conflict( + "cannot run rack setup when not preparing for RSS", + )?; + + rss_config.start_rss_request(&ctx.bootstrap_peers, log).map_err( + |err| HttpError::for_bad_request(None, format!("{err:#}")), + )? + }; + + slog::info!( + ctx.log, + "Sending RSS initialize request to {}", + lockstep_addr + ); + let client = bootstrap_agent_lockstep_client::Client::new( + &format!("http://{}", lockstep_addr), + ctx.log.new(slog::o!("component" => "bootstrap lockstep client")), + ); + + let init_id = client + .rack_initialize(&request) + .await + .map_err(|err| { + use bootstrap_agent_lockstep_client::Error as BaError; + match err { + BaError::CommunicationError(err) => { + let message = format!( + "Failed to send rack setup request: {}", + InlineErrorChain::new(&err) + ); + HttpError { + status_code: + dropshot::ErrorStatusCode::SERVICE_UNAVAILABLE, + error_code: None, + external_message: message.clone(), + internal_message: message, + headers: None, + } + } + other => HttpError::for_bad_request( + None, + format!("Rack setup request failed: {other}"), + ), + } + })? + .into_inner(); + + Ok(HttpResponseOk(init_id)) + } + + async fn get_inventory( + rqctx: RequestContext, + body_params: TypedBody, + ) -> Result, HttpError> { + let GetInventoryParams { force_refresh } = body_params.into_inner(); + + // Fetch the MGS-specific inventory first. + let maybe_mgs_inventory = match rqctx + .context() + .mgs_handle + .get_inventory_refreshing_sps(force_refresh) + .await + { + Ok(GetMgsInventoryResponse::Response { + inventory, + mgs_last_seen, + }) => Some((inventory, mgs_last_seen)), + Ok(GetMgsInventoryResponse::Unavailable) => None, + Err(GetMgsInventoryError::InvalidSpIdentifier) => { + return Err(HttpError::for_unavail( + None, + "Invalid SP identifier in request".into(), + )); + } + Err(GetMgsInventoryError::ShutdownInProgress) => { + return Err(HttpError::for_unavail( + None, + "Server is shutting down".into(), + )); + } + }; + + // Fetch the transceiver information from the SP. + let maybe_transceiver_inventory = + match rqctx.context().transceiver_handle.get_transceivers() { + GetTransceiversResponse::Response { + transceivers, + transceivers_last_seen, + } => Some((transceivers, transceivers_last_seen)), + GetTransceiversResponse::Unavailable => None, + }; + + if maybe_mgs_inventory.is_none() + && maybe_transceiver_inventory.is_none() + { + return Err(HttpError::for_unavail( + None, + "Rack inventory not yet available".into(), + )); + } + let mgs = maybe_mgs_inventory.map(|(inventory, last_seen)| { + MgsV1InventorySnapshot { inventory, last_seen } + }); + let transceivers = + maybe_transceiver_inventory.map(|(inventory, last_seen)| { + TransceiverInventorySnapshot { inventory, last_seen } + }); + let inventory = RackV1Inventory { mgs, transceivers }; + Ok(HttpResponseOk(GetInventoryResponse::Response { inventory })) + } + + async fn put_repository( + rqctx: RequestContext, + body: StreamingBody, + ) -> Result { + let rqctx = rqctx.context(); + + rqctx.update_tracker.put_repository(body.into_stream()).await?; + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn get_artifacts_and_event_reports( + rqctx: RequestContext, + ) -> Result, HttpError> + { + let response = + rqctx.context().update_tracker.artifacts_and_event_reports().await; + Ok(HttpResponseOk(GetArtifactsAndEventReportsResponse { + system_version: response.system_version, + artifacts: response + .artifacts + .into_iter() + .map(|a| InstallableArtifacts { + artifact_id: a.artifact_id, + installable: a.installable, + sign: a.sign, + }) + .collect(), + event_reports: response.event_reports, + })) + } + + async fn get_location( + rqctx: RequestContext, + ) -> Result, HttpError> { + let rqctx = rqctx.context(); + let inventory = mgs_inventory_or_unavail(&rqctx.mgs_handle).await?; + + let switch_id = rqctx.local_switch_id().await; + let sled_baseboard = rqctx.baseboard.clone(); + + let mut switch_baseboard = None; + let mut sled_id = None; + + for sp in &inventory.sps { + if Some(sp.id) == switch_id { + switch_baseboard = sp.state.as_ref().map(|state| { + Baseboard::new_gimlet( + state.serial_number.clone(), + state.model.clone(), + state.revision, + ) + }); + } else if let (Some(sled_baseboard), Some(state)) = + (sled_baseboard.as_ref(), sp.state.as_ref()) + { + if sled_baseboard.identifier() == state.serial_number + && sled_baseboard.model() == state.model + && sled_baseboard.revision() == state.revision + { + sled_id = Some(sp.id); + } + } + } + + Ok(HttpResponseOk(GetLocationResponse { + sled_id, + sled_baseboard, + switch_baseboard, + switch_id, + })) + } + + async fn post_start_update( + rqctx: RequestContext, + params: TypedBody, + ) -> Result { + let log = &rqctx.log; + let rqctx = rqctx.context(); + let params = params.into_inner(); + + if params.targets.is_empty() { + return Err(HttpError::for_bad_request( + None, + "No update targets specified".into(), + )); + } + + let inventory = match tokio::time::timeout( + WICKETD_TIMEOUT.mul_f32(0.8), + rqctx.mgs_handle.get_cached_inventory(), + ) + .await + { + Ok(Ok(inventory)) => inventory, + Ok(Err(ShutdownInProgress)) => { + return Err(HttpError::for_unavail( + None, + "Server is shutting down".into(), + )); + } + Err(_) => { + let message = + "Rack inventory not yet available (is MGS alive?)" + .to_owned(); + return Err(HttpError { + status_code: dropshot::ErrorStatusCode::SERVICE_UNAVAILABLE, + error_code: None, + external_message: message.clone(), + internal_message: message, + headers: None, + }); + } + }; + + let mut inventory_absent = BTreeSet::new(); + let mut self_update = None; + let mut maybe_self_update = BTreeSet::new(); + + let sp_states = match inventory { + GetMgsInventoryResponse::Response { inventory, .. } => inventory + .sps + .into_iter() + .filter_map(|sp| { + if params.targets.contains(&sp.id) { + if let Some(sp_state) = sp.state { + Some((sp.id, sp_state)) + } else { + None + } + } else { + None + } + }) + .collect(), + GetMgsInventoryResponse::Unavailable => BTreeMap::new(), + }; + + for target in ¶ms.targets { + let sp_state = match sp_states.get(target) { + Some(sp_state) => sp_state, + None => { + inventory_absent.insert(*target); + continue; + } + }; + + match &rqctx.baseboard { + Some(baseboard) => { + if baseboard.identifier() == sp_state.serial_number + && baseboard.model() == sp_state.model + && baseboard.revision() == sp_state.revision + { + self_update = Some(*target); + continue; + } + } + None => { + let target_is_scrimlet = matches!( + (target.type_, target.slot), + (SpType::Sled, 14 | 16) + ); + if target_is_scrimlet { + maybe_self_update.insert(*target); + continue; + } + } + } + } + + let mut errors = Vec::new(); + if !inventory_absent.is_empty() { + errors.push(format!( + "cannot update sleds (no inventory state present for {})", + sps_to_string(&inventory_absent) + )); + } + if let Some(self_update) = self_update { + errors.push(format!( + "cannot update sled where wicketd is running ({})", + SpIdentifierDisplay(self_update) + )); + } + if !maybe_self_update.is_empty() { + errors.push(format!( + "wicketd does not know its own baseboard details: \ + refusing to update either scrimlet ({})", + sps_to_string(&inventory_absent) + )); + } + + if let Some(test_error) = ¶ms.options.test_error { + errors.push( + test_error.into_error_string(log, "starting update").await, + ); + } + + let start_update_errors = if errors.is_empty() { + match rqctx + .update_tracker + .start(params.targets, params.options) + .await + { + Ok(()) => return Ok(HttpResponseUpdatedNoContent {}), + Err(errors) => errors, + } + } else { + match rqctx.update_tracker.update_pre_checks(params.targets).await { + Ok(()) => Vec::new(), + Err(errors) => errors, + } + }; + + errors + .extend(start_update_errors.iter().map(|error| error.to_string())); + + match errors.len() { + 0 => { + unreachable!( + "we already returned Ok(_) above if there were no errors" + ) + } + 1 => Err(HttpError::for_bad_request(None, errors.pop().unwrap())), + _ => Err(HttpError::for_bad_request( + None, + format!( + "multiple errors encountered:\n - {}", + itertools::join(errors, "\n - ") + ), + )), + } + } + + async fn post_clear_update_state( + rqctx: RequestContext, + params: TypedBody, + ) -> Result, HttpError> { + let log = &rqctx.log; + let rqctx = rqctx.context(); + let params = params.into_inner(); + + if params.targets.is_empty() { + return Err(HttpError::for_bad_request( + None, + "No targets specified".into(), + )); + } + + if let Some(test_error) = params.options.test_error { + return Err(test_error + .into_http_error(log, "clearing update state") + .await); + } + + match rqctx.update_tracker.clear_update_state(params.targets).await { + Ok(response) => Ok(HttpResponseOk(response)), + Err(err) => Err(err.to_http_error()), + } + } +} + +// Get the current inventory or return a 503 Unavailable. +async fn mgs_inventory_or_unavail( + mgs_handle: &MgsHandle, +) -> Result { + match mgs_handle.get_cached_inventory().await { + Ok(GetMgsInventoryResponse::Response { inventory, .. }) => { + Ok(inventory) + } + Ok(GetMgsInventoryResponse::Unavailable) => { + Err(HttpError::for_unavail( + None, + "Rack inventory not yet available".into(), + )) + } + Err(ShutdownInProgress) => { + Err(HttpError::for_unavail(None, "Server is shutting down".into())) + } + } +} + +fn convert_cert_upload_response( + resp: wicketd_api::CertificateUploadResponse, +) -> CertificateUploadResponse { + match resp { + wicketd_api::CertificateUploadResponse::WaitingOnCert => { + CertificateUploadResponse::WaitingOnCert + } + wicketd_api::CertificateUploadResponse::WaitingOnKey => { + CertificateUploadResponse::WaitingOnKey + } + wicketd_api::CertificateUploadResponse::CertKeyAccepted => { + CertificateUploadResponse::CertKeyAccepted + } + wicketd_api::CertificateUploadResponse::CertKeyDuplicateIgnored => { + CertificateUploadResponse::CertKeyDuplicateIgnored + } + } +} diff --git a/wicketd/src/http_entrypoints.rs b/wicketd/src/http_entrypoints.rs index f80020ba968..a4f03568f26 100644 --- a/wicketd/src/http_entrypoints.rs +++ b/wicketd/src/http_entrypoints.rs @@ -33,6 +33,7 @@ use slog::o; use slog_error_chain::InlineErrorChain; use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::sync::Arc; use wicket_common::WICKETD_TIMEOUT; use wicket_common::inventory::MgsV1Inventory; use wicket_common::inventory::MgsV1InventorySnapshot; @@ -51,7 +52,7 @@ use wicketd_api::*; use crate::ServerContext; -type WicketdApiDescription = ApiDescription; +type WicketdApiDescription = ApiDescription>; /// Return a description of the wicketd api for use in generating an OpenAPI spec pub fn api() -> WicketdApiDescription { @@ -62,7 +63,7 @@ pub fn api() -> WicketdApiDescription { pub enum WicketdApiImpl {} impl WicketdApi for WicketdApiImpl { - type Context = ServerContext; + type Context = Arc; async fn get_bootstrap_sleds( rqctx: RequestContext, @@ -234,7 +235,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_bgp_auth_key_info( - rqctx: RequestContext, + rqctx: RequestContext>, // A bit weird for a GET request to have a TypedBody, but there's no other // nice way to transmit this information as a batch. params: TypedBody, @@ -252,7 +253,7 @@ impl WicketdApi for WicketdApiImpl { } async fn put_bgp_auth_key( - rqctx: RequestContext, + rqctx: RequestContext>, params: Path, body: TypedBody, ) -> Result, HttpError> { @@ -268,7 +269,7 @@ impl WicketdApi for WicketdApiImpl { } async fn put_rss_config_recovery_user_password_hash( - rqctx: RequestContext, + rqctx: RequestContext>, body: TypedBody, ) -> Result { let ctx = rqctx.context(); @@ -285,7 +286,7 @@ impl WicketdApi for WicketdApiImpl { } async fn delete_rss_config( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result { let ctx = rqctx.context(); @@ -300,7 +301,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_rack_setup_state( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let ctx = rqctx.context(); @@ -346,7 +347,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_run_rack_setup( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let ctx = rqctx.context(); let log = &rqctx.log; @@ -410,7 +411,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_run_rack_reset( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let ctx = rqctx.context(); @@ -457,7 +458,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_inventory( - rqctx: RequestContext, + rqctx: RequestContext>, body_params: TypedBody, ) -> Result, HttpError> { let GetInventoryParams { force_refresh } = body_params.into_inner(); @@ -520,7 +521,7 @@ impl WicketdApi for WicketdApiImpl { } async fn put_repository( - rqctx: RequestContext, + rqctx: RequestContext>, body: StreamingBody, ) -> Result { let rqctx = rqctx.context(); @@ -531,7 +532,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_artifacts_and_event_reports( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let response = @@ -540,7 +541,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_baseboard( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let rqctx = rqctx.context(); Ok(HttpResponseOk(GetBaseboardResponse { @@ -549,7 +550,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_location( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result, HttpError> { let rqctx = rqctx.context(); let inventory = mgs_inventory_or_unavail(&rqctx.mgs_handle).await?; @@ -594,7 +595,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_start_update( - rqctx: RequestContext, + rqctx: RequestContext>, params: TypedBody, ) -> Result { let log = &rqctx.log; @@ -794,7 +795,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_update_sp( - rqctx: RequestContext, + rqctx: RequestContext>, target: Path, ) -> Result, HttpError> { let event_report = rqctx @@ -806,7 +807,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_abort_update( - rqctx: RequestContext, + rqctx: RequestContext>, target: Path, opts: TypedBody, ) -> Result { @@ -832,7 +833,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_clear_update_state( - rqctx: RequestContext, + rqctx: RequestContext>, params: TypedBody, ) -> Result, HttpError> { let log = &rqctx.log; @@ -859,7 +860,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_ignition_command( - rqctx: RequestContext, + rqctx: RequestContext>, path: Path, ) -> Result { let apictx = rqctx.context(); @@ -875,7 +876,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_start_preflight_uplink_check( - rqctx: RequestContext, + rqctx: RequestContext>, body: TypedBody, ) -> Result { let rqctx = rqctx.context(); @@ -950,7 +951,7 @@ impl WicketdApi for WicketdApiImpl { } async fn get_preflight_uplink_report( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result< HttpResponseOk, HttpError, @@ -968,7 +969,7 @@ impl WicketdApi for WicketdApiImpl { } async fn post_reload_config( - rqctx: RequestContext, + rqctx: RequestContext>, ) -> Result { let smf_values = SmfConfigValues::read_current().map_err(|err| { HttpError::for_unavail( diff --git a/wicketd/src/lib.rs b/wicketd/src/lib.rs index 38cb9e698eb..772ad734ac2 100644 --- a/wicketd/src/lib.rs +++ b/wicketd/src/lib.rs @@ -5,6 +5,7 @@ mod artifacts; mod bgp_auth_keys; mod bootstrap_addrs; +pub mod commission_http_entrypoints; mod config; mod context; mod helpers; @@ -52,6 +53,7 @@ use wicketd_client::ClientInfo as _; /// Command line arguments for wicketd pub struct Args { pub address: SocketAddrV6, + pub commission_address: SocketAddrV6, pub artifact_address: SocketAddrV6, pub mgs_address: SocketAddrV6, pub nexus_proxy_address: SocketAddrV6, @@ -61,6 +63,7 @@ pub struct Args { pub struct SmfConfigValues { pub address: SocketAddrV6, + pub commission_address: SocketAddrV6, pub rack_subnet: Option>, } @@ -72,6 +75,7 @@ impl SmfConfigValues { const CONFIG_PG: &str = "config"; const PROP_RACK_SUBNET: &str = "rack-subnet"; const PROP_ADDRESS: &str = "address"; + const PROP_COMMISSION_ADDRESS: &str = "commission-address"; let scf = ScfHandle::new()?; let instance = scf.self_instance()?; @@ -102,7 +106,17 @@ impl SmfConfigValues { })? }; - Ok(Self { address, rack_subnet }) + let commission_address = { + let addr = config.value_as_string(PROP_COMMISSION_ADDRESS)?; + addr.parse().with_context(|| { + format!( + "failed to parse {CONFIG_PG}/{PROP_COMMISSION_ADDRESS} \ + value {addr:?} as a socket address" + ) + })? + }; + + Ok(Self { address, commission_address, rack_subnet }) } #[cfg(not(target_os = "illumos"))] @@ -112,7 +126,8 @@ impl SmfConfigValues { } pub struct Server { - pub wicketd_server: HttpServer, + pub wicketd_server: HttpServer>, + pub commission_server: HttpServer>, pub installinator_server: HttpServer, pub artifact_store: WicketdArtifactStore, pub update_tracker: Arc, @@ -191,32 +206,51 @@ impl Server { anyhow!(err).context("failed to start Nexus TCP proxy") })?; + let server_context = Arc::new(ServerContext { + bind_address: args.address, + mgs_handle, + mgs_client: make_mgs_client(log.clone(), args.mgs_address), + transceiver_handle, + log: log.clone(), + local_switch_id: OnceLock::new(), + bootstrap_peers, + update_tracker: update_tracker.clone(), + baseboard: args.baseboard, + rss_or_multirack_join_config: Default::default(), + preflight_checker: PreflightCheckerHandler::new(&log), + internal_dns_resolver, + }); + let wicketd_server = { let ds_log = log.new(o!("component" => "dropshot (wicketd)")); - let mgs_client = make_mgs_client(log.clone(), args.mgs_address); dropshot::ServerBuilder::new( http_entrypoints::api(), - ServerContext { - bind_address: args.address, - mgs_handle, - mgs_client, - transceiver_handle, - log: log.clone(), - local_switch_id: OnceLock::new(), - bootstrap_peers, - update_tracker: update_tracker.clone(), - baseboard: args.baseboard, - rss_or_multirack_join_config: Default::default(), - preflight_checker: PreflightCheckerHandler::new(&log), - internal_dns_resolver, - }, + Arc::clone(&server_context), ds_log, ) - .config(dropshot_config) + .config(dropshot_config.clone()) .start() .map_err(|err| anyhow!(err).context("initializing http server"))? }; + let commission_server = { + let ds_log = + log.new(o!("component" => "dropshot (wicketd-commission)")); + let mut commission_config = dropshot_config; + commission_config.bind_address = + SocketAddr::V6(args.commission_address); + dropshot::ServerBuilder::new( + commission_http_entrypoints::api(), + Arc::clone(&server_context), + ds_log, + ) + .config(commission_config) + .start() + .map_err(|err| { + anyhow!(err).context("initializing commission http server") + })? + }; + let installinator_server = { let installinator_config = installinator_api::default_config( SocketAddr::V6(args.artifact_address), @@ -251,6 +285,7 @@ impl Server { Ok(Self { wicketd_server, + commission_server, installinator_server, artifact_store: store, update_tracker, @@ -264,6 +299,9 @@ impl Server { self.wicketd_server.close().await.map_err(|error| { anyhow!("error closing wicketd server: {error}") })?; + self.commission_server.close().await.map_err(|error| { + anyhow!("error closing commission server: {error}") + })?; self.installinator_server.close().await.map_err(|error| { anyhow!("error closing artifact server: {error}") })?; @@ -272,8 +310,8 @@ impl Server { } pub async fn wait_for_finish(self) -> Result<(), String> { - // Both servers should keep running indefinitely unless close() is - // called. Bail if either server exits. + // All servers should keep running indefinitely unless close() is + // called. Bail if any server exits. tokio::select! { res = self.wicketd_server => { match res { @@ -281,6 +319,12 @@ impl Server { Err(err) => Err(format!("running wicketd server: {err}")), } } + res = self.commission_server => { + match res { + Ok(()) => Err("commission server exited unexpectedly".to_owned()), + Err(err) => Err(format!("running commission server: {err}")), + } + } res = self.installinator_server => { match res { Ok(()) => Err("artifact server exited unexpectedly".to_owned()), diff --git a/wicketd/tests/integration_tests/setup.rs b/wicketd/tests/integration_tests/setup.rs index 5b8db1fde8f..70f0dbf9c02 100644 --- a/wicketd/tests/integration_tests/setup.rs +++ b/wicketd/tests/integration_tests/setup.rs @@ -41,6 +41,7 @@ impl WicketdTestContext { let args = wicketd::Args { address: LOCALHOST_PORT_0, + commission_address: LOCALHOST_PORT_0, artifact_address: LOCALHOST_PORT_0, mgs_address, nexus_proxy_address: LOCALHOST_PORT_0, From e224a248b9feb915cf14a19726737c16e97be17e Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Wed, 8 Jul 2026 23:04:21 +0000 Subject: [PATCH 2/2] fmt --- wicketd-commission-api/src/lib.rs | 35 ++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/wicketd-commission-api/src/lib.rs b/wicketd-commission-api/src/lib.rs index f78e4dc10ae..9fdfed2497b 100644 --- a/wicketd-commission-api/src/lib.rs +++ b/wicketd-commission-api/src/lib.rs @@ -35,7 +35,10 @@ pub trait WicketdCommissionApi { }] async fn get_bootstrap_sleds( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Get the current status of the user-provided (or system-default-provided, in /// some cases) RSS configuration. @@ -46,7 +49,10 @@ pub trait WicketdCommissionApi { }] async fn get_rss_config( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Update (a subset of) the current RSS configuration. /// @@ -74,7 +80,10 @@ pub trait WicketdCommissionApi { async fn post_rss_config_cert( rqctx: RequestContext, body: TypedBody, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Add the private key of an external certificate. /// @@ -88,7 +97,10 @@ pub trait WicketdCommissionApi { async fn post_rss_config_key( rqctx: RequestContext, body: TypedBody, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Update the RSS config recovery silo user password hash. #[endpoint { @@ -150,7 +162,10 @@ pub trait WicketdCommissionApi { async fn get_inventory( rqctx: RequestContext, body_params: TypedBody, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Upload a TUF repository to the server. /// @@ -178,7 +193,10 @@ pub trait WicketdCommissionApi { }] async fn get_artifacts_and_event_reports( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Report the identity of the sled and switch we're currently running on / /// connected to. @@ -213,5 +231,8 @@ pub trait WicketdCommissionApi { async fn post_clear_update_state( rqctx: RequestContext, params: TypedBody, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; }