From 33a080e107b9cf81bf6614abfb5ce91bd527c783 Mon Sep 17 00:00:00 2001 From: finch Date: Sat, 20 Dec 2025 19:35:04 -0500 Subject: [PATCH 1/9] Adds version 13 (`ADD_TRUST_QUORUM`) to the Sled Agent API The following endpoints are created for trust quorum reconfiguration: - POST `/trust-quorum/reconfigure` - Initiate a reconfiguration - POST `/trust-quorum/upgrade-from-lrtq` - Upgrade from low-rent (legacy) trust quorum - POST `/trust-quorum/commit` - Commit a trust-quorum - GET `/trust-quorum/coordinator-status` - Get coordinator status - POST `/trust-quorum/prepare-and-commit` - Prepare and commit a configuration Types are organized per RFD 619 (via feeding Claude the RFD): - API types defined in `sled-agent-types-versions/src/add_trust_quorum/` - Re-exported via `latest.rs` and `sled-agent-types/src/trust_quorum.rs` - API trait uses `latest::` paths for all trust quorum types Also exports `EncryptedRackSecrets`, `Salt`, and `Sha3_256Digest` from `trust-quorum-protocol` for use in the `prepare_and_commit` handler. Co-Authored-By: Claude Opus 4.5 --- .../sled-agent/sled-agent-13.0.0-015e5e.json | 9693 +++++++++++++++++ openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 68 + sled-agent/src/http_entrypoints.rs | 186 + sled-agent/src/sim/http_entrypoints.rs | 40 + sled-agent/src/sled_agent.rs | 8 + sled-agent/types/src/lib.rs | 1 + sled-agent/types/src/trust_quorum.rs | 7 + .../versions/src/add_trust_quorum/mod.rs | 9 + .../src/add_trust_quorum/trust_quorum.rs | 101 + sled-agent/types/versions/src/latest.rs | 11 + sled-agent/types/versions/src/lib.rs | 2 + trust-quorum/protocol/src/lib.rs | 6 +- 13 files changed, 10131 insertions(+), 3 deletions(-) create mode 100644 openapi/sled-agent/sled-agent-13.0.0-015e5e.json create mode 100644 sled-agent/types/src/trust_quorum.rs create mode 100644 sled-agent/types/versions/src/add_trust_quorum/mod.rs create mode 100644 sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs diff --git a/openapi/sled-agent/sled-agent-13.0.0-015e5e.json b/openapi/sled-agent/sled-agent-13.0.0-015e5e.json new file mode 100644 index 00000000000..e48731c3b37 --- /dev/null +++ b/openapi/sled-agent/sled-agent-13.0.0-015e5e.json @@ -0,0 +1,9693 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Sled Agent API", + "description": "API for interacting with individual sleds", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "13.0.0" + }, + "paths": { + "/artifacts": { + "get": { + "operationId": "artifact_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactListResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}": { + "put": { + "operationId": "artifact_put", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactPutResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}/copy-from-depot": { + "post": { + "operationId": "artifact_copy_from_depot", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotBody" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts-config": { + "get": { + "operationId": "artifact_config_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "artifact_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bootstore/status": { + "get": { + "summary": "Get the internal state of the local bootstore node", + "operationId": "bootstore_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootstoreStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/debug/switch-zone-policy": { + "get": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.", + "operationId": "debug_operator_switch_zone_policy_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.\n\nSetting the switch zone policy is asynchronous and inherently racy with the standard process of starting the switch zone. If the switch zone is in the process of being started or stopped when this policy is changed, the new policy may not take effect until that transition completes.", + "operationId": "debug_operator_switch_zone_policy_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk_id}": { + "put": { + "operationId": "disk_put", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskRuntimeState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/eip-gateways": { + "put": { + "summary": "Update per-NIC IP address <-> internet gateway mappings.", + "operationId": "set_eip_gateways", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIpGatewayMap" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/inventory": { + "get": { + "summary": "Fetch basic information about this sled", + "operationId": "inventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Inventory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/local-storage/{zpool_id}/{dataset_id}": { + "post": { + "summary": "Create a local storage dataset", + "operationId": "local_storage_dataset_ensure", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalStorageDatasetEnsureRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a local storage dataset", + "operationId": "local_storage_dataset_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/network-bootstore-config": { + "get": { + "summary": "This API endpoint is only reading the local sled agent's view of the", + "description": "bootstore. The boostore is a distributed data store that is eventually consistent. Reads from individual nodes may not represent the latest state.", + "operationId": "read_network_bootstore_config_cache", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "write_network_bootstore_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/omicron-config": { + "put": { + "operationId": "omicron_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OmicronSledConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/probes": { + "put": { + "summary": "Update the entire set of probe zones on this sled.", + "description": "Probe zones are used to debug networking configuration. They look similar to instances, in that they have an OPTE port on a VPC subnet and external addresses, but no actual VM.", + "operationId": "probes_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeSet" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sled-identifiers": { + "get": { + "summary": "Fetch sled identifiers", + "operationId": "sled_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sleds": { + "put": { + "summary": "Add a sled to a rack that was already initialized via RSS", + "operationId": "sled_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSledRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/dladm-info": { + "get": { + "operationId": "support_dladm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/health-check": { + "get": { + "operationId": "support_health_check", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/ipadm-info": { + "get": { + "operationId": "support_ipadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/logs/download/{zone}": { + "get": { + "summary": "This endpoint returns a zip file of a zone's logs organized by service.", + "operationId": "support_logs_download", + "parameters": [ + { + "in": "path", + "name": "zone", + "description": "The zone for which one would like to collect logs for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max_rotated", + "description": "The max number of rotated logs to include in the final support bundle", + "required": true, + "schema": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support/logs/zones": { + "get": { + "summary": "This endpoint returns a list of known zones on a sled that have service", + "description": "logs that can be collected into a support bundle.", + "operationId": "support_logs", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/nvmeadm-info": { + "get": { + "operationId": "support_nvmeadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pargs-info": { + "get": { + "operationId": "support_pargs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pfiles-info": { + "get": { + "operationId": "support_pfiles_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pstack-info": { + "get": { + "operationId": "support_pstack_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zfs-info": { + "get": { + "operationId": "support_zfs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zoneadm-info": { + "get": { + "operationId": "support_zoneadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zpool-info": { + "get": { + "operationId": "support_zpool_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}": { + "get": { + "summary": "List all support bundles within a particular dataset", + "operationId": "support_bundle_list", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SupportBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}": { + "post": { + "summary": "Starts creation of a support bundle within a particular dataset", + "description": "Callers should transfer chunks of the bundle with \"support_bundle_transfer\", and then call \"support_bundle_finalize\" once the bundle has finished transferring.\n\nIf a support bundle was previously created without being finalized successfully, this endpoint will reset the state.\n\nIf a support bundle was previously created and finalized successfully, this endpoint will return metadata indicating that it already exists.", + "operationId": "support_bundle_start_creation", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a support bundle from a particular dataset", + "operationId": "support_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download": { + "get": { + "summary": "Fetch a support bundle from a particular dataset", + "operationId": "support_bundle_download", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a support bundle from a particular dataset", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}": { + "get": { + "summary": "Fetch a file within a support bundle from a particular dataset", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a file within a support bundle from a particular dataset", + "operationId": "support_bundle_head_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/finalize": { + "post": { + "summary": "Finalizes the creation of a support bundle", + "description": "If the requested hash matched the bundle, the bundle is created. Otherwise, an error is returned.", + "operationId": "support_bundle_finalize", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "hash", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index": { + "get": { + "summary": "Fetch the index (list of files within a support bundle)", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about the list of files within a support bundle", + "operationId": "support_bundle_head_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/transfer": { + "put": { + "summary": "Transfers a chunk of a support bundle within a particular dataset", + "operationId": "support_bundle_transfer", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "offset", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch-ports": { + "post": { + "operationId": "uplink_ensure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPorts" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/commit": { + "post": { + "summary": "Commit a trust quorum configuration", + "operationId": "trust_quorum_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCommitResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/coordinator-status": { + "get": { + "summary": "Get the coordinator status if this node is coordinating a reconfiguration", + "operationId": "trust_quorum_coordinator_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCoordinatorStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/prepare-and-commit": { + "post": { + "summary": "Attempt to prepare and commit a trust quorum configuration", + "operationId": "trust_quorum_prepare_and_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumPrepareAndCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCommitResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/reconfigure": { + "post": { + "summary": "Initiate a trust quorum reconfiguration", + "operationId": "trust_quorum_reconfigure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumReconfigureRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/upgrade-from-lrtq": { + "post": { + "summary": "Initiate an upgrade from LRTQ", + "operationId": "trust_quorum_upgrade_from_lrtq", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumLrtqUpgradeRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v2p": { + "get": { + "summary": "List v2p mappings present on sled", + "operationId": "list_v2p", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_VirtualNetworkInterfaceHost", + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Create a mapping from a virtual NIC to a physical host", + "operationId": "set_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a mapping from a virtual NIC to a physical host", + "operationId": "del_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}": { + "put": { + "operationId": "vmm_register", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_unregister", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmUnregisterResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/disks/{disk_id}/snapshot": { + "post": { + "summary": "Take a snapshot of a disk that is attached to an instance", + "operationId": "vmm_issue_disk_snapshot_request", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/external-ip": { + "put": { + "operationId": "vmm_put_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_delete_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/multicast-group": { + "put": { + "operationId": "vmm_join_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_leave_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/state": { + "get": { + "operationId": "vmm_get_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "vmm_put_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc/{vpc_id}/firewall/rules": { + "put": { + "operationId": "vpc_firewall_rules_put", + "parameters": [ + { + "in": "path", + "name": "vpc_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRulesEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc-routes": { + "get": { + "summary": "Get the current versions of VPC routing rules.", + "operationId": "list_vpc_routes", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteState", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteState" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update VPC routing rules.", + "operationId": "set_vpc_routes", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteSet", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteSet" + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones": { + "get": { + "summary": "List the zones that are currently managed by the sled agent.", + "operationId": "zones_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup": { + "post": { + "summary": "Trigger a zone bundle cleanup.", + "operationId": "zone_bundle_cleanup", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_CleanupCount", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CleanupCount" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/context": { + "get": { + "summary": "Return context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContext" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContextUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/utilization": { + "get": { + "summary": "Return utilization information about all zone bundles.", + "operationId": "zone_bundle_utilization", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_BundleUtilization", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BundleUtilization" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles": { + "get": { + "summary": "List all zone bundles that exist, even for now-deleted zones.", + "operationId": "zone_bundle_list_all", + "parameters": [ + { + "in": "query", + "name": "filter", + "description": "An optional substring used to filter zone bundles.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}": { + "get": { + "summary": "List the zone bundles that are available for a running zone.", + "operationId": "zone_bundle_list", + "parameters": [ + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}/{bundle_id}": { + "get": { + "summary": "Fetch the binary content of a single zone bundle.", + "operationId": "zone_bundle_get", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a zone bundle.", + "operationId": "zone_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AddSledRequest": { + "description": "A request to Add a given sled after rack initialization has occurred", + "type": "object", + "properties": { + "sled_id": { + "$ref": "#/components/schemas/BaseboardId" + }, + "start_request": { + "$ref": "#/components/schemas/StartSledAgentRequest" + } + }, + "required": [ + "sled_id", + "start_request" + ] + }, + "ArtifactConfig": { + "description": "Artifact configuration.\n\nThis type is used in both GET (response) and PUT (request) operations.", + "type": "object", + "properties": { + "artifacts": { + "type": "array", + "items": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "uniqueItems": true + }, + "generation": { + "$ref": "#/components/schemas/Generation" + } + }, + "required": [ + "artifacts", + "generation" + ] + }, + "ArtifactCopyFromDepotBody": { + "description": "Request body for copying artifacts from a depot.", + "type": "object", + "properties": { + "depot_base_url": { + "type": "string" + } + }, + "required": [ + "depot_base_url" + ] + }, + "ArtifactCopyFromDepotResponse": { + "description": "Response for copying artifacts from a depot.", + "type": "object" + }, + "ArtifactListResponse": { + "description": "Response for listing artifacts.", + "type": "object", + "properties": { + "generation": { + "$ref": "#/components/schemas/Generation" + }, + "list": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + }, + "required": [ + "generation", + "list" + ] + }, + "ArtifactPutResponse": { + "description": "Response for putting an artifact.", + "type": "object", + "properties": { + "datasets": { + "description": "The number of valid M.2 artifact datasets we found on the sled. There is typically one of these datasets for each functional M.2.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "successful_writes": { + "description": "The number of valid writes to the M.2 artifact datasets. This should be less than or equal to the number of artifact datasets.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "datasets", + "successful_writes" + ] + }, + "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" + ] + } + ] + }, + "BaseboardId": { + "description": "A representation of a Baseboard ID as used in the inventory subsystem This type is essentially the same as a `Baseboard` except it doesn't have a revision or HW type (Gimlet, PC, Unknown).", + "type": "object", + "properties": { + "part_number": { + "description": "Oxide Part Number", + "type": "string" + }, + "serial_number": { + "description": "Serial number (unique for a given part number)", + "type": "string" + } + }, + "required": [ + "part_number", + "serial_number" + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdPeerConfig": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "remote": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "$ref": "#/components/schemas/SwitchLocation" + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "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" + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "shaper": { + "nullable": true, + "description": "Shaper to apply to outgoing messages.", + "default": null, + "type": "string" + } + }, + "required": [ + "asn", + "originate" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "addr": { + "description": "Address of the peer.", + "type": "string", + "format": "ipv4" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "asn": { + "description": "The autonomous system number of the router the peer belongs to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "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.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "nullable": true, + "description": "How long to delay sending open messages to a peer. In seconds.", + "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.", + "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.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "nullable": true, + "description": "The interval to send keepalive messages at.", + "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 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "default": null, + "type": "string" + }, + "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 + }, + "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" + ] + }, + "BlobStorageBackend": { + "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", + "type": "object", + "properties": { + "base64": { + "description": "The disk's initial contents, encoded as a base64 string.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + } + }, + "required": [ + "base64", + "readonly" + ], + "additionalProperties": false + }, + "Board": { + "description": "A VM's mainboard.", + "type": "object", + "properties": { + "chipset": { + "description": "The chipset to expose to guest software.", + "allOf": [ + { + "$ref": "#/components/schemas/Chipset" + } + ] + }, + "cpuid": { + "nullable": true, + "description": "The CPUID values to expose to the guest. If `None`, bhyve will derive default values from the host's CPUID values.", + "allOf": [ + { + "$ref": "#/components/schemas/Cpuid" + } + ] + }, + "cpus": { + "description": "The number of virtual logical processors attached to this VM.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "guest_hv_interface": { + "description": "The hypervisor platform to expose to the guest. The default is a bhyve-compatible interface with no additional features.\n\nFor compatibility with older versions of Propolis, this field is only serialized if it specifies a non-default interface.", + "allOf": [ + { + "$ref": "#/components/schemas/GuestHypervisorInterface" + } + ] + }, + "memory_mb": { + "description": "The amount of guest RAM attached to this VM.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "chipset", + "cpus", + "memory_mb" + ], + "additionalProperties": false + }, + "BootImageHeader": { + "type": "object", + "properties": { + "data_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "flags": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "image_name": { + "type": "string" + }, + "image_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "sha256": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 32, + "maxItems": 32 + }, + "target_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "data_size", + "flags", + "image_name", + "image_size", + "sha256", + "target_size" + ] + }, + "BootOrderEntry": { + "description": "An entry in the boot order stored in a [`BootSettings`] component.", + "type": "object", + "properties": { + "id": { + "description": "The ID of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + } + }, + "required": [ + "id" + ] + }, + "BootPartitionContents": { + "type": "object", + "properties": { + "boot_disk": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/M2Slot" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/M2Slot" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_a": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_b": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "boot_disk", + "slot_a", + "slot_b" + ] + }, + "BootPartitionDetails": { + "type": "object", + "properties": { + "artifact_hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "artifact_size": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "header": { + "$ref": "#/components/schemas/BootImageHeader" + } + }, + "required": [ + "artifact_hash", + "artifact_size", + "header" + ] + }, + "BootSettings": { + "description": "Settings supplied to the guest's firmware image that specify the order in which it should consider its options when selecting a device to try to boot from.", + "type": "object", + "properties": { + "order": { + "description": "An ordered list of components to attempt to boot from.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BootOrderEntry" + } + } + }, + "required": [ + "order" + ], + "additionalProperties": false + }, + "BootstoreStatus": { + "description": "Status of the local bootstore node.", + "type": "object", + "properties": { + "accepted_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "established_connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EstablishedConnection" + } + }, + "fsm_ledger_generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fsm_state": { + "type": "string" + }, + "negotiating_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "network_config_ledger_generation": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "peers": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "accepted_connections", + "established_connections", + "fsm_ledger_generation", + "fsm_state", + "negotiating_connections", + "peers" + ] + }, + "BundleUtilization": { + "description": "The portion of a debug dataset used for zone bundles.", + "type": "object", + "properties": { + "bytes_available": { + "description": "The total number of bytes available for zone bundles.\n\nThis is `dataset_quota` multiplied by the context's storage limit.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes_used": { + "description": "Total bundle usage, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "dataset_quota": { + "description": "The total dataset quota, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bytes_available", + "bytes_used", + "dataset_quota" + ] + }, + "ByteCount": { + "description": "Byte count to express memory or storage capacity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "Chipset": { + "description": "A kind of virtual chipset.", + "oneOf": [ + { + "description": "An Intel 440FX-compatible chipset.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i440_fx" + ] + }, + "value": { + "$ref": "#/components/schemas/I440Fx" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "CleanupContext": { + "description": "Context provided for the zone bundle cleanup task.", + "type": "object", + "properties": { + "period": { + "description": "The period on which automatic checks and cleanup is performed.", + "allOf": [ + { + "$ref": "#/components/schemas/CleanupPeriod" + } + ] + }, + "priority": { + "description": "The priority ordering for keeping old bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "description": "The limit on the dataset quota available for zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/StorageLimit" + } + ] + } + }, + "required": [ + "period", + "priority", + "storage_limit" + ] + }, + "CleanupContextUpdate": { + "description": "Parameters used to update the zone bundle cleanup context.", + "type": "object", + "properties": { + "period": { + "nullable": true, + "description": "The new period on which automatic cleanups are run.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "priority": { + "nullable": true, + "description": "The priority ordering for preserving old zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "nullable": true, + "description": "The new limit on the underlying dataset quota allowed for bundles.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "CleanupCount": { + "description": "The count of bundles / bytes removed during a cleanup operation.", + "type": "object", + "properties": { + "bundles": { + "description": "The number of bundles removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes": { + "description": "The number of bytes removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bundles", + "bytes" + ] + }, + "CleanupPeriod": { + "description": "A period on which bundles are automatically cleaned up.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "ComponentV0": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "virtio_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "nvme_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "virtio_nic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "serial_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "pci_pci_bridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "qemu_pvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "boot_settings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_pci_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_p9" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/P9fs" + }, + "type": { + "type": "string", + "enum": [ + "p9fs" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/MigrationFailureInjector" + }, + "type": { + "type": "string", + "enum": [ + "migration_failure_injector" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "crucible_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "file_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "blob_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "virtio_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "dlpi_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, + "CompressionAlgorithm": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "on" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "off" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "gzip" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "level": { + "$ref": "#/components/schemas/GzipLevel" + }, + "type": { + "type": "string", + "enum": [ + "gzip_n" + ] + } + }, + "required": [ + "level", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lz4" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lzjb" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zle" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "ConfigReconcilerInventory": { + "description": "Describes the last attempt made by the sled-agent-config-reconciler to reconcile the current sled config against the actual state of the sled.", + "type": "object", + "properties": { + "boot_partitions": { + "$ref": "#/components/schemas/BootPartitionContents" + }, + "datasets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "external_disks": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "last_reconciled_config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "orphaned_datasets": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/OrphanedDataset" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/OrphanedDataset" + }, + "uniqueItems": true + }, + "remove_mupdate_override": { + "nullable": true, + "description": "The result of removing the mupdate override file on disk.\n\n`None` if `remove_mupdate_override` was not provided in the sled config.", + "allOf": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideInventory" + } + ] + }, + "zones": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + } + }, + "required": [ + "boot_partitions", + "datasets", + "external_disks", + "last_reconciled_config", + "orphaned_datasets", + "zones" + ] + }, + "ConfigReconcilerInventoryResult": { + "oneOf": [ + { + "type": "object", + "properties": { + "result": { + "type": "string", + "enum": [ + "ok" + ] + } + }, + "required": [ + "result" + ] + }, + { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "result": { + "type": "string", + "enum": [ + "err" + ] + } + }, + "required": [ + "message", + "result" + ] + } + ] + }, + "ConfigReconcilerInventoryStatus": { + "description": "Status of the sled-agent-config-reconciler task.", + "oneOf": [ + { + "description": "The reconciler task has not yet run for the first time since sled-agent started.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_yet_run" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "The reconciler task is actively running.", + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "running_for": { + "$ref": "#/components/schemas/Duration" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "config", + "running_for", + "started_at", + "status" + ] + }, + { + "description": "The reconciler task is currently idle, but previously did complete a reconciliation attempt.\n\nThis variant does not include the `OmicronSledConfig` used in the last attempt, because that's always available via [`ConfigReconcilerInventory::last_reconciled_config`].", + "type": "object", + "properties": { + "completed_at": { + "type": "string", + "format": "date-time" + }, + "ran_for": { + "$ref": "#/components/schemas/Duration" + }, + "status": { + "type": "string", + "enum": [ + "idle" + ] + } + }, + "required": [ + "completed_at", + "ran_for", + "status" + ] + } + ] + }, + "Cpuid": { + "description": "A set of CPUID values to expose to a guest.", + "type": "object", + "properties": { + "entries": { + "description": "A list of CPUID leaves/subleaves and their associated values.\n\nPropolis servers require that each entry's `leaf` be unique and that it falls in either the \"standard\" (0 to 0xFFFF) or \"extended\" (0x8000_0000 to 0x8000_FFFF) function ranges, since these are the only valid input ranges currently defined by Intel and AMD. See the Intel 64 and IA-32 Architectures Software Developer's Manual (June 2024) Table 3-17 and the AMD64 Architecture Programmer's Manual (March 2024) Volume 3's documentation of the CPUID instruction.", + "type": "array", + "items": { + "$ref": "#/components/schemas/CpuidEntry" + } + }, + "vendor": { + "description": "The CPU vendor to emulate.\n\nCPUID leaves in the extended range (0x8000_0000 to 0x8000_FFFF) have vendor-defined semantics. Propolis uses this value to determine these semantics when deciding whether it needs to specialize the supplied template values for these leaves.", + "allOf": [ + { + "$ref": "#/components/schemas/CpuidVendor" + } + ] + } + }, + "required": [ + "entries", + "vendor" + ], + "additionalProperties": false + }, + "CpuidEntry": { + "description": "A full description of a CPUID leaf/subleaf and the values it produces.", + "type": "object", + "properties": { + "eax": { + "description": "The value to return in eax.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ebx": { + "description": "The value to return in ebx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ecx": { + "description": "The value to return in ecx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "edx": { + "description": "The value to return in edx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "leaf": { + "description": "The leaf (function) number for this entry.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "subleaf": { + "nullable": true, + "description": "The subleaf (index) number for this entry, if it uses subleaves.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "eax", + "ebx", + "ecx", + "edx", + "leaf" + ], + "additionalProperties": false + }, + "CpuidVendor": { + "description": "A CPU vendor to use when interpreting the meanings of CPUID leaves in the extended ID range (0x80000000 to 0x8000FFFF).", + "type": "string", + "enum": [ + "amd", + "intel" + ] + }, + "CrucibleStorageBackend": { + "description": "A Crucible storage backend.", + "type": "object", + "properties": { + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "request_json": { + "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", + "type": "string" + } + }, + "required": [ + "readonly", + "request_json" + ], + "additionalProperties": false + }, + "DatasetConfig": { + "description": "Configuration information necessary to request a single dataset.\n\nThese datasets are tracked directly by Nexus.", + "type": "object", + "properties": { + "compression": { + "description": "The compression mode to be used by the dataset", + "allOf": [ + { + "$ref": "#/components/schemas/CompressionAlgorithm" + } + ] + }, + "id": { + "description": "The UUID of the dataset being requested", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "name": { + "description": "The dataset's name", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetName" + } + ] + }, + "quota": { + "nullable": true, + "description": "The upper bound on the amount of storage used by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "reservation": { + "nullable": true, + "description": "The lower bound on the amount of storage usable by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "compression", + "id", + "name" + ] + }, + "DatasetKind": { + "description": "The kind of dataset. See the `DatasetKind` enum in omicron-common for possible values.", + "type": "string" + }, + "DatasetName": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/DatasetKind" + }, + "pool_name": { + "$ref": "#/components/schemas/ZpoolName" + } + }, + "required": [ + "kind", + "pool_name" + ] + }, + "DatasetUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::DatasetUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "DelegatedZvol": { + "description": "Delegate a ZFS volume to a zone", + "oneOf": [ + { + "description": "Delegate a slice of the local storage dataset present on this pool into the zone.", + "type": "object", + "properties": { + "dataset_id": { + "$ref": "#/components/schemas/DatasetUuid" + }, + "type": { + "type": "string", + "enum": [ + "local_storage" + ] + }, + "zpool_id": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + }, + "required": [ + "dataset_id", + "type", + "zpool_id" + ] + } + ] + }, + "DhcpConfig": { + "description": "DHCP configuration for a port\n\nNot present here: Hostname (DHCPv4 option 12; used in DHCPv6 option 39); we use `InstanceRuntimeState::hostname` for this value.", + "type": "object", + "properties": { + "dns_servers": { + "description": "DNS servers to send to the instance\n\n(DHCPv4 option 6; DHCPv6 option 23)", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "host_domain": { + "nullable": true, + "description": "DNS zone this instance's hostname belongs to (e.g. the `project.example` part of `instance1.project.example`)\n\n(DHCPv4 option 15; used in DHCPv6 option 39)", + "type": "string" + }, + "search_domains": { + "description": "DNS search domains\n\n(DHCPv4 option 119; DHCPv6 option 24)", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "dns_servers", + "search_domains" + ] + }, + "DiskEnsureBody": { + "description": "Sent from to a sled agent to establish the runtime state of a Disk", + "type": "object", + "properties": { + "initial_runtime": { + "description": "Last runtime state of the Disk known to Nexus (used if the agent has never seen this Disk before).", + "allOf": [ + { + "$ref": "#/components/schemas/DiskRuntimeState" + } + ] + }, + "target": { + "description": "requested runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskStateRequested" + } + ] + } + }, + "required": [ + "initial_runtime", + "target" + ] + }, + "DiskIdentity": { + "description": "Uniquely identifies a disk.", + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "serial": { + "type": "string" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "model", + "serial", + "vendor" + ] + }, + "DiskRuntimeState": { + "description": "Runtime state of the Disk, which includes its attach state and some minimal metadata", + "type": "object", + "properties": { + "disk_state": { + "description": "runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskState" + } + ] + }, + "gen": { + "description": "generation number for this state", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "time_updated": { + "description": "timestamp for this information", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "disk_state", + "gen", + "time_updated" + ] + }, + "DiskState": { + "description": "State of a Disk", + "oneOf": [ + { + "description": "Disk is being initialized", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "creating" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready but detached from any Instance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready to receive blocks from an external source", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "import_ready" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from a URL", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_url" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from bulk writes", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_bulk_writes" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being finalized to state Detached", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "finalizing" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is undergoing maintenance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "maintenance" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is being detached from the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "detaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk has been destroyed", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is unavailable", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskStateRequested": { + "description": "Used to request a Disk state change", + "oneOf": [ + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskVariant": { + "type": "string", + "enum": [ + "U2", + "M2" + ] + }, + "DlpiNetworkBackend": { + "description": "A network backend associated with a DLPI VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, + "EarlyNetworkConfig": { + "description": "Network configuration required to bring up the control plane\n\nThe fields in this structure are those from `RackInitializeRequest` necessary for use beyond RSS. This is just for the initial rack configuration and cold boot purposes. Updates come from Nexus.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/EarlyNetworkConfigBody" + }, + "generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "EarlyNetworkConfigBody": { + "description": "This is the actual configuration of EarlyNetworking.\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "ntp_servers": { + "description": "The external NTP server addresses.", + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackNetworkConfigV2" + } + ] + } + }, + "required": [ + "ntp_servers" + ] + }, + "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" + ] + }, + "EstablishedConnection": { + "description": "An established connection to a bootstore peer.", + "type": "object", + "properties": { + "addr": { + "type": "string" + }, + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + } + }, + "required": [ + "addr", + "baseboard" + ] + }, + "ExternalIp": { + "description": "An external IP address used by a probe.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external IP address.", + "type": "string", + "format": "ip" + }, + "kind": { + "description": "The kind of address this is.", + "allOf": [ + { + "$ref": "#/components/schemas/IpKind" + } + ] + }, + "last_port": { + "description": "The last port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ExternalIpConfig": { + "description": "A single- or dual-stack external IP configuration.", + "oneOf": [ + { + "description": "Single-stack IPv4 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Single-stack IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Both IPv4 and IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "$ref": "#/components/schemas/ExternalIpv4Config" + }, + "v6": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "ExternalIpGatewayMap": { + "description": "Per-NIC mappings from external IP addresses to the Internet Gateways which can choose them as a source.", + "type": "object", + "properties": { + "mappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "uniqueItems": true + } + } + } + }, + "required": [ + "mappings" + ] + }, + "ExternalIpv4Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv4" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv4" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV4" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalIpv6Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv6" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV6" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ExternalZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "FileStorageBackend": { + "description": "A storage backend backed by a file in the host system's file system.", + "type": "object", + "properties": { + "block_size": { + "description": "Block size of the backend", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "path": { + "description": "A path to a file that backs a disk.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "workers": { + "nullable": true, + "description": "Optional worker threads for the file backend, exposed for testing only.", + "type": "integer", + "format": "uint", + "minimum": 1 + } + }, + "required": [ + "block_size", + "path", + "readonly" + ], + "additionalProperties": false + }, + "Generation": { + "description": "Generation numbers stored in the database, used for optimistic concurrency control", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "GuestHypervisorInterface": { + "description": "A hypervisor interface to expose to the guest.", + "oneOf": [ + { + "description": "Expose a bhyve-like interface (\"bhyve bhyve \" as the hypervisor ID in leaf 0x4000_0000 and no additional leaves or features).", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bhyve" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "description": "Expose a Hyper-V-compatible hypervisor interface with the supplied features enabled.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hyper_v" + ] + }, + "value": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HyperVFeatureFlag" + }, + "uniqueItems": true + } + }, + "required": [ + "features" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "GzipLevel": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "HealthMonitorInventory": { + "description": "Fields of sled-agent inventory reported by the health monitor subsystem.", + "type": "object", + "properties": { + "smf_services_in_maintenance": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/SvcsInMaintenanceResult" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/SvcsInMaintenanceResult" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "smf_services_in_maintenance" + ] + }, + "HostIdentifier": { + "description": "A `HostIdentifier` represents either an IP host or network (v4 or v6), or an entire VPC (identified by its VNI). It is used in firewall rule host filters.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "HostPhase2DesiredContents": { + "description": "Describes the desired contents of a host phase 2 slot (i.e., the boot partition on one of the internal M.2 drives).", + "oneOf": [ + { + "description": "Do not change the current contents.\n\nWe use this value when we've detected a sled has been mupdated (and we don't want to overwrite phase 2 images until we understand how to recover from that mupdate) and as the default value when reading an [`OmicronSledConfig`] that was ledgered before this concept existed.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "current_contents" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Set the phase 2 slot to the given artifact.\n\nThe artifact will come from an unpacked and distributed TUF repo.", + "type": "object", + "properties": { + "hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "type": { + "type": "string", + "enum": [ + "artifact" + ] + } + }, + "required": [ + "hash", + "type" + ] + } + ] + }, + "HostPhase2DesiredSlots": { + "description": "Describes the desired contents for both host phase 2 slots.", + "type": "object", + "properties": { + "slot_a": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + }, + "slot_b": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + } + }, + "required": [ + "slot_a", + "slot_b" + ] + }, + "HostPortConfig": { + "type": "object", + "properties": { + "addrs": { + "description": "IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport (must be in infra_ip pool). May also include an optional VLAN ID.", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "lldp": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Switchport to use for external connectivity", + "type": "string" + }, + "tx_eq": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + } + }, + "required": [ + "addrs", + "port" + ] + }, + "Hostname": { + "title": "An RFC-1035-compliant hostname", + "description": "A hostname identifies a host on a network, and is usually a dot-delimited sequence of labels, where each label contains only letters, digits, or the hyphen. See RFCs 1035 and 952 for more details.", + "type": "string", + "pattern": "^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*(? for background.", + "oneOf": [ + { + "description": "Start the switch zone if a switch is present.\n\nThis is the default policy.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "start_if_switch_present" + ] + } + }, + "required": [ + "policy" + ] + }, + { + "description": "Even if a switch zone is present, stop the switch zone.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "stop_despite_switch_presence" + ] + } + }, + "required": [ + "policy" + ] + } + ] + }, + "OrphanedDataset": { + "type": "object", + "properties": { + "available": { + "$ref": "#/components/schemas/ByteCount" + }, + "id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "mounted": { + "type": "boolean" + }, + "name": { + "$ref": "#/components/schemas/DatasetName" + }, + "reason": { + "type": "string" + }, + "used": { + "$ref": "#/components/schemas/ByteCount" + } + }, + "required": [ + "available", + "mounted", + "name", + "reason", + "used" + ] + }, + "P9fs": { + "description": "Describes a filesystem to expose through a P9 device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "chunk_size": { + "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach the guest to this P9 filesystem.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "source": { + "description": "The host source path to mount into the guest.", + "type": "string" + }, + "target": { + "description": "The 9P target filesystem tag.", + "type": "string" + } + }, + "required": [ + "chunk_size", + "pci_path", + "source", + "target" + ], + "additionalProperties": false + }, + "PciPath": { + "description": "A PCI bus/device/function tuple.", + "type": "object", + "properties": { + "bus": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "device": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "function": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "bus", + "device", + "function" + ] + }, + "PciPciBridge": { + "description": "A PCI-PCI bridge.", + "type": "object", + "properties": { + "downstream_bus": { + "description": "The logical bus number of this bridge's downstream bus. Other devices may use this bus number in their PCI paths to indicate they should be attached to this bridge's bus.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this bridge.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "downstream_bus", + "pci_path" + ], + "additionalProperties": false + }, + "PhysicalDiskUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PhysicalDiskUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PortConfigV2": { + "type": "object", + "properties": { + "addresses": { + "description": "This port's addresses and optional vlan IDs", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "autoneg": { + "description": "Whether or not to set autonegotiation", + "default": false, + "type": "boolean" + }, + "bgp_peers": { + "description": "BGP peers on this port", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "lldp": { + "nullable": true, + "description": "LLDP configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Nmae of the port this config applies to.", + "type": "string" + }, + "routes": { + "description": "The set of routes associated with this port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + }, + "switch": { + "description": "Switch the port belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "TX-EQ configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + }, + "uplink_port_fec": { + "nullable": true, + "description": "Port forward error correction type.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "uplink_port_speed": { + "description": "Port speed.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + } + }, + "required": [ + "addresses", + "bgp_peers", + "port", + "routes", + "switch", + "uplink_port_speed" + ] + }, + "PortFec": { + "description": "Switchport FEC options", + "type": "string", + "enum": [ + "firecode", + "none", + "rs" + ] + }, + "PortSpeed": { + "description": "Switchport Speed options", + "type": "string", + "enum": [ + "speed0_g", + "speed1_g", + "speed10_g", + "speed25_g", + "speed40_g", + "speed50_g", + "speed100_g", + "speed200_g", + "speed400_g" + ] + }, + "PriorityDimension": { + "description": "A dimension along with bundles can be sorted, to determine priority.", + "oneOf": [ + { + "description": "Sorting by time, with older bundles with lower priority.", + "type": "string", + "enum": [ + "time" + ] + }, + { + "description": "Sorting by the cause for creating the bundle.", + "type": "string", + "enum": [ + "cause" + ] + } + ] + }, + "PriorityOrder": { + "description": "The priority order for bundles during cleanup.\n\nBundles are sorted along the dimensions in [`PriorityDimension`], with each dimension appearing exactly once. During cleanup, lesser-priority bundles are pruned first, to maintain the dataset quota. Note that bundles are sorted by each dimension in the order in which they appear, with each dimension having higher priority than the next.\n\nTODO: The serde deserializer does not currently verify uniqueness of dimensions.", + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorityDimension" + }, + "minItems": 2, + "maxItems": 2 + }, + "PrivateIpConfig": { + "description": "VPC-private IP address configuration for a network interface.", + "oneOf": [ + { + "description": "The interface has only an IPv4 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface is dual-stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "description": "The interface's IPv4 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + ] + }, + "v6": { + "description": "The interface's IPv6 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + ] + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpv4Config": { + "description": "VPC-private IPv4 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv4" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip", + "subnet" + ] + }, + "PrivateIpv6Config": { + "description": "VPC-private IPv6 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv6" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "subnet", + "transit_ips" + ] + }, + "ProbeCreate": { + "description": "Parameters used to create a probe.", + "type": "object", + "properties": { + "external_ips": { + "description": "The external IP addresses assigned to the probe.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIp" + } + }, + "id": { + "description": "The ID for the probe.", + "allOf": [ + { + "$ref": "#/components/schemas/ProbeUuid" + } + ] + }, + "interface": { + "description": "The probe's networking interface.", + "allOf": [ + { + "$ref": "#/components/schemas/NetworkInterface" + } + ] + } + }, + "required": [ + "external_ips", + "id", + "interface" + ] + }, + "ProbeSet": { + "description": "A set of probes that the target sled should run.", + "type": "object", + "properties": { + "probes": { + "title": "IdHashMap", + "description": "The exact set of probes to run.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ProbeCreate" + } + ], + "path": "iddqd::IdHashMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeCreate" + }, + "uniqueItems": true + } + }, + "required": [ + "probes" + ] + }, + "ProbeUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ProbeUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, + "RackNetworkConfigV2": { + "description": "Initial network configuration", + "type": "object", + "properties": { + "bfd": { + "description": "BFD configuration for connecting the rack to external networks", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdPeerConfig" + } + }, + "bgp": { + "description": "BGP configurations for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "infra_ip_first": { + "description": "First ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "infra_ip_last": { + "description": "Last ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "ports": { + "description": "Uplinks for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortConfigV2" + } + }, + "rack_subnet": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + "required": [ + "bgp", + "infra_ip_first", + "infra_ip_last", + "ports", + "rack_subnet" + ] + }, + "RackUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::RackUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "RemoveMupdateOverrideBootSuccessInventory": { + "description": "Status of removing the mupdate override on the boot disk.", + "oneOf": [ + { + "description": "The mupdate override was successfully removed.", + "type": "string", + "enum": [ + "removed" + ] + }, + { + "description": "No mupdate override was found.\n\nThis is considered a success for idempotency reasons.", + "type": "string", + "enum": [ + "no_override" + ] + } + ] + }, + "RemoveMupdateOverrideInventory": { + "description": "Status of removing the mupdate override in the inventory.", + "type": "object", + "properties": { + "boot_disk_result": { + "description": "The result of removing the mupdate override on the boot disk.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "non_boot_message": { + "description": "What happened on non-boot disks.\n\nWe aren't modeling this out in more detail, because we plan to not try and keep ledgered data in sync across both disks in the future.", + "type": "string" + } + }, + "required": [ + "boot_disk_result", + "non_boot_message" + ] + }, + "ResolvedVpcFirewallRule": { + "description": "VPC firewall rule after object name resolution has been performed by Nexus", + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + }, + "direction": { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + }, + "filter_hosts": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/HostIdentifier" + }, + "uniqueItems": true + }, + "filter_ports": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/L4PortRange" + } + }, + "filter_protocols": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleProtocol" + } + }, + "priority": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterface" + } + } + }, + "required": [ + "action", + "direction", + "priority", + "status", + "targets" + ] + }, + "ResolvedVpcRoute": { + "description": "A VPC route resolved into a concrete target.", + "type": "object", + "properties": { + "dest": { + "$ref": "#/components/schemas/IpNet" + }, + "target": { + "$ref": "#/components/schemas/RouterTarget" + } + }, + "required": [ + "dest", + "target" + ] + }, + "ResolvedVpcRouteSet": { + "description": "An updated set of routes for a given VPC and/or subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRoute" + }, + "uniqueItems": true + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id", + "routes" + ] + }, + "ResolvedVpcRouteState": { + "description": "Version information for routes on a given VPC subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id" + ] + }, + "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" + ] + }, + "RouterId": { + "description": "Identifier for a VPC and/or subnet.", + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/RouterKind" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "kind", + "vni" + ] + }, + "RouterKind": { + "description": "The scope of a set of VPC router rules.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "system" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "subnet": { + "$ref": "#/components/schemas/IpNet" + }, + "type": { + "type": "string", + "enum": [ + "custom" + ] + } + }, + "required": [ + "subnet", + "type" + ] + } + ] + }, + "RouterTarget": { + "description": "The target for a given router entry.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "drop" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "internet_gateway" + ] + }, + "value": { + "$ref": "#/components/schemas/InternetGatewayRouterTarget" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc_subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "RouterVersion": { + "description": "Information on the current parent router (and version) of a route set according to the control plane.", + "type": "object", + "properties": { + "router_id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "router_id", + "version" + ] + }, + "SerialPort": { + "description": "A serial port device.", + "type": "object", + "properties": { + "num": { + "description": "The serial port number for this port.", + "allOf": [ + { + "$ref": "#/components/schemas/SerialPortNumber" + } + ] + } + }, + "required": [ + "num" + ], + "additionalProperties": false + }, + "SerialPortNumber": { + "description": "A serial port identifier, which determines what I/O ports a guest can use to access a port.", + "type": "string", + "enum": [ + "com1", + "com2", + "com3", + "com4" + ] + }, + "SledCpuFamily": { + "description": "Identifies the kind of CPU present on a sled, determined by reading CPUID.\n\nThis is intended to broadly support the control plane answering the question \"can I run this instance on that sled?\" given an instance with either no or some CPU platform requirement. It is not enough information for more precise placement questions - for example, is a CPU a high-frequency part or many-core part? We don't include Genoa here, but in that CPU family there are high frequency parts, many-core parts, and large-cache parts. To support those questions (or satisfactorily answer #8730) we would need to collect additional information and send it along.", + "oneOf": [ + { + "description": "The CPU vendor or its family number don't correspond to any of the known family variants.", + "type": "string", + "enum": [ + "unknown" + ] + }, + { + "description": "AMD Milan processors (or very close). Could be an actual Milan in a Gimlet, a close-to-Milan client Zen 3 part, or Zen 4 (for which Milan is the greatest common denominator).", + "type": "string", + "enum": [ + "amd_milan" + ] + }, + { + "description": "AMD Turin processors (or very close). Could be an actual Turin in a Cosmo, or a close-to-Turin client Zen 5 part.", + "type": "string", + "enum": [ + "amd_turin" + ] + }, + { + "description": "AMD Turin Dense processors. There are no \"Turin Dense-like\" CPUs unlike other cases, so this means a bona fide Zen 5c Turin Dense part.", + "type": "string", + "enum": [ + "amd_turin_dense" + ] + } + ] + }, + "SledDiagnosticsQueryOutput": { + "oneOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "object", + "properties": { + "command": { + "description": "The command and its arguments.", + "type": "string" + }, + "exit_code": { + "nullable": true, + "description": "The exit code if one was present when the command exited.", + "type": "integer", + "format": "int32" + }, + "exit_status": { + "description": "The exit status of the command. This will be the exit code (if any) and exit reason such as from a signal.", + "type": "string" + }, + "stdio": { + "description": "Any stdout/stderr produced by the command.", + "type": "string" + } + }, + "required": [ + "command", + "exit_status", + "stdio" + ] + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "failure": { + "type": "object", + "properties": { + "error": { + "description": "The reason the command failed to execute.", + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "required": [ + "failure" + ], + "additionalProperties": false + } + ] + }, + "SledIdentifiers": { + "description": "Identifiers for a single sled.\n\nThis is intended primarily to be used in timeseries, to identify sled from which metric data originates.", + "type": "object", + "properties": { + "model": { + "description": "Model name of the sled", + "type": "string" + }, + "rack_id": { + "description": "Control plane ID of the rack this sled is a member of", + "type": "string", + "format": "uuid" + }, + "revision": { + "description": "Revision number of the sled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "description": "Serial number of the sled", + "type": "string" + }, + "sled_id": { + "description": "Control plane ID for the sled itself", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "model", + "rack_id", + "revision", + "serial", + "sled_id" + ] + }, + "SledRole": { + "description": "Describes the role of the sled within the rack.\n\nNote that this may change if the sled is physically moved within the rack.", + "oneOf": [ + { + "description": "The sled is a general compute sled.", + "type": "string", + "enum": [ + "gimlet" + ] + }, + { + "description": "The sled is attached to the network switch, and has additional responsibilities.", + "type": "string", + "enum": [ + "scrimlet" + ] + } + ] + }, + "SledUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SledUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SledVmmState": { + "description": "A wrapper type containing a sled's total knowledge of the state of a VMM.", + "type": "object", + "properties": { + "migration_in": { + "nullable": true, + "description": "The current state of any inbound migration to this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "migration_out": { + "nullable": true, + "description": "The state of any outbound migration from this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "vmm_state": { + "description": "The most recent state of the sled's VMM process.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmRuntimeState" + } + ] + } + }, + "required": [ + "vmm_state" + ] + }, + "SoftNpuP9": { + "description": "Describes a PCI device that shares host files with the guest using the P9 protocol.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the port's associated DLPI backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "link_name": { + "description": "The data link name for this port.", + "type": "string" + } + }, + "required": [ + "backend_id", + "link_name" + ], + "additionalProperties": false + }, + "SourceNatConfigGeneric": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ip" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV4": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv4" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV6": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv6" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SpecKey": { + "description": "A key identifying a component in an instance spec.", + "oneOf": [ + { + "title": "uuid", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "type": "string" + } + ] + } + ] + }, + "StartSledAgentRequest": { + "description": "Configuration information for launching a Sled Agent.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/StartSledAgentRequestBody" + }, + "generation": { + "description": "The current generation number of data as stored in CRDB.\n\nThe initial generation is set during RSS time and then only mutated by Nexus. For now, we don't actually anticipate mutating this data, but we leave open the possiblity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "StartSledAgentRequestBody": { + "description": "This is the actual app level data of `StartSledAgentRequest`\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "id": { + "description": "Uuid of the Sled Agent to be created.", + "allOf": [ + { + "$ref": "#/components/schemas/SledUuid" + } + ] + }, + "is_lrtq_learner": { + "description": "Is this node an LRTQ learner node?\n\nWe only put the node into learner mode if `use_trust_quorum` is also true.", + "type": "boolean" + }, + "rack_id": { + "description": "Uuid of the rack to which this sled agent belongs.", + "type": "string", + "format": "uuid" + }, + "subnet": { + "description": "Portion of the IP space to be managed by the Sled Agent.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Subnet" + } + ] + }, + "use_trust_quorum": { + "description": "Use trust quorum for key generation", + "type": "boolean" + } + }, + "required": [ + "id", + "is_lrtq_learner", + "rack_id", + "subnet", + "use_trust_quorum" + ] + }, + "StorageLimit": { + "description": "The limit on space allowed for zone bundles, as a percentage of the overall dataset's quota.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "SupportBundleMetadata": { + "description": "Metadata about a support bundle.", + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/SupportBundleState" + }, + "support_bundle_id": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + "required": [ + "state", + "support_bundle_id" + ] + }, + "SupportBundleState": { + "description": "State of a support bundle.", + "type": "string", + "enum": [ + "complete", + "incomplete" + ] + }, + "SupportBundleUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SupportBundleUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SvcInMaintenance": { + "description": "Information about an SMF service that is enabled but not running", + "type": "object", + "properties": { + "fmri": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "required": [ + "fmri", + "zone" + ] + }, + "SvcsInMaintenanceResult": { + "description": "Lists services in maintenance status if any, and the time the health check for SMF services ran", + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SvcInMaintenance" + } + }, + "time_of_status": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "errors", + "services" + ] + }, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SwitchPorts": { + "description": "A set of switch uplinks.", + "type": "object", + "properties": { + "uplinks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostPortConfig" + } + } + }, + "required": [ + "uplinks" + ] + }, + "TrustQuorumCommitRequest": { + "description": "Request to commit a trust quorum configuration at a given epoch.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + } + }, + "required": [ + "epoch", + "rack_id" + ] + }, + "TrustQuorumCommitResponse": { + "description": "Response indicating the commit status.", + "oneOf": [ + { + "description": "The configuration has been committed.", + "type": "string", + "enum": [ + "committed" + ] + }, + { + "description": "The commit is still pending.", + "type": "string", + "enum": [ + "pending" + ] + } + ] + }, + "TrustQuorumConfiguration": { + "description": "A trust quorum configuration.", + "type": "object", + "properties": { + "coordinator": { + "description": "The coordinator of this reconfiguration.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "epoch": { + "description": "Unique, monotonically increasing identifier for a configuration.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "description": "All members of the configuration and the hex-encoded SHA3-256 hash of their key shares.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "rack_id": { + "description": "Unique ID of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + }, + "threshold": { + "description": "The number of sleds required to reconstruct the rack secret.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "coordinator", + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "TrustQuorumCoordinatorStatus": { + "description": "Status of a node coordinating a trust quorum reconfiguration.", + "type": "object", + "properties": { + "acked_prepares": { + "description": "The set of nodes that have acknowledged the prepare.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "config": { + "description": "The configuration being prepared.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumConfiguration" + } + ] + } + }, + "required": [ + "acked_prepares", + "config" + ] + }, + "TrustQuorumEncryptedRackSecrets": { + "description": "Encrypted rack secrets for prior configurations.", + "type": "object", + "properties": { + "data": { + "description": "Hex-encoded encrypted data.", + "type": "string" + }, + "salt": { + "description": "Hex-encoded 32-byte salt used to derive the encryption key.", + "type": "string" + } + }, + "required": [ + "data", + "salt" + ] + }, + "TrustQuorumLrtqUpgradeRequest": { + "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + }, + "threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "TrustQuorumPrepareAndCommitRequest": { + "description": "Request to prepare and commit a trust quorum configuration.", + "type": "object", + "properties": { + "coordinator": { + "description": "The coordinator of this reconfiguration.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "encrypted_rack_secrets": { + "nullable": true, + "description": "Encrypted rack secrets from prior configurations, if any.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumEncryptedRackSecrets" + } + ] + }, + "epoch": { + "description": "Unique, monotonically increasing identifier for a configuration.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "description": "All members of the configuration and the hex-encoded SHA3-256 hash of their key shares.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "rack_id": { + "description": "Unique ID of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + }, + "threshold": { + "description": "The number of sleds required to reconstruct the rack secret.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "coordinator", + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "TrustQuorumReconfigureRequest": { + "description": "Reconfigure message for trust quorum changes.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "last_committed_epoch": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + }, + "threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "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" + } + } + }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "VirtioDisk": { + "description": "A disk that presents a virtio-block interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtioNetworkBackend": { + "description": "A network backend associated with a virtio-net (viona) VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the viona VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "VirtioNic": { + "description": "A network card that presents a virtio-net interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the device's backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "interface_id": { + "description": "A caller-defined correlation identifier for this interface. If Propolis is configured to collect network interface kstats in its Oximeter metrics, the metric series for this interface will be associated with this identifier.", + "type": "string", + "format": "uuid" + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "interface_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtualNetworkInterfaceHost": { + "description": "A mapping from a virtual NIC to a physical host", + "type": "object", + "properties": { + "physical_host_ip": { + "type": "string", + "format": "ipv6" + }, + "virtual_ip": { + "type": "string", + "format": "ip" + }, + "virtual_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "physical_host_ip", + "virtual_ip", + "virtual_mac", + "vni" + ] + }, + "VmmIssueDiskSnapshotRequestBody": { + "description": "Request body for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmIssueDiskSnapshotRequestResponse": { + "description": "Response for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmPutStateBody": { + "description": "The body of a request to move a previously-ensured instance into a specific runtime state.", + "type": "object", + "properties": { + "state": { + "description": "The state into which the instance should be driven.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmStateRequested" + } + ] + } + }, + "required": [ + "state" + ] + }, + "VmmPutStateResponse": { + "description": "The response sent from a request to move an instance into a specific runtime state.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current runtime state of the instance after handling the request to change its state. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "VmmRuntimeState": { + "description": "The dynamic runtime properties of an individual VMM process.", + "type": "object", + "properties": { + "gen": { + "description": "The generation number for this VMM's state.", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "state": { + "description": "The last state reported by this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmState" + } + ] + }, + "time_updated": { + "description": "Timestamp for the VMM's state.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "gen", + "state", + "time_updated" + ] + }, + "VmmSpec": { + "description": "Specifies the virtual hardware configuration of a new Propolis VMM in the form of a Propolis instance specification.", + "allOf": [ + { + "$ref": "#/components/schemas/InstanceSpecV0" + } + ] + }, + "VmmState": { + "description": "One of the states that a VMM can be in.", + "oneOf": [ + { + "description": "The VMM is initializing and has not started running guest CPUs yet.", + "type": "string", + "enum": [ + "starting" + ] + }, + { + "description": "The VMM has finished initializing and may be running guest CPUs.", + "type": "string", + "enum": [ + "running" + ] + }, + { + "description": "The VMM is shutting down.", + "type": "string", + "enum": [ + "stopping" + ] + }, + { + "description": "The VMM's guest has stopped, and the guest will not run again, but the VMM process may not have released all of its resources yet.", + "type": "string", + "enum": [ + "stopped" + ] + }, + { + "description": "The VMM is being restarted or its guest OS is rebooting.", + "type": "string", + "enum": [ + "rebooting" + ] + }, + { + "description": "The VMM is part of a live migration.", + "type": "string", + "enum": [ + "migrating" + ] + }, + { + "description": "The VMM process reported an internal failure.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "The VMM process has been destroyed and its resources have been released.", + "type": "string", + "enum": [ + "destroyed" + ] + } + ] + }, + "VmmStateRequested": { + "description": "Requestable running state of an Instance.\n\nA subset of [`omicron_common::api::external::InstanceState`].", + "oneOf": [ + { + "description": "Run this instance by migrating in from a previous running incarnation of the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "migration_target" + ] + }, + "value": { + "$ref": "#/components/schemas/InstanceMigrationTargetParams" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Start the instance if it is not already running.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Stop the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stopped" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Immediately reset the instance, as though it had stopped and immediately began to run again.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reboot" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "VmmUnregisterResponse": { + "description": "The response sent from a request to unregister an instance.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current state of the instance after handling the request to unregister it. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "VpcFirewallIcmpFilter": { + "type": "object", + "properties": { + "code": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/IcmpParamRange" + } + ] + }, + "icmp_type": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "icmp_type" + ] + }, + "VpcFirewallRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "VpcFirewallRuleDirection": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ] + }, + "VpcFirewallRuleProtocol": { + "description": "The protocols that may be specified in a firewall rule's filter", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "udp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp" + ] + }, + "value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallIcmpFilter" + } + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleStatus": { + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "VpcFirewallRulesEnsureBody": { + "description": "Update firewall rules for a VPC", + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcFirewallRule" + } + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "rules", + "vni" + ] + }, + "ZoneArtifactInventory": { + "description": "Inventory representation of a single zone artifact on a boot disk.\n\nPart of [`ManifestBootInventory`].", + "type": "object", + "properties": { + "expected_hash": { + "description": "The expected digest of the file's contents.", + "type": "string", + "format": "hex string (32 bytes)" + }, + "expected_size": { + "description": "The expected size of the file, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "file_name": { + "description": "The name of the zone file on disk, for example `nexus.tar.gz`. Zone files are always \".tar.gz\".", + "type": "string" + }, + "path": { + "description": "The full path to the zone file.", + "type": "string", + "format": "Utf8PathBuf" + }, + "status": { + "description": "The status of the artifact.\n\nThis is `Ok(())` if the artifact is present and matches the expected size and digest, or an error message if it is missing or does not match.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "type": "string", + "enum": [ + null + ] + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "expected_hash", + "expected_size", + "file_name", + "path", + "status" + ] + }, + "ZoneBundleCause": { + "description": "The reason or cause for a zone bundle, i.e., why it was created.", + "oneOf": [ + { + "description": "Some other, unspecified reason.", + "type": "string", + "enum": [ + "other" + ] + }, + { + "description": "A zone bundle taken when a sled agent finds a zone that it does not expect to be running.", + "type": "string", + "enum": [ + "unexpected_zone" + ] + }, + { + "description": "An instance zone was terminated.", + "type": "string", + "enum": [ + "terminated_instance" + ] + } + ] + }, + "ZoneBundleId": { + "description": "An identifier for a zone bundle.", + "type": "object", + "properties": { + "bundle_id": { + "description": "The ID for this bundle itself.", + "type": "string", + "format": "uuid" + }, + "zone_name": { + "description": "The name of the zone this bundle is derived from.", + "type": "string" + } + }, + "required": [ + "bundle_id", + "zone_name" + ] + }, + "ZoneBundleMetadata": { + "description": "Metadata about a zone bundle.", + "type": "object", + "properties": { + "cause": { + "description": "The reason or cause a bundle was created.", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleCause" + } + ] + }, + "id": { + "description": "Identifier for this zone bundle", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleId" + } + ] + }, + "time_created": { + "description": "The time at which this zone bundle was created.", + "type": "string", + "format": "date-time" + }, + "version": { + "description": "A version number for this zone bundle.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "cause", + "id", + "time_created", + "version" + ] + }, + "ZoneImageResolverInventory": { + "description": "Inventory representation of zone image resolver status and health.", + "type": "object", + "properties": { + "mupdate_override": { + "description": "The mupdate override status.", + "allOf": [ + { + "$ref": "#/components/schemas/MupdateOverrideInventory" + } + ] + }, + "zone_manifest": { + "description": "The zone manifest status.", + "allOf": [ + { + "$ref": "#/components/schemas/ManifestInventory" + } + ] + } + }, + "required": [ + "mupdate_override", + "zone_manifest" + ] + }, + "ZpoolName": { + "title": "The name of a Zpool", + "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", + "type": "string", + "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "ZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PropolisUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PropolisUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index d546d53ee9f..a0d41629ca3 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-12.0.0-ffacab.json \ No newline at end of file +sled-agent-13.0.0-015e5e.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 62c997dee4a..b0ac1fd577e 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -34,6 +34,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (13, ADD_TRUST_QUORUM), (12, ADD_SMF_SERVICES_HEALTH_CHECK), (11, ADD_DUAL_STACK_EXTERNAL_IP_CONFIG), (10, ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES), @@ -1065,4 +1066,71 @@ pub trait SledAgentApi { request_context: RequestContext, path_params: Path, ) -> Result; + + /// Initiate a trust quorum reconfiguration + #[endpoint { + method = POST, + path = "/trust-quorum/reconfigure", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_reconfigure( + request_context: RequestContext, + body: TypedBody, + ) -> Result; + + /// Initiate an upgrade from LRTQ + #[endpoint { + method = POST, + path = "/trust-quorum/upgrade-from-lrtq", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_upgrade_from_lrtq( + request_context: RequestContext, + body: TypedBody, + ) -> Result; + + /// Commit a trust quorum configuration + #[endpoint { + method = POST, + path = "/trust-quorum/commit", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result< + HttpResponseOk, + HttpError, + >; + + /// Get the coordinator status if this node is coordinating a reconfiguration + #[endpoint { + method = GET, + path = "/trust-quorum/coordinator-status", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_coordinator_status( + request_context: RequestContext, + ) -> Result< + HttpResponseOk< + Option, + >, + HttpError, + >; + + /// Attempt to prepare and commit a trust quorum configuration + #[endpoint { + method = POST, + path = "/trust-quorum/prepare-and-commit", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_prepare_and_commit( + request_context: RequestContext, + body: TypedBody< + latest::trust_quorum::TrustQuorumPrepareAndCommitRequest, + >, + ) -> Result< + HttpResponseOk, + HttpError, + >; } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 62b6fb9b408..3bb2a03f24d 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -55,6 +55,12 @@ use sled_agent_types::support_bundle::{ SupportBundleMetadata, SupportBundlePathParam, SupportBundleTransferQueryParams, }; +use sled_agent_types::trust_quorum::{ + TrustQuorumCommitRequest, TrustQuorumCommitResponse, + TrustQuorumConfiguration, TrustQuorumCoordinatorStatus, + TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, + TrustQuorumReconfigureRequest, +}; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, CleanupPeriod, StorageLimit, ZoneBundleFilter, ZoneBundleId, @@ -1178,4 +1184,184 @@ impl SledAgentApi for SledAgentImpl { Ok(HttpResponseUpdatedNoContent()) } + + async fn trust_quorum_reconfigure( + request_context: RequestContext, + body: TypedBody, + ) -> Result { + let sa = request_context.context(); + let request = body.into_inner(); + + let msg = trust_quorum_protocol::ReconfigureMsg { + rack_id: request.rack_id, + epoch: trust_quorum_protocol::Epoch(request.epoch), + last_committed_epoch: request + .last_committed_epoch + .map(trust_quorum_protocol::Epoch), + members: request.members, + threshold: trust_quorum_protocol::Threshold(request.threshold), + }; + + sa.trust_quorum() + .reconfigure(msg) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn trust_quorum_upgrade_from_lrtq( + request_context: RequestContext, + body: TypedBody, + ) -> Result { + let sa = request_context.context(); + let request = body.into_inner(); + + let msg = trust_quorum_protocol::LrtqUpgradeMsg { + rack_id: request.rack_id, + epoch: trust_quorum_protocol::Epoch(request.epoch), + members: request.members, + threshold: trust_quorum_protocol::Threshold(request.threshold), + }; + + sa.trust_quorum() + .upgrade_from_lrtq(msg) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn trust_quorum_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let sa = request_context.context(); + let request = body.into_inner(); + + let status = sa + .trust_quorum() + .commit( + request.rack_id, + trust_quorum_protocol::Epoch(request.epoch), + ) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let response = match status { + trust_quorum::CommitStatus::Committed => { + TrustQuorumCommitResponse::Committed + } + trust_quorum::CommitStatus::Pending => { + TrustQuorumCommitResponse::Pending + } + }; + + Ok(HttpResponseOk(response)) + } + + async fn trust_quorum_coordinator_status( + request_context: RequestContext, + ) -> Result>, HttpError> + { + let sa = request_context.context(); + + let status = sa + .trust_quorum() + .coordinator_status() + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let response = status.map(|s| TrustQuorumCoordinatorStatus { + config: TrustQuorumConfiguration { + rack_id: s.config.rack_id, + epoch: s.config.epoch.0, + coordinator: s.config.coordinator, + members: s + .config + .members + .into_iter() + .map(|(id, digest)| (id, hex::encode(digest.0))) + .collect(), + threshold: s.config.threshold.0, + }, + acked_prepares: s.acked_prepares, + }); + + Ok(HttpResponseOk(response)) + } + + async fn trust_quorum_prepare_and_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let sa = request_context.context(); + let request = body.into_inner(); + + let bad_request = |msg: String| HttpError::for_bad_request(None, msg); + + let parse_digest = |hex: &str| -> Result<[u8; 32], HttpError> { + let bytes = hex::decode(hex) + .map_err(|e| bad_request(format!("invalid hex: {e}")))?; + bytes.try_into().map_err(|v: Vec| { + bad_request(format!("digest must be 32 bytes, got {}", v.len())) + }) + }; + + let members = request + .members + .into_iter() + .map(|(id, hex)| { + Ok(( + id, + trust_quorum_protocol::Sha3_256Digest(parse_digest(&hex)?), + )) + }) + .collect::>()?; + + let encrypted_rack_secrets = request + .encrypted_rack_secrets + .map( + |ers| -> Result< + trust_quorum_protocol::EncryptedRackSecrets, + HttpError, + > { + let salt = parse_digest(&ers.salt)?; + let data = hex::decode(&ers.data).map_err(|e| { + bad_request(format!("invalid hex data: {e}")) + })?; + Ok(trust_quorum_protocol::EncryptedRackSecrets::new( + trust_quorum_protocol::Salt(salt), + data.into_boxed_slice(), + )) + }, + ) + .transpose()?; + + let config = trust_quorum_protocol::Configuration { + rack_id: request.rack_id, + epoch: trust_quorum_protocol::Epoch(request.epoch), + coordinator: request.coordinator, + members, + threshold: trust_quorum_protocol::Threshold(request.threshold), + encrypted_rack_secrets, + }; + + let status = sa + .trust_quorum() + .prepare_and_commit(config) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let response = match status { + trust_quorum::CommitStatus::Committed => { + TrustQuorumCommitResponse::Committed + } + trust_quorum::CommitStatus::Pending => { + TrustQuorumCommitResponse::Pending + } + }; + + Ok(HttpResponseOk(response)) + } } diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index b38b41e56e5..39ef7b23269 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -67,6 +67,11 @@ use sled_agent_types::support_bundle::{ SupportBundleMetadata, SupportBundlePathParam, SupportBundleTransferQueryParams, }; +use sled_agent_types::trust_quorum::{ + TrustQuorumCommitRequest, TrustQuorumCommitResponse, + TrustQuorumCoordinatorStatus, TrustQuorumLrtqUpgradeRequest, + TrustQuorumPrepareAndCommitRequest, TrustQuorumReconfigureRequest, +}; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, ZoneBundleFilter, ZoneBundleId, ZoneBundleMetadata, ZonePathParam, @@ -922,6 +927,41 @@ impl SledAgentApi for SledAgentSimImpl { ) -> Result { Ok(HttpResponseUpdatedNoContent()) } + + async fn trust_quorum_reconfigure( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result { + method_unimplemented() + } + + async fn trust_quorum_upgrade_from_lrtq( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result { + method_unimplemented() + } + + async fn trust_quorum_commit( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result, HttpError> { + method_unimplemented() + } + + async fn trust_quorum_coordinator_status( + _request_context: RequestContext, + ) -> Result>, HttpError> + { + method_unimplemented() + } + + async fn trust_quorum_prepare_and_commit( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result, HttpError> { + method_unimplemented() + } } fn method_unimplemented() -> Result { diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index c2b369469a5..6a0f1dea5d6 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -370,6 +370,9 @@ struct SledAgentInner { // A handle to the bootstore. bootstore: bootstore::NodeHandle, + // A handle to the trust quorum. + trust_quorum: trust_quorum::NodeTaskHandle, + // A handle to the hardware monitor. hardware_monitor: HardwareMonitorHandle, @@ -683,6 +686,7 @@ impl SledAgent { rack_network_config, zone_bundler: long_running_task_handles.zone_bundler.clone(), bootstore: long_running_task_handles.bootstore.clone(), + trust_quorum: long_running_task_handles.trust_quorum.clone(), hardware_monitor: long_running_task_handles .hardware_monitor .clone(), @@ -1048,6 +1052,10 @@ impl SledAgent { self.inner.bootstore.clone() } + pub fn trust_quorum(&self) -> trust_quorum::NodeTaskHandle { + self.inner.trust_quorum.clone() + } + pub fn list_vpc_routes(&self) -> Vec { self.inner.port_manager.vpc_routes_list() } diff --git a/sled-agent/types/src/lib.rs b/sled-agent/types/src/lib.rs index ada7793b041..19479f22e9f 100644 --- a/sled-agent/types/src/lib.rs +++ b/sled-agent/types/src/lib.rs @@ -20,5 +20,6 @@ pub mod rack_init; pub mod rack_ops; pub mod sled; pub mod support_bundle; +pub mod trust_quorum; pub mod zone_bundle; pub mod zone_images; diff --git a/sled-agent/types/src/trust_quorum.rs b/sled-agent/types/src/trust_quorum.rs new file mode 100644 index 00000000000..c1d1dc2c91b --- /dev/null +++ b/sled-agent/types/src/trust_quorum.rs @@ -0,0 +1,7 @@ +// 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/. + +//! Trust quorum types for the Sled Agent API. + +pub use sled_agent_types_versions::latest::trust_quorum::*; diff --git a/sled-agent/types/versions/src/add_trust_quorum/mod.rs b/sled-agent/types/versions/src/add_trust_quorum/mod.rs new file mode 100644 index 00000000000..69df0e7349b --- /dev/null +++ b/sled-agent/types/versions/src/add_trust_quorum/mod.rs @@ -0,0 +1,9 @@ +// 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 `ADD_TRUST_QUORUM` of the Sled Agent API. +//! +//! This version adds endpoints for trust quorum reconfiguration. + +pub mod trust_quorum; diff --git a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs new file mode 100644 index 00000000000..b18c4b8bb31 --- /dev/null +++ b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs @@ -0,0 +1,101 @@ +// 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/. + +//! Trust quorum types for the Sled Agent API. + +use std::collections::{BTreeMap, BTreeSet}; + +use omicron_uuid_kinds::RackUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::super::v1::sled::BaseboardId; + +/// Reconfigure message for trust quorum changes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumReconfigureRequest { + pub rack_id: RackUuid, + pub epoch: u64, + pub last_committed_epoch: Option, + pub members: BTreeSet, + pub threshold: u8, +} + +/// Request to upgrade from LRTQ (Legacy Rack Trust Quorum). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumLrtqUpgradeRequest { + pub rack_id: RackUuid, + pub epoch: u64, + pub members: BTreeSet, + pub threshold: u8, +} + +/// Request to commit a trust quorum configuration at a given epoch. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumCommitRequest { + pub rack_id: RackUuid, + pub epoch: u64, +} + +/// Response indicating the commit status. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TrustQuorumCommitResponse { + /// The configuration has been committed. + Committed, + /// The commit is still pending. + Pending, +} + +/// Status of a node coordinating a trust quorum reconfiguration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumCoordinatorStatus { + /// The configuration being prepared. + pub config: TrustQuorumConfiguration, + /// The set of nodes that have acknowledged the prepare. + pub acked_prepares: BTreeSet, +} + +/// A trust quorum configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumConfiguration { + /// Unique ID of the rack. + pub rack_id: RackUuid, + /// Unique, monotonically increasing identifier for a configuration. + pub epoch: u64, + /// The coordinator of this reconfiguration. + pub coordinator: BaseboardId, + /// All members of the configuration and the hex-encoded SHA3-256 hash of + /// their key shares. + pub members: BTreeMap, + /// The number of sleds required to reconstruct the rack secret. + pub threshold: u8, +} + +/// Request to prepare and commit a trust quorum configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumPrepareAndCommitRequest { + /// Unique ID of the rack. + pub rack_id: RackUuid, + /// Unique, monotonically increasing identifier for a configuration. + pub epoch: u64, + /// The coordinator of this reconfiguration. + pub coordinator: BaseboardId, + /// All members of the configuration and the hex-encoded SHA3-256 hash of + /// their key shares. + pub members: BTreeMap, + /// The number of sleds required to reconstruct the rack secret. + pub threshold: u8, + /// Encrypted rack secrets from prior configurations, if any. + pub encrypted_rack_secrets: Option, +} + +/// Encrypted rack secrets for prior configurations. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumEncryptedRackSecrets { + /// Hex-encoded 32-byte salt used to derive the encryption key. + pub salt: String, + /// Hex-encoded encrypted data. + pub data: String, +} diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 3552aef5c5a..910a4dad292 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -155,6 +155,17 @@ pub mod support_bundle { pub use crate::v1::support_bundle::SupportBundleTransferQueryParams; } +pub mod trust_quorum { + pub use crate::v13::trust_quorum::TrustQuorumCommitRequest; + pub use crate::v13::trust_quorum::TrustQuorumCommitResponse; + pub use crate::v13::trust_quorum::TrustQuorumConfiguration; + pub use crate::v13::trust_quorum::TrustQuorumCoordinatorStatus; + pub use crate::v13::trust_quorum::TrustQuorumEncryptedRackSecrets; + pub use crate::v13::trust_quorum::TrustQuorumLrtqUpgradeRequest; + pub use crate::v13::trust_quorum::TrustQuorumPrepareAndCommitRequest; + pub use crate::v13::trust_quorum::TrustQuorumReconfigureRequest; +} + pub mod zone_bundle { pub use crate::v1::zone_bundle::BundleUtilization; pub use crate::v1::zone_bundle::CleanupContext; diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index 09bbc900ccf..9e534c776a4 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -41,6 +41,8 @@ pub mod v10; pub mod v11; #[path = "add_health_monitor/mod.rs"] pub mod v12; +#[path = "add_trust_quorum/mod.rs"] +pub mod v13; #[path = "add_switch_zone_operator_policy/mod.rs"] pub mod v3; #[path = "add_nexus_lockstep_port_to_inventory/mod.rs"] diff --git a/trust-quorum/protocol/src/lib.rs b/trust-quorum/protocol/src/lib.rs index 44f0d75379c..e2137fd6cc9 100644 --- a/trust-quorum/protocol/src/lib.rs +++ b/trust-quorum/protocol/src/lib.rs @@ -9,7 +9,6 @@ //! All persistent state and all networking is managed outside of this //! implementation. -use crypto::Sha3_256Digest; use daft::Diffable; use derive_more::Display; use gfss::shamir::Share; @@ -42,7 +41,10 @@ pub use validators::{ }; pub use alarm::Alarm; -pub use crypto::{RackSecret, ReconstructedRackSecret}; +pub use crypto::{ + EncryptedRackSecrets, RackSecret, ReconstructedRackSecret, Salt, + Sha3_256Digest, +}; pub use messages::*; pub use node::{CommitError, Node, NodeDiff, PrepareAndCommitError}; // public only for docs. From 7c4588dab10a39787d979d1545468f7ce012682b Mon Sep 17 00:00:00 2001 From: finch Date: Sat, 20 Dec 2025 20:38:42 -0500 Subject: [PATCH 2/9] Stub out SledAgentApi methods in test utils for Nexus --- .../src/test_util/host_phase_2_test_state.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index ecf8fac14c2..ca5ee8c65cf 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -936,5 +936,62 @@ mod api_impl { ) -> Result { unimplemented!() } + + async fn trust_quorum_reconfigure( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumReconfigureRequest, + >, + ) -> Result { + unimplemented!() + } + + async fn trust_quorum_upgrade_from_lrtq( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumLrtqUpgradeRequest, + >, + ) -> Result { + unimplemented!() + } + + async fn trust_quorum_commit( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumCommitRequest, + >, + ) -> Result< + HttpResponseOk< + sled_agent_types::trust_quorum::TrustQuorumCommitResponse, + >, + HttpError, + > { + unimplemented!() + } + + async fn trust_quorum_coordinator_status( + _request_context: RequestContext, + ) -> Result< + HttpResponseOk< + Option, + >, + HttpError, + >{ + unimplemented!() + } + + async fn trust_quorum_prepare_and_commit( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumPrepareAndCommitRequest, + >, + ) -> Result< + HttpResponseOk< + sled_agent_types::trust_quorum::TrustQuorumCommitResponse, + >, + HttpError, + > { + unimplemented!() + } } } From 90b4c3fb60c92c9663c6d1d6807b786a963e2f1a Mon Sep 17 00:00:00 2001 From: finch Date: Sun, 21 Dec 2025 18:27:09 -0500 Subject: [PATCH 3/9] Add trust quorum proxy endpoints and use `serde_with` for hex encoding Add three new proxy endpoints to the sled-agent API that allow forwarding trust quorum operations to other nodes: - POST `/trust-quorum/proxy/commit` - POST `/trust-quorum/proxy/prepare-and-commit` - POST `/trust-quorum/proxy/status` Also refactors the trust quorum types to use `serde_with` with `Hex` encoding for cleaner serialization of digests and encrypted rack secrets, replacing manual hex string parsing with automatic encoding/decoding. Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 2 + .../src/test_util/host_phase_2_test_state.rs | 42 ++ ...e5e.json => sled-agent-13.0.0-25c5af.json} | 481 +++++++++++++++++- openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 44 ++ sled-agent/src/http_entrypoints.rs | 240 +++++++-- sled-agent/src/sim/http_entrypoints.rs | 25 +- sled-agent/types/versions/Cargo.toml | 1 + .../src/add_trust_quorum/trust_quorum.rs | 147 +++++- sled-agent/types/versions/src/latest.rs | 6 + 10 files changed, 936 insertions(+), 54 deletions(-) rename openapi/sled-agent/{sled-agent-13.0.0-015e5e.json => sled-agent-13.0.0-25c5af.json} (94%) diff --git a/Cargo.lock b/Cargo.lock index 79f55f5c650..c741c9ef374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12718,6 +12718,7 @@ dependencies = [ "hex", "indexmap 1.9.3", "indexmap 2.12.1", + "schemars 0.8.22", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -13107,6 +13108,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", + "serde_with", "sha3", "sled-hardware-types", "slog", diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index ca5ee8c65cf..3611acbdce0 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -993,5 +993,47 @@ mod api_impl { > { unimplemented!() } + + async fn trust_quorum_proxy_commit( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumProxyCommitRequest, + >, + ) -> Result< + HttpResponseOk< + sled_agent_types::trust_quorum::TrustQuorumCommitResponse, + >, + HttpError, + > { + unimplemented!() + } + + async fn trust_quorum_proxy_prepare_and_commit( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest, + >, + ) -> Result< + HttpResponseOk< + sled_agent_types::trust_quorum::TrustQuorumCommitResponse, + >, + HttpError, + > { + unimplemented!() + } + + async fn trust_quorum_proxy_status( + _request_context: RequestContext, + _body: TypedBody< + sled_agent_types::trust_quorum::TrustQuorumProxyStatusRequest, + >, + ) -> Result< + HttpResponseOk< + sled_agent_types::trust_quorum::TrustQuorumNodeStatus, + >, + HttpError, + > { + unimplemented!() + } } } diff --git a/openapi/sled-agent/sled-agent-13.0.0-015e5e.json b/openapi/sled-agent/sled-agent-13.0.0-25c5af.json similarity index 94% rename from openapi/sled-agent/sled-agent-13.0.0-015e5e.json rename to openapi/sled-agent/sled-agent-13.0.0-25c5af.json index e48731c3b37..5a336e64b4f 100644 --- a/openapi/sled-agent/sled-agent-13.0.0-015e5e.json +++ b/openapi/sled-agent/sled-agent-13.0.0-25c5af.json @@ -1639,6 +1639,108 @@ } } }, + "/trust-quorum/proxy/commit": { + "post": { + "summary": "Proxy a commit operation to another trust quorum node", + "operationId": "trust_quorum_proxy_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumProxyCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCommitResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/proxy/prepare-and-commit": { + "post": { + "summary": "Proxy a prepare-and-commit operation to another trust quorum node", + "operationId": "trust_quorum_proxy_prepare_and_commit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumProxyPrepareAndCommitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumCommitResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/proxy/status": { + "post": { + "summary": "Proxy a status request to another trust quorum node", + "operationId": "trust_quorum_proxy_status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumProxyStatusRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustQuorumNodeStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/trust-quorum/reconfigure": { "post": { "summary": "Initiate a trust quorum reconfiguration", @@ -8689,6 +8791,169 @@ "uplinks" ] }, + "TrustQuorumAlarm": { + "description": "An alarm indicating a protocol invariant violation in trust quorum.", + "oneOf": [ + { + "description": "Different configurations found for the same epoch.", + "type": "object", + "properties": { + "mismatched_configurations": { + "type": "object", + "properties": { + "config1": { + "description": "The first configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumConfiguration" + } + ] + }, + "config2": { + "description": "The second (mismatched) configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumConfiguration" + } + ] + }, + "from": { + "description": "The source of the mismatch (either a baseboard ID or \"Nexus\").", + "type": "string" + } + }, + "required": [ + "config1", + "config2", + "from" + ] + } + }, + "required": [ + "mismatched_configurations" + ], + "additionalProperties": false + }, + { + "description": "The key share computer could not compute this node's share.", + "type": "object", + "properties": { + "share_computation_failed": { + "type": "object", + "properties": { + "epoch": { + "description": "The epoch for which share computation failed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "error": { + "description": "The error message.", + "type": "string" + } + }, + "required": [ + "epoch", + "error" + ] + } + }, + "required": [ + "share_computation_failed" + ], + "additionalProperties": false + }, + { + "description": "We started collecting shares for a committed configuration, but we no longer have that configuration in our persistent state.", + "type": "object", + "properties": { + "committed_configuration_lost": { + "type": "object", + "properties": { + "collecting_epoch": { + "description": "The epoch we were collecting shares for.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "latest_committed_epoch": { + "description": "The latest committed epoch.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "collecting_epoch", + "latest_committed_epoch" + ] + } + }, + "required": [ + "committed_configuration_lost" + ], + "additionalProperties": false + }, + { + "description": "Decrypting the encrypted rack secrets failed when presented with a valid rack secret.", + "type": "object", + "properties": { + "rack_secret_decryption_failed": { + "type": "object", + "properties": { + "epoch": { + "description": "The epoch for which decryption failed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "error": { + "description": "The error message.", + "type": "string" + } + }, + "required": [ + "epoch", + "error" + ] + } + }, + "required": [ + "rack_secret_decryption_failed" + ], + "additionalProperties": false + }, + { + "description": "Reconstructing the rack secret failed when presented with valid shares.", + "type": "object", + "properties": { + "rack_secret_reconstruction_failed": { + "type": "object", + "properties": { + "epoch": { + "description": "The epoch for which reconstruction failed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "error": { + "description": "The error message.", + "type": "string" + } + }, + "required": [ + "epoch", + "error" + ] + } + }, + "required": [ + "rack_secret_reconstruction_failed" + ], + "additionalProperties": false + } + ] + }, "TrustQuorumCommitRequest": { "description": "Request to commit a trust quorum configuration at a given epoch.", "type": "object", @@ -8745,7 +9010,7 @@ "minimum": 0 }, "members": { - "description": "All members of the configuration and the hex-encoded SHA3-256 hash of their key shares.", + "description": "All members of the configuration and the SHA3-256 hash of their key shares.", "type": "object", "additionalProperties": { "type": "string" @@ -8805,11 +9070,11 @@ "type": "object", "properties": { "data": { - "description": "Hex-encoded encrypted data.", + "description": "Encrypted data.", "type": "string" }, "salt": { - "description": "Hex-encoded 32-byte salt used to derive the encryption key.", + "description": "32-byte salt used to derive the encryption key.", "type": "string" } }, @@ -8850,6 +9115,98 @@ "threshold" ] }, + "TrustQuorumNodeStatus": { + "description": "Status of a trust quorum node, returned from a proxied status request.", + "type": "object", + "properties": { + "alarms": { + "description": "Any alarms raised by this node.", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrustQuorumAlarm" + } + }, + "connected_peers": { + "description": "The peers this node is connected to.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "persistent_state": { + "description": "Summary of the node's persistent state.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumPersistentStateSummary" + } + ] + }, + "proxied_requests": { + "description": "Number of proxied requests currently in flight.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "alarms", + "connected_peers", + "persistent_state", + "proxied_requests" + ] + }, + "TrustQuorumPersistentStateSummary": { + "description": "Summary of a trust quorum node's persistent state.", + "type": "object", + "properties": { + "commits": { + "description": "Epochs that have been committed.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + }, + "configs": { + "description": "Epochs for which configurations have been prepared.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + }, + "expunged": { + "description": "Whether this node has been expunged from the quorum.", + "type": "boolean" + }, + "has_lrtq_share": { + "description": "Whether the node has an LRTQ (legacy) share.", + "type": "boolean" + }, + "shares": { + "description": "Epochs for which key shares exist.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + } + }, + "required": [ + "commits", + "configs", + "expunged", + "has_lrtq_share", + "shares" + ] + }, "TrustQuorumPrepareAndCommitRequest": { "description": "Request to prepare and commit a trust quorum configuration.", "type": "object", @@ -8878,7 +9235,7 @@ "minimum": 0 }, "members": { - "description": "All members of the configuration and the hex-encoded SHA3-256 hash of their key shares.", + "description": "All members of the configuration and the SHA3-256 hash of their key shares.", "type": "object", "additionalProperties": { "type": "string" @@ -8907,6 +9264,122 @@ "threshold" ] }, + "TrustQuorumProxyCommitRequest": { + "description": "Request to proxy a commit operation to another trust quorum node.", + "type": "object", + "properties": { + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "epoch": { + "description": "The epoch to commit.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rack_id": { + "description": "Unique ID of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + } + }, + "required": [ + "destination", + "epoch", + "rack_id" + ] + }, + "TrustQuorumProxyPrepareAndCommitRequest": { + "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", + "type": "object", + "properties": { + "coordinator": { + "description": "The coordinator of this reconfiguration.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "encrypted_rack_secrets": { + "nullable": true, + "description": "Encrypted rack secrets from prior configurations, if any.", + "allOf": [ + { + "$ref": "#/components/schemas/TrustQuorumEncryptedRackSecrets" + } + ] + }, + "epoch": { + "description": "Unique, monotonically increasing identifier for a configuration.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "description": "All members of the configuration and the SHA3-256 hash of their key shares.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "rack_id": { + "description": "Unique ID of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + }, + "threshold": { + "description": "The number of sleds required to reconstruct the rack secret.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "coordinator", + "destination", + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "TrustQuorumProxyStatusRequest": { + "description": "Request to proxy a status request to another trust quorum node.", + "type": "object", + "properties": { + "destination": { + "description": "The target node to get the status from.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + } + }, + "required": [ + "destination" + ] + }, "TrustQuorumReconfigureRequest": { "description": "Reconfigure message for trust quorum changes.", "type": "object", diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index a0d41629ca3..613aef2c002 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-13.0.0-015e5e.json \ No newline at end of file +sled-agent-13.0.0-25c5af.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index b0ac1fd577e..522ecbe1d76 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -1133,4 +1133,48 @@ pub trait SledAgentApi { HttpResponseOk, HttpError, >; + + /// Proxy a commit operation to another trust quorum node + #[endpoint { + method = POST, + path = "/trust-quorum/proxy/commit", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_proxy_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result< + HttpResponseOk, + HttpError, + >; + + /// Proxy a prepare-and-commit operation to another trust quorum node + #[endpoint { + method = POST, + path = "/trust-quorum/proxy/prepare-and-commit", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_proxy_prepare_and_commit( + request_context: RequestContext, + body: TypedBody< + latest::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest, + >, + ) -> Result< + HttpResponseOk, + HttpError, + >; + + /// Proxy a status request to another trust quorum node + #[endpoint { + method = POST, + path = "/trust-quorum/proxy/status", + versions = VERSION_ADD_TRUST_QUORUM.., + }] + async fn trust_quorum_proxy_status( + request_context: RequestContext, + body: TypedBody, + ) -> Result< + HttpResponseOk, + HttpError, + >; } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 3bb2a03f24d..8300885655f 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -56,10 +56,12 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - TrustQuorumCommitRequest, TrustQuorumCommitResponse, + TrustQuorumAlarm, TrustQuorumCommitRequest, TrustQuorumCommitResponse, TrustQuorumConfiguration, TrustQuorumCoordinatorStatus, - TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, - TrustQuorumReconfigureRequest, + TrustQuorumLrtqUpgradeRequest, TrustQuorumNodeStatus, + TrustQuorumPersistentStateSummary, TrustQuorumPrepareAndCommitRequest, + TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, + TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -1281,7 +1283,7 @@ impl SledAgentApi for SledAgentImpl { .config .members .into_iter() - .map(|(id, digest)| (id, hex::encode(digest.0))) + .map(|(id, digest)| (id, digest.0)) .collect(), threshold: s.config.threshold.0, }, @@ -1298,45 +1300,97 @@ impl SledAgentApi for SledAgentImpl { let sa = request_context.context(); let request = body.into_inner(); - let bad_request = |msg: String| HttpError::for_bad_request(None, msg); + let members = request + .members + .into_iter() + .map(|(id, digest)| (id, trust_quorum_protocol::Sha3_256Digest(digest))) + .collect(); + + let encrypted_rack_secrets = + request.encrypted_rack_secrets.map(|ers| { + trust_quorum_protocol::EncryptedRackSecrets::new( + trust_quorum_protocol::Salt(ers.salt), + ers.data.into_boxed_slice(), + ) + }); + + let config = trust_quorum_protocol::Configuration { + rack_id: request.rack_id, + epoch: trust_quorum_protocol::Epoch(request.epoch), + coordinator: request.coordinator, + members, + threshold: trust_quorum_protocol::Threshold(request.threshold), + encrypted_rack_secrets, + }; + + let status = sa + .trust_quorum() + .prepare_and_commit(config) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let response = match status { + trust_quorum::CommitStatus::Committed => { + TrustQuorumCommitResponse::Committed + } + trust_quorum::CommitStatus::Pending => { + TrustQuorumCommitResponse::Pending + } + }; - let parse_digest = |hex: &str| -> Result<[u8; 32], HttpError> { - let bytes = hex::decode(hex) - .map_err(|e| bad_request(format!("invalid hex: {e}")))?; - bytes.try_into().map_err(|v: Vec| { - bad_request(format!("digest must be 32 bytes, got {}", v.len())) - }) + Ok(HttpResponseOk(response)) + } + + async fn trust_quorum_proxy_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let sa = request_context.context(); + let request = body.into_inner(); + + let status = sa + .trust_quorum() + .proxy() + .commit( + request.destination, + request.rack_id, + trust_quorum_protocol::Epoch(request.epoch), + ) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let response = match status { + trust_quorum::CommitStatus::Committed => { + TrustQuorumCommitResponse::Committed + } + trust_quorum::CommitStatus::Pending => { + TrustQuorumCommitResponse::Pending + } }; + Ok(HttpResponseOk(response)) + } + + async fn trust_quorum_proxy_prepare_and_commit( + request_context: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let sa = request_context.context(); + let request = body.into_inner(); + let members = request .members .into_iter() - .map(|(id, hex)| { - Ok(( - id, - trust_quorum_protocol::Sha3_256Digest(parse_digest(&hex)?), - )) - }) - .collect::>()?; - - let encrypted_rack_secrets = request - .encrypted_rack_secrets - .map( - |ers| -> Result< - trust_quorum_protocol::EncryptedRackSecrets, - HttpError, - > { - let salt = parse_digest(&ers.salt)?; - let data = hex::decode(&ers.data).map_err(|e| { - bad_request(format!("invalid hex data: {e}")) - })?; - Ok(trust_quorum_protocol::EncryptedRackSecrets::new( - trust_quorum_protocol::Salt(salt), - data.into_boxed_slice(), - )) - }, - ) - .transpose()?; + .map(|(id, digest)| (id, trust_quorum_protocol::Sha3_256Digest(digest))) + .collect(); + + let encrypted_rack_secrets = + request.encrypted_rack_secrets.map(|ers| { + trust_quorum_protocol::EncryptedRackSecrets::new( + trust_quorum_protocol::Salt(ers.salt), + ers.data.into_boxed_slice(), + ) + }); let config = trust_quorum_protocol::Configuration { rack_id: request.rack_id, @@ -1349,7 +1403,8 @@ impl SledAgentApi for SledAgentImpl { let status = sa .trust_quorum() - .prepare_and_commit(config) + .proxy() + .prepare_and_commit(request.destination, config) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; @@ -1364,4 +1419,113 @@ impl SledAgentApi for SledAgentImpl { Ok(HttpResponseOk(response)) } + + async fn trust_quorum_proxy_status( + request_context: RequestContext, + body: TypedBody, + ) -> Result, HttpError> { + let sa = request_context.context(); + let request = body.into_inner(); + + let status = sa + .trust_quorum() + .proxy() + .status(request.destination) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + // We can't define a `From` impl between Alarm (the internal protocol type) and + // TrustQuorumAlarm (the external API type) due to orphan rules, and this conversion is only + // used here to bridge the API to the underlying trust-quorum types, so we'll just define a + // couple free functions in this scope for conversion: + + fn convert_configuration( + config: trust_quorum_protocol::Configuration, + ) -> TrustQuorumConfiguration { + TrustQuorumConfiguration { + rack_id: config.rack_id, + epoch: config.epoch.0, + coordinator: config.coordinator, + members: config + .members + .into_iter() + .map(|(id, digest)| (id, digest.0)) + .collect(), + threshold: config.threshold.0, + } + } + + fn convert_alarm( + alarm: trust_quorum_protocol::Alarm, + ) -> TrustQuorumAlarm { + match alarm { + trust_quorum_protocol::Alarm::MismatchedConfigurations { + config1, + config2, + from, + } => TrustQuorumAlarm::MismatchedConfigurations { + config1: convert_configuration(config1), + config2: convert_configuration(config2), + from, + }, + trust_quorum_protocol::Alarm::ShareComputationFailed { epoch, err } => { + TrustQuorumAlarm::ShareComputationFailed { + epoch: epoch.0, + error: err.to_string(), + } + } + trust_quorum_protocol::Alarm::CommittedConfigurationLost { + latest_committed_epoch, + collecting_epoch, + } => TrustQuorumAlarm::CommittedConfigurationLost { + latest_committed_epoch: latest_committed_epoch.0, + collecting_epoch: collecting_epoch.0, + }, + trust_quorum_protocol::Alarm::RackSecretDecryptionFailed { + epoch, + err, + } => TrustQuorumAlarm::RackSecretDecryptionFailed { + epoch: epoch.0, + error: err.to_string(), + }, + trust_quorum_protocol::Alarm::RackSecretReconstructionFailed { + epoch, + err, + } => TrustQuorumAlarm::RackSecretReconstructionFailed { + epoch: epoch.0, + error: err.to_string(), + }, + } + } + + let response = TrustQuorumNodeStatus { + connected_peers: status.connected_peers, + alarms: status.alarms.into_iter().map(convert_alarm).collect(), + persistent_state: TrustQuorumPersistentStateSummary { + has_lrtq_share: status.persistent_state.has_lrtq_share, + configs: status + .persistent_state + .configs + .into_iter() + .map(|e| e.0) + .collect(), + shares: status + .persistent_state + .shares + .into_iter() + .map(|e| e.0) + .collect(), + commits: status + .persistent_state + .commits + .into_iter() + .map(|e| e.0) + .collect(), + expunged: status.persistent_state.expunged.is_some(), + }, + proxied_requests: status.proxied_requests, + }; + + Ok(HttpResponseOk(response)) + } } diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 39ef7b23269..4c335edf43e 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -70,7 +70,9 @@ use sled_agent_types::support_bundle::{ use sled_agent_types::trust_quorum::{ TrustQuorumCommitRequest, TrustQuorumCommitResponse, TrustQuorumCoordinatorStatus, TrustQuorumLrtqUpgradeRequest, - TrustQuorumPrepareAndCommitRequest, TrustQuorumReconfigureRequest, + TrustQuorumNodeStatus, TrustQuorumPrepareAndCommitRequest, + TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, + TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -962,6 +964,27 @@ impl SledAgentApi for SledAgentSimImpl { ) -> Result, HttpError> { method_unimplemented() } + + async fn trust_quorum_proxy_commit( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result, HttpError> { + method_unimplemented() + } + + async fn trust_quorum_proxy_prepare_and_commit( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result, HttpError> { + method_unimplemented() + } + + async fn trust_quorum_proxy_status( + _request_context: RequestContext, + _body: TypedBody, + ) -> Result, HttpError> { + method_unimplemented() + } } fn method_unimplemented() -> Result { diff --git a/sled-agent/types/versions/Cargo.toml b/sled-agent/types/versions/Cargo.toml index a70565a47c0..f2a3ee26b2e 100644 --- a/sled-agent/types/versions/Cargo.toml +++ b/sled-agent/types/versions/Cargo.toml @@ -26,6 +26,7 @@ propolis_api_types.workspace = true proptest = { workspace = true, optional = true } schemars.workspace = true serde.workspace = true +serde_with = { workspace = true, features = ["hex", "schemars_0_8"] } serde_json.workspace = true sha3.workspace = true sled-hardware-types.workspace = true diff --git a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs index b18c4b8bb31..f0db0ba50a7 100644 --- a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs +++ b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet}; use omicron_uuid_kinds::RackUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::{hex::Hex, serde_as}; use super::super::v1::sled::BaseboardId; @@ -58,6 +59,7 @@ pub struct TrustQuorumCoordinatorStatus { } /// A trust quorum configuration. +#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumConfiguration { /// Unique ID of the rack. @@ -66,14 +68,16 @@ pub struct TrustQuorumConfiguration { pub epoch: u64, /// The coordinator of this reconfiguration. pub coordinator: BaseboardId, - /// All members of the configuration and the hex-encoded SHA3-256 hash of - /// their key shares. - pub members: BTreeMap, + /// All members of the configuration and the SHA3-256 hash of their key shares. + #[serde_as(as = "BTreeMap<_, Hex>")] + #[schemars(with = "BTreeMap")] + pub members: BTreeMap, /// The number of sleds required to reconstruct the rack secret. pub threshold: u8, } /// Request to prepare and commit a trust quorum configuration. +#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumPrepareAndCommitRequest { /// Unique ID of the rack. @@ -82,9 +86,10 @@ pub struct TrustQuorumPrepareAndCommitRequest { pub epoch: u64, /// The coordinator of this reconfiguration. pub coordinator: BaseboardId, - /// All members of the configuration and the hex-encoded SHA3-256 hash of - /// their key shares. - pub members: BTreeMap, + /// All members of the configuration and the SHA3-256 hash of their key shares. + #[serde_as(as = "BTreeMap<_, Hex>")] + #[schemars(with = "BTreeMap")] + pub members: BTreeMap, /// The number of sleds required to reconstruct the rack secret. pub threshold: u8, /// Encrypted rack secrets from prior configurations, if any. @@ -92,10 +97,132 @@ pub struct TrustQuorumPrepareAndCommitRequest { } /// Encrypted rack secrets for prior configurations. +#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumEncryptedRackSecrets { - /// Hex-encoded 32-byte salt used to derive the encryption key. - pub salt: String, - /// Hex-encoded encrypted data. - pub data: String, + /// 32-byte salt used to derive the encryption key. + #[serde_as(as = "Hex")] + #[schemars(with = "String")] + pub salt: [u8; 32], + /// Encrypted data. + #[serde_as(as = "Hex")] + #[schemars(with = "String")] + pub data: Vec, +} + +/// Request to proxy a commit operation to another trust quorum node. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumProxyCommitRequest { + /// The target node to proxy the request to. + pub destination: BaseboardId, + /// Unique ID of the rack. + pub rack_id: RackUuid, + /// The epoch to commit. + pub epoch: u64, +} + +/// Request to proxy a prepare-and-commit operation to another trust quorum node. +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumProxyPrepareAndCommitRequest { + /// The target node to proxy the request to. + pub destination: BaseboardId, + /// Unique ID of the rack. + pub rack_id: RackUuid, + /// Unique, monotonically increasing identifier for a configuration. + pub epoch: u64, + /// The coordinator of this reconfiguration. + pub coordinator: BaseboardId, + /// All members of the configuration and the SHA3-256 hash of their key shares. + #[serde_as(as = "BTreeMap<_, Hex>")] + #[schemars(with = "BTreeMap")] + pub members: BTreeMap, + /// The number of sleds required to reconstruct the rack secret. + pub threshold: u8, + /// Encrypted rack secrets from prior configurations, if any. + pub encrypted_rack_secrets: Option, +} + +/// Request to proxy a status request to another trust quorum node. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumProxyStatusRequest { + /// The target node to get the status from. + pub destination: BaseboardId, +} + +/// Status of a trust quorum node, returned from a proxied status request. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumNodeStatus { + /// The peers this node is connected to. + pub connected_peers: BTreeSet, + /// Any alarms raised by this node. + pub alarms: Vec, + /// Summary of the node's persistent state. + pub persistent_state: TrustQuorumPersistentStateSummary, + /// Number of proxied requests currently in flight. + pub proxied_requests: u64, +} + +/// An alarm indicating a protocol invariant violation in trust quorum. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TrustQuorumAlarm { + /// Different configurations found for the same epoch. + MismatchedConfigurations { + /// The first configuration. + config1: TrustQuorumConfiguration, + /// The second (mismatched) configuration. + config2: TrustQuorumConfiguration, + /// The source of the mismatch (either a baseboard ID or "Nexus"). + from: String, + }, + + /// The key share computer could not compute this node's share. + ShareComputationFailed { + /// The epoch for which share computation failed. + epoch: u64, + /// The error message. + error: String, + }, + + /// We started collecting shares for a committed configuration, + /// but we no longer have that configuration in our persistent state. + CommittedConfigurationLost { + /// The latest committed epoch. + latest_committed_epoch: u64, + /// The epoch we were collecting shares for. + collecting_epoch: u64, + }, + + /// Decrypting the encrypted rack secrets failed when presented with a + /// valid rack secret. + RackSecretDecryptionFailed { + /// The epoch for which decryption failed. + epoch: u64, + /// The error message. + error: String, + }, + + /// Reconstructing the rack secret failed when presented with valid shares. + RackSecretReconstructionFailed { + /// The epoch for which reconstruction failed. + epoch: u64, + /// The error message. + error: String, + }, +} + +/// Summary of a trust quorum node's persistent state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct TrustQuorumPersistentStateSummary { + /// Whether the node has an LRTQ (legacy) share. + pub has_lrtq_share: bool, + /// Epochs for which configurations have been prepared. + pub configs: BTreeSet, + /// Epochs for which key shares exist. + pub shares: BTreeSet, + /// Epochs that have been committed. + pub commits: BTreeSet, + /// Whether this node has been expunged from the quorum. + pub expunged: bool, } diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 910a4dad292..f0e4069643f 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -156,13 +156,19 @@ pub mod support_bundle { } pub mod trust_quorum { + pub use crate::v13::trust_quorum::TrustQuorumAlarm; pub use crate::v13::trust_quorum::TrustQuorumCommitRequest; pub use crate::v13::trust_quorum::TrustQuorumCommitResponse; pub use crate::v13::trust_quorum::TrustQuorumConfiguration; pub use crate::v13::trust_quorum::TrustQuorumCoordinatorStatus; pub use crate::v13::trust_quorum::TrustQuorumEncryptedRackSecrets; pub use crate::v13::trust_quorum::TrustQuorumLrtqUpgradeRequest; + pub use crate::v13::trust_quorum::TrustQuorumNodeStatus; + pub use crate::v13::trust_quorum::TrustQuorumPersistentStateSummary; pub use crate::v13::trust_quorum::TrustQuorumPrepareAndCommitRequest; + pub use crate::v13::trust_quorum::TrustQuorumProxyCommitRequest; + pub use crate::v13::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest; + pub use crate::v13::trust_quorum::TrustQuorumProxyStatusRequest; pub use crate::v13::trust_quorum::TrustQuorumReconfigureRequest; } From 757b742556f3bc6664a9958256ce68c2ddc1a323 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 13:18:20 -0500 Subject: [PATCH 4/9] Factor trust-quorum types into versioned crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create `trust-quorum-types` and `trust-quorum-types-versions` crates following the RFD 619 pattern for API type versioning. - `trust-quorum-types`: Re-exports `latest::*` from versions crate - `trust-quorum-types-versions`: Contains versioned type definitions Types used in the public API of sled-agent moved into these crates, while attempting to keep as much protocol implementation as possible in the `trust-quorum-protocol` crate. Since Rust doesn't allow adding inherent methods to foreign types, methods that operated on types now in `trust-quorum-types` have been converted to free functions: - `Salt::new()` → `new_salt()` - `EncryptedRackSecrets::decrypt()` → `decrypt_rack_secrets()` - `Configuration::new()` → `new_configuration()` Moved `BaseboardId` from `sled-agent-types-versions` to `sled-hardware-types` to allow sharing across crates without circular dependency. The sled-agent trust-quorum API types now re-export from `trust-quorum-types-versions` instead of duplicating definitions, eliminating the `TrustQuorum*`-prefixed wrapper types. Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 32 + Cargo.toml | 6 + ...5af.json => sled-agent-13.0.0-065232.json} | 992 +++++++++--------- openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 14 +- sled-agent/src/http_entrypoints.rs | 248 +---- sled-agent/src/sim/http_entrypoints.rs | 17 +- sled-agent/types/versions/Cargo.toml | 1 + .../src/add_trust_quorum/trust_quorum.rs | 194 +--- sled-agent/types/versions/src/initial/sled.rs | 57 +- sled-agent/types/versions/src/latest.rs | 20 +- sled-hardware/types/Cargo.toml | 2 + sled-hardware/types/src/lib.rs | 54 + trust-quorum/gfss/Cargo.toml | 1 + trust-quorum/gfss/src/shamir.rs | 6 +- trust-quorum/protocol/Cargo.toml | 1 + trust-quorum/protocol/src/configuration.rs | 221 ++-- .../protocol/src/coordinator_state.rs | 17 +- trust-quorum/protocol/src/crypto.rs | 230 ++-- trust-quorum/protocol/src/lib.rs | 79 +- trust-quorum/protocol/src/messages.rs | 6 +- trust-quorum/protocol/src/persistent_state.rs | 43 +- .../protocol/src/rack_secret_loader.rs | 5 +- trust-quorum/protocol/src/validators.rs | 5 +- trust-quorum/src/proxy.rs | 5 +- trust-quorum/src/task.rs | 61 +- trust-quorum/types/Cargo.toml | 12 + trust-quorum/types/src/lib.rs | 11 + trust-quorum/types/versions/Cargo.toml | 26 + .../types/versions/src/impls/epoch.rs | 16 + trust-quorum/types/versions/src/impls/mod.rs | 13 + .../versions/src/initial}/alarm.rs | 25 +- .../versions/src/initial/configuration.rs | 104 ++ .../types/versions/src/initial/crypto.rs | 153 +++ .../types/versions/src/initial/mod.rs | 14 + .../versions/src/initial/persistent_state.rs | 36 + .../types/versions/src/initial/status.rs | 49 + .../types/versions/src/initial/types.rs | 51 + trust-quorum/types/versions/src/latest.rs | 46 + trust-quorum/types/versions/src/lib.rs | 34 + 40 files changed, 1475 insertions(+), 1434 deletions(-) rename openapi/sled-agent/{sled-agent-13.0.0-25c5af.json => sled-agent-13.0.0-065232.json} (97%) create mode 100644 trust-quorum/types/Cargo.toml create mode 100644 trust-quorum/types/src/lib.rs create mode 100644 trust-quorum/types/versions/Cargo.toml create mode 100644 trust-quorum/types/versions/src/impls/epoch.rs create mode 100644 trust-quorum/types/versions/src/impls/mod.rs rename trust-quorum/{protocol/src => types/versions/src/initial}/alarm.rs (80%) create mode 100644 trust-quorum/types/versions/src/initial/configuration.rs create mode 100644 trust-quorum/types/versions/src/initial/crypto.rs create mode 100644 trust-quorum/types/versions/src/initial/mod.rs create mode 100644 trust-quorum/types/versions/src/initial/persistent_state.rs create mode 100644 trust-quorum/types/versions/src/initial/status.rs create mode 100644 trust-quorum/types/versions/src/initial/types.rs create mode 100644 trust-quorum/types/versions/src/latest.rs create mode 100644 trust-quorum/types/versions/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c741c9ef374..3fe04a22142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4087,6 +4087,7 @@ dependencies = [ "omicron-workspace-hack", "proptest", "rand 0.9.2", + "schemars 0.8.22", "secrecy 0.10.3", "serde", "subtle", @@ -13115,6 +13116,7 @@ dependencies = [ "strum 0.27.2", "test-strategy", "thiserror 2.0.17", + "trust-quorum-types-versions", "tufaceous-artifact", "uuid", ] @@ -13232,12 +13234,14 @@ dependencies = [ name = "sled-hardware-types" version = "0.1.0" dependencies = [ + "daft", "illumos-utils", "macaddr", "omicron-common", "omicron-workspace-hack", "schemars 0.8.22", "serde", + "thiserror 2.0.17", ] [[package]] @@ -15103,6 +15107,7 @@ dependencies = [ "test-strategy", "thiserror 2.0.17", "trust-quorum-test-utils", + "trust-quorum-types", "uuid", "zeroize", ] @@ -15127,6 +15132,33 @@ dependencies = [ "trust-quorum-protocol", ] +[[package]] +name = "trust-quorum-types" +version = "0.1.0" +dependencies = [ + "omicron-workspace-hack", + "trust-quorum-types-versions", +] + +[[package]] +name = "trust-quorum-types-versions" +version = "0.1.0" +dependencies = [ + "daft", + "derive_more 0.99.20", + "gfss", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "schemars 0.8.22", + "serde", + "serde_with", + "sled-hardware-types", + "slog", + "slog-error-chain", + "thiserror 2.0.17", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 0b3714563b7..ea97908cb8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,6 +152,8 @@ members = [ "trust-quorum/protocol", "trust-quorum/test-utils", "trust-quorum/tqdb", + "trust-quorum/types", + "trust-quorum/types/versions", "typed-rng", "update-common", "update-engine", @@ -320,6 +322,8 @@ default-members = [ "trust-quorum/protocol", "trust-quorum/test-utils", "trust-quorum/tqdb", + "trust-quorum/types", + "trust-quorum/types/versions", "test-utils", "typed-rng", "update-common", @@ -494,6 +498,8 @@ gfss = { path = "trust-quorum/gfss" } trust-quorum = { path = "trust-quorum" } trust-quorum-protocol = { path = "trust-quorum/protocol" } trust-quorum-test-utils = { path = "trust-quorum/test-utils" } +trust-quorum-types = { path = "trust-quorum/types" } +trust-quorum-types-versions = { path = "trust-quorum/types/versions" } glob = "0.3.2" guppy = "0.17.20" headers = "0.4.1" diff --git a/openapi/sled-agent/sled-agent-13.0.0-25c5af.json b/openapi/sled-agent/sled-agent-13.0.0-065232.json similarity index 97% rename from openapi/sled-agent/sled-agent-13.0.0-25c5af.json rename to openapi/sled-agent/sled-agent-13.0.0-065232.json index 5a336e64b4f..a91d8eac525 100644 --- a/openapi/sled-agent/sled-agent-13.0.0-25c5af.json +++ b/openapi/sled-agent/sled-agent-13.0.0-065232.json @@ -1567,7 +1567,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCommitResponse" + "$ref": "#/components/schemas/CommitStatus" } } } @@ -1591,7 +1591,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCoordinatorStatus" + "$ref": "#/components/schemas/CoordinatorStatus" } } } @@ -1625,7 +1625,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCommitResponse" + "$ref": "#/components/schemas/CommitStatus" } } } @@ -1659,7 +1659,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCommitResponse" + "$ref": "#/components/schemas/CommitStatus" } } } @@ -1693,7 +1693,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCommitResponse" + "$ref": "#/components/schemas/CommitStatus" } } } @@ -1727,7 +1727,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumNodeStatus" + "$ref": "#/components/schemas/NodeStatus" } } } @@ -2617,6 +2617,151 @@ "start_request" ] }, + "Alarm": { + "description": "An alarm indicating a protocol invariant violation.", + "oneOf": [ + { + "description": "Different configurations found for the same epoch.\n\nReason: Nexus creates configurations and stores them in CRDB before sending them to a coordinator of its choosing. Nexus will not send the same reconfiguration request to different coordinators. If it does those coordinators will generate different key shares. However, since Nexus will not tell different nodes to coordinate the same configuration, this state should be impossible to reach.", + "type": "object", + "properties": { + "mismatched_configurations": { + "type": "object", + "properties": { + "config1": { + "$ref": "#/components/schemas/Configuration" + }, + "config2": { + "$ref": "#/components/schemas/Configuration" + }, + "from": { + "description": "Either a stringified `BaseboardId` or \"Nexus\".", + "type": "string" + } + }, + "required": [ + "config1", + "config2", + "from" + ] + } + }, + "required": [ + "mismatched_configurations" + ], + "additionalProperties": false + }, + { + "description": "The `keyShareComputer` could not compute this node's share.\n\nReason: A threshold of valid key shares were received based on the the share digests in the Configuration. However, computation of the share still failed. This should be impossible.", + "type": "object", + "properties": { + "share_computation_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/CombineError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "share_computation_failed" + ], + "additionalProperties": false + }, + { + "description": "We started collecting shares for a committed configuration, but we no longer have that configuration in our persistent state.", + "type": "object", + "properties": { + "committed_configuration_lost": { + "type": "object", + "properties": { + "collecting_epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "latest_committed_epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "collecting_epoch", + "latest_committed_epoch" + ] + } + }, + "required": [ + "committed_configuration_lost" + ], + "additionalProperties": false + }, + { + "description": "Decrypting the encrypted rack secrets failed when presented with a `valid` RackSecret.\n\n`Configuration` membership contains the hashes of each valid share. All shares utilized to reconstruct the rack secret were validated against these hashes, and the rack secret was reconstructed. However, using the rack secret to derive encryption keys and decrypt the secrets from old configurations still failed. This should never be possible, and therefore we raise an alarm.", + "type": "object", + "properties": { + "rack_secret_decryption_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/DecryptionError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "rack_secret_decryption_failed" + ], + "additionalProperties": false + }, + { + "description": "Reconstructing the rack secret failed when presented with `valid` shares.\n\n`Configuration` membership contains the hashes of each valid share. All shares utilized to reconstruct the rack secret were validated against these hashes, and yet, the reconstruction still failed. This indicates either a bit flip in a share after validation, or, more likely, an invalid hash.", + "type": "object", + "properties": { + "rack_secret_reconstruction_failed": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "err": { + "$ref": "#/components/schemas/RackSecretReconstructError" + } + }, + "required": [ + "epoch", + "err" + ] + } + }, + "required": [ + "rack_secret_reconstruction_failed" + ], + "additionalProperties": false + } + ] + }, "ArtifactConfig": { "description": "Artifact configuration.\n\nThis type is used in both GET (response) and PUT (request) operations.", "type": "object", @@ -2767,7 +2912,7 @@ ] }, "BaseboardId": { - "description": "A representation of a Baseboard ID as used in the inventory subsystem This type is essentially the same as a `Baseboard` except it doesn't have a revision or HW type (Gimlet, PC, Unknown).", + "description": "A representation of a Baseboard ID as used in the inventory subsystem.\n\nThis type is essentially the same as a `Baseboard` except it doesn't have a revision or HW type (Gimlet, PC, Unknown).", "type": "object", "properties": { "part_number": { @@ -3514,6 +3659,23 @@ } ] }, + "CombineError": { + "type": "string", + "enum": [ + "too_few_shares", + "duplicate_x_coordinates", + "invalid_share_lengths", + "invalid_share_id" + ] + }, + "CommitStatus": { + "description": "Whether or not a configuration has been committed or is still underway.", + "type": "string", + "enum": [ + "committed", + "pending" + ] + }, "ComponentV0": { "oneOf": [ { @@ -4120,6 +4282,105 @@ } ] }, + "Configuration": { + "description": "The configuration for a given epoch.\n\nOnly valid for non-lrtq configurations.", + "type": "object", + "properties": { + "coordinator": { + "description": "Who was the coordinator of this reconfiguration?", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "encrypted_rack_secrets": { + "nullable": true, + "description": "There are no encrypted rack secrets for the initial configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/EncryptedRackSecrets" + } + ] + }, + "epoch": { + "description": "Unique, monotonically increasing identifier for a configuration.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "description": "All members of the current configuration and the hash of their key shares.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigurationMember" + } + }, + "rack_id": { + "description": "Unique Id of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + }, + "threshold": { + "description": "The number of sleds required to reconstruct the rack secret.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "coordinator", + "epoch", + "members", + "rack_id", + "threshold" + ] + }, + "ConfigurationMember": { + "description": "A member entry in a trust quorum configuration.\n\nThis type is used for OpenAPI schema generation since OpenAPI v3.0.x doesn't support tuple arrays.", + "type": "object", + "properties": { + "id": { + "description": "The baseboard ID of the member.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "share_digest": { + "description": "The SHA3-256 hash of the member's key share.", + "type": "string" + } + }, + "required": [ + "id", + "share_digest" + ] + }, + "CoordinatorStatus": { + "description": "Status of the node coordinating the reconfiguration or LRTQ upgrade.", + "type": "object", + "properties": { + "acked_prepares": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "config": { + "$ref": "#/components/schemas/Configuration" + } + }, + "required": [ + "acked_prepares", + "config" + ] + }, "Cpuid": { "description": "A set of CPUID values to expose to a guest.", "type": "object", @@ -4305,6 +4566,25 @@ "type": "string", "format": "uuid" }, + "DecryptionError": { + "description": "Error decrypting rack secrets.", + "oneOf": [ + { + "description": "An opaque error indicating decryption failed.", + "type": "string", + "enum": [ + "aead" + ] + }, + { + "description": "The length of the plaintext is not the correct size and cannot be decoded.", + "type": "string", + "enum": [ + "invalid_length" + ] + } + ] + }, "DelegatedZvol": { "description": "Delegate a ZFS volume to a zone", "oneOf": [ @@ -4795,6 +5075,23 @@ "ntp_servers" ] }, + "EncryptedRackSecrets": { + "description": "All possibly relevant __encrypted__ rack secrets for _prior_ committed configurations.", + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "salt": { + "description": "A random value used to derive the key to encrypt the rack secrets for prior committed epochs.", + "type": "string" + } + }, + "required": [ + "data", + "salt" + ] + }, "Error": { "description": "Error information from a response.", "type": "object", @@ -4830,14 +5127,38 @@ "baseboard" ] }, - "ExternalIp": { - "description": "An external IP address used by a probe.", + "ExpungedMetadata": { + "description": "Metadata about a node being expunged from the trust quorum.", "type": "object", "properties": { - "first_port": { - "description": "The first port used by the address.", + "epoch": { + "description": "The committed epoch, later than its current configuration at which the node learned that it had been expunged.", "type": "integer", - "format": "uint16", + "format": "uint64", + "minimum": 0 + }, + "from": { + "description": "Which node this commit information was learned from.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + } + }, + "required": [ + "epoch", + "from" + ] + }, + "ExternalIp": { + "description": "An external IP address used by a probe.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used by the address.", + "type": "integer", + "format": "uint16", "minimum": 0 }, "ip": { @@ -5712,6 +6033,13 @@ } ] }, + "InvalidRackSecretSizeError": { + "description": "Error indicating the rack secret has an invalid size.", + "type": "string", + "enum": [ + null + ] + }, "Inventory": { "description": "Identity and basic status information about this sled agent", "type": "object", @@ -6609,6 +6937,90 @@ } ] }, + "NodePersistentStateSummary": { + "description": "A summary of a node's persistent state.", + "type": "object", + "properties": { + "commits": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + }, + "configs": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + }, + "expunged": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ExpungedMetadata" + } + ] + }, + "has_lrtq_share": { + "type": "boolean" + }, + "shares": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "uniqueItems": true + } + }, + "required": [ + "commits", + "configs", + "has_lrtq_share", + "shares" + ] + }, + "NodeStatus": { + "description": "Details about a given node's status.", + "type": "object", + "properties": { + "alarms": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Alarm" + }, + "uniqueItems": true + }, + "connected_peers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "persistent_state": { + "$ref": "#/components/schemas/NodePersistentStateSummary" + }, + "proxied_requests": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "alarms", + "connected_peers", + "persistent_state", + "proxied_requests" + ] + }, "NvmeDisk": { "description": "A disk that presents an NVMe interface to the guest.", "type": "object", @@ -7835,6 +8247,35 @@ "rack_subnet" ] }, + "RackSecretReconstructError": { + "description": "Error reconstructing a rack secret from shares.", + "oneOf": [ + { + "type": "object", + "properties": { + "combine": { + "$ref": "#/components/schemas/CombineError" + } + }, + "required": [ + "combine" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "size": { + "$ref": "#/components/schemas/InvalidRackSecretSizeError" + } + }, + "required": [ + "size" + ], + "additionalProperties": false + } + ] + }, "RackUuid": { "x-rust-type": { "crate": "omicron-uuid-kinds", @@ -8791,479 +9232,68 @@ "uplinks" ] }, - "TrustQuorumAlarm": { - "description": "An alarm indicating a protocol invariant violation in trust quorum.", - "oneOf": [ - { - "description": "Different configurations found for the same epoch.", - "type": "object", - "properties": { - "mismatched_configurations": { - "type": "object", - "properties": { - "config1": { - "description": "The first configuration.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumConfiguration" - } - ] - }, - "config2": { - "description": "The second (mismatched) configuration.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumConfiguration" - } - ] - }, - "from": { - "description": "The source of the mismatch (either a baseboard ID or \"Nexus\").", - "type": "string" - } - }, - "required": [ - "config1", - "config2", - "from" - ] - } - }, - "required": [ - "mismatched_configurations" - ], - "additionalProperties": false + "TrustQuorumCommitRequest": { + "description": "Request to commit a trust quorum configuration at a given epoch.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 }, - { - "description": "The key share computer could not compute this node's share.", - "type": "object", - "properties": { - "share_computation_failed": { - "type": "object", - "properties": { - "epoch": { - "description": "The epoch for which share computation failed.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "error": { - "description": "The error message.", - "type": "string" - } - }, - "required": [ - "epoch", - "error" - ] - } - }, - "required": [ - "share_computation_failed" - ], - "additionalProperties": false + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + } + }, + "required": [ + "epoch", + "rack_id" + ] + }, + "TrustQuorumLrtqUpgradeRequest": { + "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 }, - { - "description": "We started collecting shares for a committed configuration, but we no longer have that configuration in our persistent state.", - "type": "object", - "properties": { - "committed_configuration_lost": { - "type": "object", - "properties": { - "collecting_epoch": { - "description": "The epoch we were collecting shares for.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "latest_committed_epoch": { - "description": "The latest committed epoch.", - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "collecting_epoch", - "latest_committed_epoch" - ] - } + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" }, - "required": [ - "committed_configuration_lost" - ], - "additionalProperties": false - }, - { - "description": "Decrypting the encrypted rack secrets failed when presented with a valid rack secret.", - "type": "object", - "properties": { - "rack_secret_decryption_failed": { - "type": "object", - "properties": { - "epoch": { - "description": "The epoch for which decryption failed.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "error": { - "description": "The error message.", - "type": "string" - } - }, - "required": [ - "epoch", - "error" - ] - } - }, - "required": [ - "rack_secret_decryption_failed" - ], - "additionalProperties": false - }, - { - "description": "Reconstructing the rack secret failed when presented with valid shares.", - "type": "object", - "properties": { - "rack_secret_reconstruction_failed": { - "type": "object", - "properties": { - "epoch": { - "description": "The epoch for which reconstruction failed.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "error": { - "description": "The error message.", - "type": "string" - } - }, - "required": [ - "epoch", - "error" - ] - } - }, - "required": [ - "rack_secret_reconstruction_failed" - ], - "additionalProperties": false - } - ] - }, - "TrustQuorumCommitRequest": { - "description": "Request to commit a trust quorum configuration at a given epoch.", - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 + "uniqueItems": true }, "rack_id": { "$ref": "#/components/schemas/RackUuid" - } - }, - "required": [ - "epoch", - "rack_id" - ] - }, - "TrustQuorumCommitResponse": { - "description": "Response indicating the commit status.", - "oneOf": [ - { - "description": "The configuration has been committed.", - "type": "string", - "enum": [ - "committed" - ] - }, - { - "description": "The commit is still pending.", - "type": "string", - "enum": [ - "pending" - ] - } - ] - }, - "TrustQuorumConfiguration": { - "description": "A trust quorum configuration.", - "type": "object", - "properties": { - "coordinator": { - "description": "The coordinator of this reconfiguration.", - "allOf": [ - { - "$ref": "#/components/schemas/BaseboardId" - } - ] - }, - "epoch": { - "description": "Unique, monotonically increasing identifier for a configuration.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "description": "All members of the configuration and the SHA3-256 hash of their key shares.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "rack_id": { - "description": "Unique ID of the rack.", - "allOf": [ - { - "$ref": "#/components/schemas/RackUuid" - } - ] }, "threshold": { - "description": "The number of sleds required to reconstruct the rack secret.", "type": "integer", "format": "uint8", "minimum": 0 } }, "required": [ - "coordinator", "epoch", "members", "rack_id", "threshold" ] }, - "TrustQuorumCoordinatorStatus": { - "description": "Status of a node coordinating a trust quorum reconfiguration.", + "TrustQuorumPrepareAndCommitRequest": { + "description": "Request to prepare and commit a trust quorum configuration.\n\nThis is the `Configuration` sent to a node that missed the `Prepare` phase.", "type": "object", "properties": { - "acked_prepares": { - "description": "The set of nodes that have acknowledged the prepare.", - "type": "array", - "items": { - "$ref": "#/components/schemas/BaseboardId" - }, - "uniqueItems": true - }, "config": { - "description": "The configuration being prepared.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumConfiguration" - } - ] + "$ref": "#/components/schemas/Configuration" } }, "required": [ - "acked_prepares", "config" ] }, - "TrustQuorumEncryptedRackSecrets": { - "description": "Encrypted rack secrets for prior configurations.", - "type": "object", - "properties": { - "data": { - "description": "Encrypted data.", - "type": "string" - }, - "salt": { - "description": "32-byte salt used to derive the encryption key.", - "type": "string" - } - }, - "required": [ - "data", - "salt" - ] - }, - "TrustQuorumLrtqUpgradeRequest": { - "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BaseboardId" - }, - "uniqueItems": true - }, - "rack_id": { - "$ref": "#/components/schemas/RackUuid" - }, - "threshold": { - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "epoch", - "members", - "rack_id", - "threshold" - ] - }, - "TrustQuorumNodeStatus": { - "description": "Status of a trust quorum node, returned from a proxied status request.", - "type": "object", - "properties": { - "alarms": { - "description": "Any alarms raised by this node.", - "type": "array", - "items": { - "$ref": "#/components/schemas/TrustQuorumAlarm" - } - }, - "connected_peers": { - "description": "The peers this node is connected to.", - "type": "array", - "items": { - "$ref": "#/components/schemas/BaseboardId" - }, - "uniqueItems": true - }, - "persistent_state": { - "description": "Summary of the node's persistent state.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumPersistentStateSummary" - } - ] - }, - "proxied_requests": { - "description": "Number of proxied requests currently in flight.", - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "alarms", - "connected_peers", - "persistent_state", - "proxied_requests" - ] - }, - "TrustQuorumPersistentStateSummary": { - "description": "Summary of a trust quorum node's persistent state.", - "type": "object", - "properties": { - "commits": { - "description": "Epochs that have been committed.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "uniqueItems": true - }, - "configs": { - "description": "Epochs for which configurations have been prepared.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "uniqueItems": true - }, - "expunged": { - "description": "Whether this node has been expunged from the quorum.", - "type": "boolean" - }, - "has_lrtq_share": { - "description": "Whether the node has an LRTQ (legacy) share.", - "type": "boolean" - }, - "shares": { - "description": "Epochs for which key shares exist.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "uniqueItems": true - } - }, - "required": [ - "commits", - "configs", - "expunged", - "has_lrtq_share", - "shares" - ] - }, - "TrustQuorumPrepareAndCommitRequest": { - "description": "Request to prepare and commit a trust quorum configuration.", - "type": "object", - "properties": { - "coordinator": { - "description": "The coordinator of this reconfiguration.", - "allOf": [ - { - "$ref": "#/components/schemas/BaseboardId" - } - ] - }, - "encrypted_rack_secrets": { - "nullable": true, - "description": "Encrypted rack secrets from prior configurations, if any.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumEncryptedRackSecrets" - } - ] - }, - "epoch": { - "description": "Unique, monotonically increasing identifier for a configuration.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "description": "All members of the configuration and the SHA3-256 hash of their key shares.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "rack_id": { - "description": "Unique ID of the rack.", - "allOf": [ - { - "$ref": "#/components/schemas/RackUuid" - } - ] - }, - "threshold": { - "description": "The number of sleds required to reconstruct the rack secret.", - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "coordinator", - "epoch", - "members", - "rack_id", - "threshold" - ] - }, "TrustQuorumProxyCommitRequest": { "description": "Request to proxy a commit operation to another trust quorum node.", "type": "object", @@ -9301,11 +9331,11 @@ "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", "type": "object", "properties": { - "coordinator": { - "description": "The coordinator of this reconfiguration.", + "config": { + "description": "The configuration to prepare and commit.", "allOf": [ { - "$ref": "#/components/schemas/BaseboardId" + "$ref": "#/components/schemas/Configuration" } ] }, @@ -9316,51 +9346,11 @@ "$ref": "#/components/schemas/BaseboardId" } ] - }, - "encrypted_rack_secrets": { - "nullable": true, - "description": "Encrypted rack secrets from prior configurations, if any.", - "allOf": [ - { - "$ref": "#/components/schemas/TrustQuorumEncryptedRackSecrets" - } - ] - }, - "epoch": { - "description": "Unique, monotonically increasing identifier for a configuration.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "description": "All members of the configuration and the SHA3-256 hash of their key shares.", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "rack_id": { - "description": "Unique ID of the rack.", - "allOf": [ - { - "$ref": "#/components/schemas/RackUuid" - } - ] - }, - "threshold": { - "description": "The number of sleds required to reconstruct the rack secret.", - "type": "integer", - "format": "uint8", - "minimum": 0 } }, "required": [ - "coordinator", - "destination", - "epoch", - "members", - "rack_id", - "threshold" + "config", + "destination" ] }, "TrustQuorumProxyStatusRequest": { diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 613aef2c002..2518ddb6e44 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-13.0.0-25c5af.json \ No newline at end of file +sled-agent-13.0.0-065232.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 522ecbe1d76..c3646624cc2 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -1099,7 +1099,7 @@ pub trait SledAgentApi { request_context: RequestContext, body: TypedBody, ) -> Result< - HttpResponseOk, + HttpResponseOk, HttpError, >; @@ -1112,9 +1112,7 @@ pub trait SledAgentApi { async fn trust_quorum_coordinator_status( request_context: RequestContext, ) -> Result< - HttpResponseOk< - Option, - >, + HttpResponseOk>, HttpError, >; @@ -1130,7 +1128,7 @@ pub trait SledAgentApi { latest::trust_quorum::TrustQuorumPrepareAndCommitRequest, >, ) -> Result< - HttpResponseOk, + HttpResponseOk, HttpError, >; @@ -1144,7 +1142,7 @@ pub trait SledAgentApi { request_context: RequestContext, body: TypedBody, ) -> Result< - HttpResponseOk, + HttpResponseOk, HttpError, >; @@ -1160,7 +1158,7 @@ pub trait SledAgentApi { latest::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest, >, ) -> Result< - HttpResponseOk, + HttpResponseOk, HttpError, >; @@ -1174,7 +1172,7 @@ pub trait SledAgentApi { request_context: RequestContext, body: TypedBody, ) -> Result< - HttpResponseOk, + HttpResponseOk, HttpError, >; } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 8300885655f..79ab2c24441 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -56,10 +56,8 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - TrustQuorumAlarm, TrustQuorumCommitRequest, TrustQuorumCommitResponse, - TrustQuorumConfiguration, TrustQuorumCoordinatorStatus, - TrustQuorumLrtqUpgradeRequest, TrustQuorumNodeStatus, - TrustQuorumPersistentStateSummary, TrustQuorumPrepareAndCommitRequest, + CommitStatus, CoordinatorStatus, NodeStatus, TrustQuorumCommitRequest, + TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, }; @@ -1196,12 +1194,10 @@ impl SledAgentApi for SledAgentImpl { let msg = trust_quorum_protocol::ReconfigureMsg { rack_id: request.rack_id, - epoch: trust_quorum_protocol::Epoch(request.epoch), - last_committed_epoch: request - .last_committed_epoch - .map(trust_quorum_protocol::Epoch), + epoch: request.epoch, + last_committed_epoch: request.last_committed_epoch, members: request.members, - threshold: trust_quorum_protocol::Threshold(request.threshold), + threshold: request.threshold, }; sa.trust_quorum() @@ -1221,9 +1217,9 @@ impl SledAgentApi for SledAgentImpl { let msg = trust_quorum_protocol::LrtqUpgradeMsg { rack_id: request.rack_id, - epoch: trust_quorum_protocol::Epoch(request.epoch), + epoch: request.epoch, members: request.members, - threshold: trust_quorum_protocol::Threshold(request.threshold), + threshold: request.threshold, }; sa.trust_quorum() @@ -1237,35 +1233,22 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_commit( request_context: RequestContext, body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); let status = sa .trust_quorum() - .commit( - request.rack_id, - trust_quorum_protocol::Epoch(request.epoch), - ) + .commit(request.rack_id, request.epoch) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let response = match status { - trust_quorum::CommitStatus::Committed => { - TrustQuorumCommitResponse::Committed - } - trust_quorum::CommitStatus::Pending => { - TrustQuorumCommitResponse::Pending - } - }; - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } async fn trust_quorum_coordinator_status( request_context: RequestContext, - ) -> Result>, HttpError> - { + ) -> Result>, HttpError> { let sa = request_context.context(); let status = sa @@ -1274,156 +1257,63 @@ impl SledAgentApi for SledAgentImpl { .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let response = status.map(|s| TrustQuorumCoordinatorStatus { - config: TrustQuorumConfiguration { - rack_id: s.config.rack_id, - epoch: s.config.epoch.0, - coordinator: s.config.coordinator, - members: s - .config - .members - .into_iter() - .map(|(id, digest)| (id, digest.0)) - .collect(), - threshold: s.config.threshold.0, - }, - acked_prepares: s.acked_prepares, - }); - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } async fn trust_quorum_prepare_and_commit( request_context: RequestContext, body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); - let members = request - .members - .into_iter() - .map(|(id, digest)| (id, trust_quorum_protocol::Sha3_256Digest(digest))) - .collect(); - - let encrypted_rack_secrets = - request.encrypted_rack_secrets.map(|ers| { - trust_quorum_protocol::EncryptedRackSecrets::new( - trust_quorum_protocol::Salt(ers.salt), - ers.data.into_boxed_slice(), - ) - }); - - let config = trust_quorum_protocol::Configuration { - rack_id: request.rack_id, - epoch: trust_quorum_protocol::Epoch(request.epoch), - coordinator: request.coordinator, - members, - threshold: trust_quorum_protocol::Threshold(request.threshold), - encrypted_rack_secrets, - }; - let status = sa .trust_quorum() - .prepare_and_commit(config) + .prepare_and_commit(request.config) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let response = match status { - trust_quorum::CommitStatus::Committed => { - TrustQuorumCommitResponse::Committed - } - trust_quorum::CommitStatus::Pending => { - TrustQuorumCommitResponse::Pending - } - }; - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } async fn trust_quorum_proxy_commit( request_context: RequestContext, body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); let status = sa .trust_quorum() .proxy() - .commit( - request.destination, - request.rack_id, - trust_quorum_protocol::Epoch(request.epoch), - ) + .commit(request.destination, request.rack_id, request.epoch) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let response = match status { - trust_quorum::CommitStatus::Committed => { - TrustQuorumCommitResponse::Committed - } - trust_quorum::CommitStatus::Pending => { - TrustQuorumCommitResponse::Pending - } - }; - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } async fn trust_quorum_proxy_prepare_and_commit( request_context: RequestContext, body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); - let members = request - .members - .into_iter() - .map(|(id, digest)| (id, trust_quorum_protocol::Sha3_256Digest(digest))) - .collect(); - - let encrypted_rack_secrets = - request.encrypted_rack_secrets.map(|ers| { - trust_quorum_protocol::EncryptedRackSecrets::new( - trust_quorum_protocol::Salt(ers.salt), - ers.data.into_boxed_slice(), - ) - }); - - let config = trust_quorum_protocol::Configuration { - rack_id: request.rack_id, - epoch: trust_quorum_protocol::Epoch(request.epoch), - coordinator: request.coordinator, - members, - threshold: trust_quorum_protocol::Threshold(request.threshold), - encrypted_rack_secrets, - }; - let status = sa .trust_quorum() .proxy() - .prepare_and_commit(request.destination, config) + .prepare_and_commit(request.destination, request.config) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let response = match status { - trust_quorum::CommitStatus::Committed => { - TrustQuorumCommitResponse::Committed - } - trust_quorum::CommitStatus::Pending => { - TrustQuorumCommitResponse::Pending - } - }; - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } async fn trust_quorum_proxy_status( request_context: RequestContext, body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); @@ -1434,98 +1324,6 @@ impl SledAgentApi for SledAgentImpl { .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - // We can't define a `From` impl between Alarm (the internal protocol type) and - // TrustQuorumAlarm (the external API type) due to orphan rules, and this conversion is only - // used here to bridge the API to the underlying trust-quorum types, so we'll just define a - // couple free functions in this scope for conversion: - - fn convert_configuration( - config: trust_quorum_protocol::Configuration, - ) -> TrustQuorumConfiguration { - TrustQuorumConfiguration { - rack_id: config.rack_id, - epoch: config.epoch.0, - coordinator: config.coordinator, - members: config - .members - .into_iter() - .map(|(id, digest)| (id, digest.0)) - .collect(), - threshold: config.threshold.0, - } - } - - fn convert_alarm( - alarm: trust_quorum_protocol::Alarm, - ) -> TrustQuorumAlarm { - match alarm { - trust_quorum_protocol::Alarm::MismatchedConfigurations { - config1, - config2, - from, - } => TrustQuorumAlarm::MismatchedConfigurations { - config1: convert_configuration(config1), - config2: convert_configuration(config2), - from, - }, - trust_quorum_protocol::Alarm::ShareComputationFailed { epoch, err } => { - TrustQuorumAlarm::ShareComputationFailed { - epoch: epoch.0, - error: err.to_string(), - } - } - trust_quorum_protocol::Alarm::CommittedConfigurationLost { - latest_committed_epoch, - collecting_epoch, - } => TrustQuorumAlarm::CommittedConfigurationLost { - latest_committed_epoch: latest_committed_epoch.0, - collecting_epoch: collecting_epoch.0, - }, - trust_quorum_protocol::Alarm::RackSecretDecryptionFailed { - epoch, - err, - } => TrustQuorumAlarm::RackSecretDecryptionFailed { - epoch: epoch.0, - error: err.to_string(), - }, - trust_quorum_protocol::Alarm::RackSecretReconstructionFailed { - epoch, - err, - } => TrustQuorumAlarm::RackSecretReconstructionFailed { - epoch: epoch.0, - error: err.to_string(), - }, - } - } - - let response = TrustQuorumNodeStatus { - connected_peers: status.connected_peers, - alarms: status.alarms.into_iter().map(convert_alarm).collect(), - persistent_state: TrustQuorumPersistentStateSummary { - has_lrtq_share: status.persistent_state.has_lrtq_share, - configs: status - .persistent_state - .configs - .into_iter() - .map(|e| e.0) - .collect(), - shares: status - .persistent_state - .shares - .into_iter() - .map(|e| e.0) - .collect(), - commits: status - .persistent_state - .commits - .into_iter() - .map(|e| e.0) - .collect(), - expunged: status.persistent_state.expunged.is_some(), - }, - proxied_requests: status.proxied_requests, - }; - - Ok(HttpResponseOk(response)) + Ok(HttpResponseOk(status)) } } diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 4c335edf43e..484721ed525 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -68,9 +68,8 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - TrustQuorumCommitRequest, TrustQuorumCommitResponse, - TrustQuorumCoordinatorStatus, TrustQuorumLrtqUpgradeRequest, - TrustQuorumNodeStatus, TrustQuorumPrepareAndCommitRequest, + CommitStatus, CoordinatorStatus, NodeStatus, TrustQuorumCommitRequest, + TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, }; @@ -947,13 +946,13 @@ impl SledAgentApi for SledAgentSimImpl { async fn trust_quorum_commit( _request_context: RequestContext, _body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_coordinator_status( _request_context: RequestContext, - ) -> Result>, HttpError> + ) -> Result>, HttpError> { method_unimplemented() } @@ -961,28 +960,28 @@ impl SledAgentApi for SledAgentSimImpl { async fn trust_quorum_prepare_and_commit( _request_context: RequestContext, _body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_proxy_commit( _request_context: RequestContext, _body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_proxy_prepare_and_commit( _request_context: RequestContext, _body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_proxy_status( _request_context: RequestContext, _body: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { method_unimplemented() } } diff --git a/sled-agent/types/versions/Cargo.toml b/sled-agent/types/versions/Cargo.toml index f2a3ee26b2e..3cf988b53b3 100644 --- a/sled-agent/types/versions/Cargo.toml +++ b/sled-agent/types/versions/Cargo.toml @@ -33,6 +33,7 @@ sled-hardware-types.workspace = true strum.workspace = true test-strategy = { workspace = true, optional = true } thiserror.workspace = true +trust-quorum-types-versions.workspace = true tufaceous-artifact.workspace = true uuid.workspace = true diff --git a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs index f0db0ba50a7..08c305dd501 100644 --- a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs +++ b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs @@ -3,111 +3,61 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Trust quorum types for the Sled Agent API. +//! +//! Core types are re-exported from `trust-quorum-types-versions` to ensure +//! consistency with the trust quorum protocol implementation. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use omicron_uuid_kinds::RackUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_with::{hex::Hex, serde_as}; use super::super::v1::sled::BaseboardId; +// Re-export core types from trust-quorum-types-versions. +// These are the canonical type definitions used by the trust quorum protocol. +pub use trust_quorum_types_versions::v1::alarm::Alarm; +pub use trust_quorum_types_versions::v1::configuration::Configuration; +pub use trust_quorum_types_versions::v1::crypto::EncryptedRackSecrets; +pub use trust_quorum_types_versions::v1::persistent_state::ExpungedMetadata; +pub use trust_quorum_types_versions::v1::status::{ + CommitStatus, CoordinatorStatus, NodePersistentStateSummary, NodeStatus, +}; +pub use trust_quorum_types_versions::v1::types::{Epoch, Threshold}; + /// Reconfigure message for trust quorum changes. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumReconfigureRequest { pub rack_id: RackUuid, - pub epoch: u64, - pub last_committed_epoch: Option, + pub epoch: Epoch, + pub last_committed_epoch: Option, pub members: BTreeSet, - pub threshold: u8, + pub threshold: Threshold, } /// Request to upgrade from LRTQ (Legacy Rack Trust Quorum). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumLrtqUpgradeRequest { pub rack_id: RackUuid, - pub epoch: u64, + pub epoch: Epoch, pub members: BTreeSet, - pub threshold: u8, + pub threshold: Threshold, } /// Request to commit a trust quorum configuration at a given epoch. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumCommitRequest { pub rack_id: RackUuid, - pub epoch: u64, -} - -/// Response indicating the commit status. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TrustQuorumCommitResponse { - /// The configuration has been committed. - Committed, - /// The commit is still pending. - Pending, -} - -/// Status of a node coordinating a trust quorum reconfiguration. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumCoordinatorStatus { - /// The configuration being prepared. - pub config: TrustQuorumConfiguration, - /// The set of nodes that have acknowledged the prepare. - pub acked_prepares: BTreeSet, -} - -/// A trust quorum configuration. -#[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumConfiguration { - /// Unique ID of the rack. - pub rack_id: RackUuid, - /// Unique, monotonically increasing identifier for a configuration. - pub epoch: u64, - /// The coordinator of this reconfiguration. - pub coordinator: BaseboardId, - /// All members of the configuration and the SHA3-256 hash of their key shares. - #[serde_as(as = "BTreeMap<_, Hex>")] - #[schemars(with = "BTreeMap")] - pub members: BTreeMap, - /// The number of sleds required to reconstruct the rack secret. - pub threshold: u8, + pub epoch: Epoch, } /// Request to prepare and commit a trust quorum configuration. -#[serde_as] +/// +/// This is the `Configuration` sent to a node that missed the `Prepare` phase. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumPrepareAndCommitRequest { - /// Unique ID of the rack. - pub rack_id: RackUuid, - /// Unique, monotonically increasing identifier for a configuration. - pub epoch: u64, - /// The coordinator of this reconfiguration. - pub coordinator: BaseboardId, - /// All members of the configuration and the SHA3-256 hash of their key shares. - #[serde_as(as = "BTreeMap<_, Hex>")] - #[schemars(with = "BTreeMap")] - pub members: BTreeMap, - /// The number of sleds required to reconstruct the rack secret. - pub threshold: u8, - /// Encrypted rack secrets from prior configurations, if any. - pub encrypted_rack_secrets: Option, -} - -/// Encrypted rack secrets for prior configurations. -#[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumEncryptedRackSecrets { - /// 32-byte salt used to derive the encryption key. - #[serde_as(as = "Hex")] - #[schemars(with = "String")] - pub salt: [u8; 32], - /// Encrypted data. - #[serde_as(as = "Hex")] - #[schemars(with = "String")] - pub data: Vec, + pub config: Configuration, } /// Request to proxy a commit operation to another trust quorum node. @@ -118,29 +68,16 @@ pub struct TrustQuorumProxyCommitRequest { /// Unique ID of the rack. pub rack_id: RackUuid, /// The epoch to commit. - pub epoch: u64, + pub epoch: Epoch, } /// Request to proxy a prepare-and-commit operation to another trust quorum node. -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct TrustQuorumProxyPrepareAndCommitRequest { /// The target node to proxy the request to. pub destination: BaseboardId, - /// Unique ID of the rack. - pub rack_id: RackUuid, - /// Unique, monotonically increasing identifier for a configuration. - pub epoch: u64, - /// The coordinator of this reconfiguration. - pub coordinator: BaseboardId, - /// All members of the configuration and the SHA3-256 hash of their key shares. - #[serde_as(as = "BTreeMap<_, Hex>")] - #[schemars(with = "BTreeMap")] - pub members: BTreeMap, - /// The number of sleds required to reconstruct the rack secret. - pub threshold: u8, - /// Encrypted rack secrets from prior configurations, if any. - pub encrypted_rack_secrets: Option, + /// The configuration to prepare and commit. + pub config: Configuration, } /// Request to proxy a status request to another trust quorum node. @@ -149,80 +86,3 @@ pub struct TrustQuorumProxyStatusRequest { /// The target node to get the status from. pub destination: BaseboardId, } - -/// Status of a trust quorum node, returned from a proxied status request. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumNodeStatus { - /// The peers this node is connected to. - pub connected_peers: BTreeSet, - /// Any alarms raised by this node. - pub alarms: Vec, - /// Summary of the node's persistent state. - pub persistent_state: TrustQuorumPersistentStateSummary, - /// Number of proxied requests currently in flight. - pub proxied_requests: u64, -} - -/// An alarm indicating a protocol invariant violation in trust quorum. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TrustQuorumAlarm { - /// Different configurations found for the same epoch. - MismatchedConfigurations { - /// The first configuration. - config1: TrustQuorumConfiguration, - /// The second (mismatched) configuration. - config2: TrustQuorumConfiguration, - /// The source of the mismatch (either a baseboard ID or "Nexus"). - from: String, - }, - - /// The key share computer could not compute this node's share. - ShareComputationFailed { - /// The epoch for which share computation failed. - epoch: u64, - /// The error message. - error: String, - }, - - /// We started collecting shares for a committed configuration, - /// but we no longer have that configuration in our persistent state. - CommittedConfigurationLost { - /// The latest committed epoch. - latest_committed_epoch: u64, - /// The epoch we were collecting shares for. - collecting_epoch: u64, - }, - - /// Decrypting the encrypted rack secrets failed when presented with a - /// valid rack secret. - RackSecretDecryptionFailed { - /// The epoch for which decryption failed. - epoch: u64, - /// The error message. - error: String, - }, - - /// Reconstructing the rack secret failed when presented with valid shares. - RackSecretReconstructionFailed { - /// The epoch for which reconstruction failed. - epoch: u64, - /// The error message. - error: String, - }, -} - -/// Summary of a trust quorum node's persistent state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumPersistentStateSummary { - /// Whether the node has an LRTQ (legacy) share. - pub has_lrtq_share: bool, - /// Epochs for which configurations have been prepared. - pub configs: BTreeSet, - /// Epochs for which key shares exist. - pub shares: BTreeSet, - /// Epochs that have been committed. - pub commits: BTreeSet, - /// Whether this node has been expunged from the quorum. - pub expunged: bool, -} diff --git a/sled-agent/types/versions/src/initial/sled.rs b/sled-agent/types/versions/src/initial/sled.rs index 8d2fda2e707..ccc98859024 100644 --- a/sled-agent/types/versions/src/initial/sled.rs +++ b/sled-agent/types/versions/src/initial/sled.rs @@ -5,7 +5,6 @@ //! Sled-related types for the Sled Agent API. use async_trait::async_trait; -use daft::Diffable; use omicron_common::address::{Ipv6Subnet, SLED_PREFIX}; use omicron_common::ledger::Ledgerable; use omicron_uuid_kinds::SledUuid; @@ -13,35 +12,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -/// A representation of a Baseboard ID as used in the inventory subsystem -/// This type is essentially the same as a `Baseboard` except it doesn't have a -/// revision or HW type (Gimlet, PC, Unknown). -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - JsonSchema, - Diffable, -)] -#[daft(leaf)] -pub struct BaseboardId { - /// Oxide Part Number - pub part_number: String, - /// Serial number (unique for a given part number) - pub serial_number: String, -} - -impl std::fmt::Display for BaseboardId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.part_number, self.serial_number) - } -} +// Re-export BaseboardId and UnknownBaseboardError from sled-hardware-types +pub use sled_hardware_types::{BaseboardId, UnknownBaseboardError}; /// A request to Add a given sled after rack initialization has occurred #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] @@ -96,31 +68,6 @@ pub struct StartSledAgentRequestBody { pub subnet: Ipv6Subnet, } -#[derive(Debug, thiserror::Error)] -#[error("Baseboard is of unknown type")] -pub struct UnknownBaseboardError; - -impl TryFrom for BaseboardId { - type Error = UnknownBaseboardError; - - fn try_from( - value: sled_hardware_types::Baseboard, - ) -> Result { - use sled_hardware_types::Baseboard; - match value { - Baseboard::Gimlet { identifier, model, .. } => Ok(BaseboardId { - part_number: model, - serial_number: identifier, - }), - Baseboard::Pc { identifier, model } => Ok(BaseboardId { - part_number: model, - serial_number: identifier, - }), - Baseboard::Unknown => Err(UnknownBaseboardError), - } - } -} - #[async_trait] impl Ledgerable for StartSledAgentRequest { fn is_newer_than(&self, other: &Self) -> bool { diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index f0e4069643f..ad1597efa9c 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -156,15 +156,21 @@ pub mod support_bundle { } pub mod trust_quorum { - pub use crate::v13::trust_quorum::TrustQuorumAlarm; + // Core types re-exported from trust-quorum-types-versions + pub use crate::v13::trust_quorum::Alarm; + pub use crate::v13::trust_quorum::CommitStatus; + pub use crate::v13::trust_quorum::Configuration; + pub use crate::v13::trust_quorum::CoordinatorStatus; + pub use crate::v13::trust_quorum::EncryptedRackSecrets; + pub use crate::v13::trust_quorum::Epoch; + pub use crate::v13::trust_quorum::ExpungedMetadata; + pub use crate::v13::trust_quorum::NodePersistentStateSummary; + pub use crate::v13::trust_quorum::NodeStatus; + pub use crate::v13::trust_quorum::Threshold; + + // HTTP request types specific to the sled-agent API pub use crate::v13::trust_quorum::TrustQuorumCommitRequest; - pub use crate::v13::trust_quorum::TrustQuorumCommitResponse; - pub use crate::v13::trust_quorum::TrustQuorumConfiguration; - pub use crate::v13::trust_quorum::TrustQuorumCoordinatorStatus; - pub use crate::v13::trust_quorum::TrustQuorumEncryptedRackSecrets; pub use crate::v13::trust_quorum::TrustQuorumLrtqUpgradeRequest; - pub use crate::v13::trust_quorum::TrustQuorumNodeStatus; - pub use crate::v13::trust_quorum::TrustQuorumPersistentStateSummary; pub use crate::v13::trust_quorum::TrustQuorumPrepareAndCommitRequest; pub use crate::v13::trust_quorum::TrustQuorumProxyCommitRequest; pub use crate::v13::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest; diff --git a/sled-hardware/types/Cargo.toml b/sled-hardware/types/Cargo.toml index cf8f8d6a18b..8e56eb61f4b 100644 --- a/sled-hardware/types/Cargo.toml +++ b/sled-hardware/types/Cargo.toml @@ -8,10 +8,12 @@ license = "MPL-2.0" workspace = true [dependencies] +daft.workspace = true illumos-utils.workspace = true omicron-common.workspace = true schemars.workspace = true serde.workspace = true +thiserror.workspace = true omicron-workspace-hack.workspace = true [dev-dependencies] diff --git a/sled-hardware/types/src/lib.rs b/sled-hardware/types/src/lib.rs index 982db8a45cd..77e8a27509b 100644 --- a/sled-hardware/types/src/lib.rs +++ b/sled-hardware/types/src/lib.rs @@ -2,6 +2,7 @@ // 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 daft::Diffable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::RangeInclusive; @@ -185,6 +186,59 @@ impl std::fmt::Display for Baseboard { } } +/// A representation of a Baseboard ID as used in the inventory subsystem. +/// +/// This type is essentially the same as a `Baseboard` except it doesn't have a +/// revision or HW type (Gimlet, PC, Unknown). +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + JsonSchema, + Diffable, +)] +#[daft(leaf)] +pub struct BaseboardId { + /// Oxide Part Number + pub part_number: String, + /// Serial number (unique for a given part number) + pub serial_number: String, +} + +impl std::fmt::Display for BaseboardId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.part_number, self.serial_number) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Baseboard is of unknown type")] +pub struct UnknownBaseboardError; + +impl TryFrom for BaseboardId { + type Error = UnknownBaseboardError; + + fn try_from(value: Baseboard) -> Result { + match value { + Baseboard::Gimlet { identifier, model, .. } => Ok(BaseboardId { + part_number: model, + serial_number: identifier, + }), + Baseboard::Pc { identifier, model } => Ok(BaseboardId { + part_number: model, + serial_number: identifier, + }), + Baseboard::Unknown => Err(UnknownBaseboardError), + } + } +} + /// Identifies the kind of CPU present on a sled, determined by reading CPUID. /// /// This is intended to broadly support the control plane answering the question diff --git a/trust-quorum/gfss/Cargo.toml b/trust-quorum/gfss/Cargo.toml index e4089294539..785123330dc 100644 --- a/trust-quorum/gfss/Cargo.toml +++ b/trust-quorum/gfss/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] digest.workspace = true rand.workspace = true +schemars.workspace = true secrecy.workspace = true serde.workspace = true subtle.workspace = true diff --git a/trust-quorum/gfss/src/shamir.rs b/trust-quorum/gfss/src/shamir.rs index 49ea0a90a48..0d2f4919d93 100644 --- a/trust-quorum/gfss/src/shamir.rs +++ b/trust-quorum/gfss/src/shamir.rs @@ -7,6 +7,7 @@ use digest::Digest; use rand::TryRngCore; use rand::{Rng, rngs::OsRng}; +use schemars::JsonSchema; use secrecy::SecretBox; use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; @@ -15,7 +16,8 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::gf256::{self, Gf256}; use crate::polynomial::Polynomial; -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub enum SplitError { #[error("splitting requires at least a threshold of 2")] ThresholdToSmall, @@ -33,7 +35,9 @@ pub enum SplitError { Ord, Serialize, Deserialize, + JsonSchema, )] +#[serde(rename_all = "snake_case")] pub enum CombineError { #[error("must be at least 2 shares to combine")] TooFewShares, diff --git a/trust-quorum/protocol/Cargo.toml b/trust-quorum/protocol/Cargo.toml index 443a334ee0c..cbaee171f9e 100644 --- a/trust-quorum/protocol/Cargo.toml +++ b/trust-quorum/protocol/Cargo.toml @@ -32,6 +32,7 @@ slog-error-chain.workspace = true static_assertions.workspace = true subtle.workspace = true thiserror.workspace = true +trust-quorum-types.workspace = true uuid.workspace = true zeroize.workspace = true omicron-workspace-hack.workspace = true diff --git a/trust-quorum/protocol/src/configuration.rs b/trust-quorum/protocol/src/configuration.rs index 2684a86da78..d0e88c699de 100644 --- a/trust-quorum/protocol/src/configuration.rs +++ b/trust-quorum/protocol/src/configuration.rs @@ -2,155 +2,92 @@ // 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/. -//! A configuration of a trust quroum at a given epoch +//! Configuration creation for trust quorum. +//! +//! The `Configuration` type itself is defined in `trust-quorum-types` for API versioning akin to +//! RFD 619. This module provides the `new_configuration` function which creates configurations (and +//! cannot be an inherent method on a foreign type due to `Configuration` being defined in another crate). -use crate::crypto::{EncryptedRackSecrets, RackSecret, Sha3_256Digest}; -use crate::{BaseboardId, Epoch, Threshold}; -use daft::Diffable; -use gfss::shamir::{Share, SplitError}; -use iddqd::{IdOrdItem, id_upcast}; -use omicron_uuid_kinds::RackUuid; +use gfss::shamir::Share; use secrecy::ExposeSecret; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use slog_error_chain::SlogInlineError; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError)] -pub enum ConfigurationError { - #[error("rack secret split error")] - RackSecretSplit( - #[from] - #[source] - SplitError, - ), - #[error("too many members: must be fewer than 255")] - TooManyMembers, -} +use crate::crypto::RackSecret; +use trust_quorum_types::configuration::{ + BaseboardId, Configuration, ConfigurationError, NewConfigParams, +}; +use trust_quorum_types::crypto::Sha3_256Digest; -/// The configuration for a given epoch. +/// Create a new configuration for the trust quorum. /// -/// Only valid for non-lrtq configurations -#[serde_as] -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Diffable, -)] -pub struct Configuration { - /// Unique Id of the rack - pub rack_id: RackUuid, - - // Unique, monotonically increasing identifier for a configuration - pub epoch: Epoch, - - /// Who was the coordinator of this reconfiguration? - pub coordinator: BaseboardId, - - // All members of the current configuration and the hash of their key shares - #[serde_as(as = "Vec<(_, _)>")] - pub members: BTreeMap, - - /// The number of sleds required to reconstruct the rack secret - pub threshold: Threshold, - - // There are no encrypted rack secrets for the initial configuration - pub encrypted_rack_secrets: Option, -} - -impl IdOrdItem for Configuration { - type Key<'a> = Epoch; - - fn key(&self) -> Self::Key<'_> { - self.epoch +/// This is a free function because `Configuration` is defined in +/// `trust-quorum-types` (for API versioning per RFD 619) and Rust doesn't +/// allow adding inherent methods to types from other crates. +/// +/// `previous_configuration` is never filled in upon construction. A +/// coordinator will fill this in as necessary after retrieving shares for +/// the last committed epoch. +pub fn new_configuration( + params: NewConfigParams<'_>, +) -> Result<(Configuration, BTreeMap), ConfigurationError> { + let coordinator = params.coordinator_id.clone(); + let rack_secret = RackSecret::new(); + let shares = rack_secret.split( + params.threshold, + params + .members + .len() + .try_into() + .map_err(|_| ConfigurationError::TooManyMembers)?, + )?; + + let shares_and_digests = shares.shares.expose_secret().iter().map(|s| { + let mut digest = Sha3_256Digest::default(); + s.digest::(&mut digest.0); + (s.clone(), digest) + }); + + let mut members: BTreeMap = BTreeMap::new(); + let mut shares: BTreeMap = BTreeMap::new(); + for (platform_id, (share, digest)) in + params.members.iter().cloned().zip(shares_and_digests) + { + members.insert(platform_id.clone(), digest); + shares.insert(platform_id, share); } - id_upcast!(); + Ok(( + Configuration { + rack_id: params.rack_id, + epoch: params.epoch, + coordinator, + members, + threshold: params.threshold, + encrypted_rack_secrets: None, + }, + shares, + )) } -pub struct NewConfigParams<'a> { - pub rack_id: RackUuid, - pub epoch: Epoch, - pub members: &'a BTreeSet, - pub threshold: Threshold, - pub coordinator_id: &'a BaseboardId, -} - -impl Configuration { - /// Create a new configuration for the trust quorum - /// - /// `previous_configuration` is never filled in upon construction. A - /// coordinator will fill this in as necessary after retrieving shares for - /// the last committed epoch. - pub fn new( - params: NewConfigParams<'_>, - ) -> Result<(Configuration, BTreeMap), ConfigurationError> - { - let coordinator = params.coordinator_id.clone(); - let rack_secret = RackSecret::new(); - let shares = rack_secret.split( - params.threshold, - params - .members - .len() - .try_into() - .map_err(|_| ConfigurationError::TooManyMembers)?, - )?; - - let shares_and_digests = - shares.shares.expose_secret().iter().map(|s| { - let mut digest = Sha3_256Digest::default(); - s.digest::(&mut digest.0); - (s.clone(), digest) - }); - - let mut members: BTreeMap = - BTreeMap::new(); - let mut shares: BTreeMap = BTreeMap::new(); - for (platform_id, (share, digest)) in - params.members.iter().cloned().zip(shares_and_digests) - { - members.insert(platform_id.clone(), digest); - shares.insert(platform_id, share); - } - - Ok(( - Configuration { - rack_id: params.rack_id, - epoch: params.epoch, - coordinator, - members, - threshold: params.threshold, - encrypted_rack_secrets: None, - }, - shares, - )) - } - - #[cfg(feature = "testing")] - pub fn equal_except_for_crypto_data(&self, other: &Self) -> bool { - let encrypted_rack_secrets_match = - match (&self.encrypted_rack_secrets, &other.encrypted_rack_secrets) - { - (None, None) => true, - (Some(_), Some(_)) => true, - _ => false, - }; - self.rack_id == other.rack_id - && self.epoch == other.epoch - && self.coordinator == other.coordinator - && self - .members - .keys() - .zip(other.members.keys()) - .all(|(id1, id2)| id1 == id2) - && self.threshold == other.threshold - && encrypted_rack_secrets_match - } +/// Check if two configurations are equal except for crypto data. +/// +/// This is a free function because `Configuration` is defined in +/// `trust-quorum-types` (for API versioning per RFD 619). +#[cfg(feature = "testing")] +pub fn configurations_equal_except_for_crypto_data( + a: &Configuration, + b: &Configuration, +) -> bool { + let encrypted_rack_secrets_match = + match (&a.encrypted_rack_secrets, &b.encrypted_rack_secrets) { + (None, None) => true, + (Some(_), Some(_)) => true, + _ => false, + }; + a.rack_id == b.rack_id + && a.epoch == b.epoch + && a.coordinator == b.coordinator + && a.members.keys().zip(b.members.keys()).all(|(id1, id2)| id1 == id2) + && a.threshold == b.threshold + && encrypted_rack_secrets_match } diff --git a/trust-quorum/protocol/src/coordinator_state.rs b/trust-quorum/protocol/src/coordinator_state.rs index 725cdfb8397..3b3a5243ee0 100644 --- a/trust-quorum/protocol/src/coordinator_state.rs +++ b/trust-quorum/protocol/src/coordinator_state.rs @@ -5,12 +5,16 @@ //! State of a reconfiguration coordinator inside a [`crate::Node`] use crate::NodeHandlerCtx; -use crate::configuration::{ConfigurationDiff, ConfigurationError}; -use crate::crypto::{LrtqShare, PlaintextRackSecrets, ReconstructedRackSecret}; +use crate::configuration::new_configuration; +use crate::crypto::{ + LrtqShare, PlaintextRackSecrets, ReconstructedRackSecret, decrypt_rack_secrets, +}; +use crate::{ConfigurationError}; use crate::validators::{ ReconfigurationError, ValidatedLrtqUpgradeMsg, ValidatedReconfigureMsg, }; use crate::{BaseboardId, Configuration, Epoch, PeerMsgKind, RackSecret}; +use trust_quorum_types::configuration::ConfigurationDiff; use bootstore::trust_quorum::RackSecret as LrtqRackSecret; use daft::{Diffable, Leaf}; use gfss::shamir::Share; @@ -100,7 +104,7 @@ impl CoordinatorState { { let log = log.new(o!("component" => "tq-coordinator-state")); // Create a configuration for this epoch - let (config, shares) = Configuration::new((&msg).into())?; + let (config, shares) = new_configuration((&msg).into())?; let mut prepares = BTreeMap::new(); // `my_share` is optional only so that we can fill it in via the @@ -145,7 +149,7 @@ impl CoordinatorState { our_latest_committed_share: Share, ) -> Result { let log = log.new(o!("component" => "tq-coordinator-state")); - let (config, new_shares) = Configuration::new((&msg).into())?; + let (config, new_shares) = new_configuration((&msg).into())?; info!( log, @@ -176,7 +180,7 @@ impl CoordinatorState { msg: ValidatedLrtqUpgradeMsg, ) -> Result { let log = log.new(o!("component" => "tq-coordinator-state")); - let (configuration, new_shares) = Configuration::new((&msg).into())?; + let (configuration, new_shares) = new_configuration((&msg).into())?; info!( log, @@ -462,7 +466,8 @@ impl CoordinatorState { let mut plaintext_secrets = if let Some(encrypted_secrets) = &old_config.encrypted_rack_secrets { - match encrypted_secrets.decrypt( + match decrypt_rack_secrets( + encrypted_secrets, old_config.rack_id, old_config.epoch, &old_rack_secret, diff --git a/trust-quorum/protocol/src/crypto.rs b/trust-quorum/protocol/src/crypto.rs index 84ba89c4691..54b5b61415d 100644 --- a/trust-quorum/protocol/src/crypto.rs +++ b/trust-quorum/protocol/src/crypto.rs @@ -2,12 +2,18 @@ // 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/. -//! Various cryptographic constructs used by trust quroum. +//! Internal cryptographic constructs used by trust quorum. +//! +//! This module contains internal types (RackSecret, ReconstructedRackSecret, PlaintextRackSecrets) +//! and free functions for cryptographic operations. +//! +//! The published API types (Sha3_256Digest, Salt, EncryptedRackSecrets, etc.) are defined in +//! trust-quorum-types and re-exported from this crate's lib.rs. use bootstore::trust_quorum::RackSecret as LrtqRackSecret; use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, aead, aead::Aead}; use derive_more::From; -use gfss::shamir::{self, CombineError, SecretShares, Share, SplitError}; +use gfss::shamir::{self, SecretShares, Share, SplitError}; use hkdf::Hkdf; use omicron_uuid_kinds::RackUuid; use rand::TryRngCore; @@ -15,14 +21,17 @@ use rand::rngs::OsRng; use secrecy::{ExposeSecret, SecretBox}; use serde::{Deserialize, Serialize}; use sha3::{Digest, Sha3_256}; -use slog_error_chain::SlogInlineError; use static_assertions::const_assert_eq; use std::collections::BTreeMap; -use std::fmt::Debug; use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -use crate::{Epoch, Threshold}; +// Import published types from trust-quorum-types +use trust_quorum_types::crypto::{ + DecryptionError, EncryptedRackSecrets, InvalidRackSecretSizeError, + RackSecretReconstructError, Salt, Sha3_256Digest, +}; +use trust_quorum_types::types::{Epoch, Threshold}; /// Each share contains a byte for the y-coordinate of 32 points on 32 different /// polynomials over Ed25519. All points share an x-coordinate, which is the 0th @@ -78,24 +87,9 @@ impl LrtqShare { )] pub struct ShareDigestLrtq(Sha3_256Digest); -#[derive( - Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, -)] -pub struct Sha3_256Digest(pub [u8; 32]); - -impl std::fmt::Debug for Sha3_256Digest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "sha3 digest: ")?; - for v in self.0.as_slice() { - write!(f, "{:x?}", v)?; - } - Ok(()) - } -} - impl From for bootstore::Sha3_256Digest { fn from(value: ShareDigestLrtq) -> Self { - bootstore::Sha3_256Digest::new(value.0.0) + bootstore::Sha3_256Digest::new(value.0 .0) } } @@ -137,19 +131,6 @@ impl PartialEq for ReconstructedRackSecret { } } -#[derive( - Debug, - Clone, - PartialEq, - Eq, - thiserror::Error, - PartialOrd, - Ord, - Serialize, - Deserialize, -)] -#[error("invalid rack secret size")] -pub struct InvalidRackSecretSizeError; impl TryFrom> for ReconstructedRackSecret { type Error = InvalidRackSecretSizeError; @@ -193,28 +174,6 @@ impl From for ReconstructedRackSecret { } } -#[derive( - Debug, - Clone, - thiserror::Error, - PartialEq, - Eq, - SlogInlineError, - PartialOrd, - Ord, - Serialize, - Deserialize, -)] -pub enum RackSecretReconstructError { - #[error("share combine error")] - Combine( - #[from] - #[source] - CombineError, - ), - #[error(transparent)] - Size(#[from] InvalidRackSecretSizeError), -} /// A shared secret based on GF256 #[derive(Debug)] @@ -277,111 +236,61 @@ impl PartialEq for RackSecret { impl Eq for RackSecret {} -/// Some public randomness for cryptographic operations -#[derive( - Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, -)] -pub struct Salt(pub [u8; 32]); - -impl Salt { - pub fn new() -> Salt { - let mut rng = OsRng; - let mut salt = [0u8; 32]; - rng.try_fill_bytes(&mut salt).expect("fetched random bytes from OsRng"); - Salt(salt) - } -} - -impl Default for Salt { - fn default() -> Self { - Self::new() - } +/// Generate a new random salt. +/// +/// This is a free function because `Salt` is defined in `trust-quorum-types` (for API versioning +/// akin to RFD 619). +pub fn new_salt() -> Salt { + let mut rng = OsRng; + let mut salt = [0u8; 32]; + rng.try_fill_bytes(&mut salt).expect("fetched random bytes from OsRng"); + Salt(salt) } -#[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, -)] -/// All possibly relevant __encrypted__ rack secrets for _prior_ committed -/// configurations -pub struct EncryptedRackSecrets { - /// A random value used to derive the key to encrypt the rack secrets for - /// prior committed epochs. - salt: Salt, - data: Box<[u8]>, -} +/// Decrypt rack secrets from an `EncryptedRackSecrets` structure. +/// +/// This is a free function because `EncryptedRackSecrets` is defined in `trust-quorum-types` (for +/// API versioning akin to RFD 619) and Rust doesn't allow adding inherent methods to types from +/// other crates. +pub fn decrypt_rack_secrets( + encrypted: &EncryptedRackSecrets, + rack_id: RackUuid, + epoch: Epoch, + rack_secret: &ReconstructedRackSecret, +) -> Result { + let key = derive_encryption_key_for_rack_secrets( + rack_id, + epoch, + encrypted.salt, + rack_secret, + ); -#[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - thiserror::Error, - SlogInlineError, - Serialize, - Deserialize, -)] -pub enum DecryptionError { - // An opaque error indicating decryption failed - #[error("Failed to decrypt rack secrets")] - Aead, - - // The length of the plaintext is not the correct size and cannot - // be decoded. - #[error("Plaintext length is invalid")] - InvalidLength, -} + // This key only encrypts a single plaintext and so a nonce of all zeroes + // is all that's required. + let nonce = [0u8; CHACHA20POLY1305_NONCE_LEN].into(); -impl From for DecryptionError { - fn from(_: aead::Error) -> Self { - DecryptionError::Aead - } -} + let plaintext = Zeroizing::new( + key.decrypt(&nonce, encrypted.data.as_ref()) + .map_err(|_| DecryptionError::Aead)?, + ); -impl EncryptedRackSecrets { - pub fn new(salt: Salt, data: Box<[u8]>) -> Self { - EncryptedRackSecrets { salt, data } + if plaintext.len() % (SECRET_LEN + EPOCH_LEN) != 0 { + return Err(DecryptionError::InvalidLength); } - pub fn decrypt( - &self, - rack_id: RackUuid, - epoch: Epoch, - rack_secret: &ReconstructedRackSecret, - ) -> Result { - let key = derive_encryption_key_for_rack_secrets( - rack_id, - epoch, - self.salt, - rack_secret, - ); - - // This key only encrypts a single plaintext and so a nonce of all zeroes - // is all that's required. - let nonce = [0u8; CHACHA20POLY1305_NONCE_LEN].into(); - - let plaintext = - Zeroizing::new(key.decrypt(&nonce, self.data.as_ref())?); - - if plaintext.len() % (SECRET_LEN + EPOCH_LEN) != 0 { - return Err(DecryptionError::InvalidLength); - } - - let mut plaintext_secrets = PlaintextRackSecrets::new(); + let mut plaintext_secrets = PlaintextRackSecrets::new(); - for p in plaintext.chunks_exact(SECRET_LEN + EPOCH_LEN) { - // SAFETY: Neither of these unwraps will ever fail as we've checked - // the validity of plaintext length above. - let epoch = - Epoch(u64::from_be_bytes(p[0..EPOCH_LEN].try_into().unwrap())); - let secret: ReconstructedRackSecret = - p[EPOCH_LEN..].try_into().unwrap(); - plaintext_secrets.insert(epoch, secret); - } - - Ok(plaintext_secrets) + for p in plaintext.chunks_exact(SECRET_LEN + EPOCH_LEN) { + // SAFETY: Neither of these unwraps will ever fail as we've checked + // the validity of plaintext length above. + let epoch = + Epoch(u64::from_be_bytes(p[0..EPOCH_LEN].try_into().unwrap())); + let secret: ReconstructedRackSecret = + p[EPOCH_LEN..].try_into().unwrap(); + plaintext_secrets.insert(epoch, secret); } + + Ok(plaintext_secrets) } /// All possibly relevant __unencrypted__ rack secrets for _prior_ committed @@ -428,7 +337,7 @@ impl PlaintextRackSecrets { // We generate a fresh salt because we should only be encrypting // once for this epoch. This is also why we consume `self`. - let salt = Salt::new(); + let salt = new_salt(); let key = derive_encryption_key_for_rack_secrets( rack_id, new_epoch, @@ -453,7 +362,7 @@ impl PlaintextRackSecrets { let nonce = [0u8; CHACHA20POLY1305_NONCE_LEN].into(); let encrypted = key.encrypt(&nonce, plaintext.as_ref())?.into_boxed_slice(); - Ok(EncryptedRackSecrets { salt, data: encrypted }) + Ok(EncryptedRackSecrets::new(salt, encrypted)) } } @@ -565,7 +474,7 @@ mod tests { let encrypted = plaintext.encrypt(rack_id, new_epoch, &new_rack_secret).unwrap(); let decrypted = - encrypted.decrypt(rack_id, new_epoch, &new_rack_secret).unwrap(); + decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &new_rack_secret).unwrap(); // We don't actually do any comparisons of rack secrets outside this // test and we don't want to derive `PartialEq` due to data dependent @@ -594,29 +503,26 @@ mod tests { // Decrypting with wrong rack_id fails. assert!( - encrypted - .decrypt(RackUuid::new_v4(), new_epoch, &new_rack_secret) + decrypt_rack_secrets(&encrypted, RackUuid::new_v4(), new_epoch, &new_rack_secret) .is_err() ); // Decrypting with wrong epoch fails. assert!( - encrypted - .decrypt(RackUuid::new_v4(), Epoch(99), &new_rack_secret) + decrypt_rack_secrets(&encrypted, RackUuid::new_v4(), Epoch(99), &new_rack_secret) .is_err() ); // Decrypting with the wrong secret fails assert!( - encrypted - .decrypt(rack_id, new_epoch, &RackSecret::new().into()) + decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &RackSecret::new().into()) .is_err() ); // Decrypting with corrupted plaintext is invalid encrypted.data = vec![0u8, 1u8].into_boxed_slice(); assert!( - encrypted.decrypt(rack_id, new_epoch, &new_rack_secret).is_err() + decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &new_rack_secret).is_err() ); } } diff --git a/trust-quorum/protocol/src/lib.rs b/trust-quorum/protocol/src/lib.rs index e2137fd6cc9..2d52479fb61 100644 --- a/trust-quorum/protocol/src/lib.rs +++ b/trust-quorum/protocol/src/lib.rs @@ -10,13 +10,10 @@ //! implementation. use daft::Diffable; -use derive_more::Display; use gfss::shamir::Share; use serde::{Deserialize, Serialize}; -pub use sled_agent_types::sled::BaseboardId; use slog::{Logger, error, warn}; -mod alarm; mod compute_key_share; mod configuration; mod coordinator_state; @@ -29,7 +26,25 @@ mod persistent_state; mod rack_secret_loader; mod validators; -pub use configuration::Configuration; +// Re-export types from trust-quorum-types for backward compatibility. +// These types were previously defined in this crate but have been factored +// out to support API versioning per RFD 619. +pub use trust_quorum_types::alarm::Alarm; +pub use trust_quorum_types::configuration::{ + BaseboardId, Configuration, ConfigurationError, NewConfigParams, +}; +pub use trust_quorum_types::crypto::{ + DecryptionError, EncryptedRackSecrets, InvalidRackSecretSizeError, + RackSecretReconstructError, Salt, Sha3_256Digest, +}; +pub use trust_quorum_types::persistent_state::{ + ExpungedMetadata, PersistentStateSummary, +}; +pub use trust_quorum_types::status::{ + CommitStatus, CoordinatorStatus, NodePersistentStateSummary, NodeStatus, +}; +pub use trust_quorum_types::types::{Epoch, Threshold}; + pub use coordinator_state::{ CoordinatingMsg, CoordinatorOperation, CoordinatorState, CoordinatorStateDiff, @@ -40,61 +55,21 @@ pub use validators::{ ValidatedReconfigureMsgDiff, }; -pub use alarm::Alarm; +// These crypto types and functions are NOT in trust-quorum-types because they +// contain sensitive data or have complex implementations tied to this crate. +pub use configuration::new_configuration; +#[cfg(feature = "testing")] +pub use configuration::configurations_equal_except_for_crypto_data; pub use crypto::{ - EncryptedRackSecrets, RackSecret, ReconstructedRackSecret, Salt, - Sha3_256Digest, + PlaintextRackSecrets, RackSecret, ReconstructedRackSecret, SECRET_LEN, + decrypt_rack_secrets, new_salt, }; pub use messages::*; pub use node::{CommitError, Node, NodeDiff, PrepareAndCommitError}; // public only for docs. pub use node_ctx::NodeHandlerCtx; pub use node_ctx::{NodeCallerCtx, NodeCommonCtx, NodeCtx, NodeCtxDiff}; -pub use persistent_state::{ - ExpungedMetadata, PersistentState, PersistentStateSummary, -}; - -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - Display, - Diffable, -)] -#[daft(leaf)] -pub struct Epoch(pub u64); - -impl Epoch { - pub fn next(&self) -> Epoch { - Epoch(self.0.checked_add(1).expect("fewer than 2^64 epochs")) - } -} - -/// The number of shares required to reconstruct the rack secret -/// -/// Typically referred to as `k` in the docs -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Display, - Diffable, -)] -#[daft(leaf)] -pub struct Threshold(pub u8); +pub use persistent_state::PersistentState; /// A container to make messages between trust quorum nodes routable #[derive(Debug, Clone, Serialize, Deserialize, Diffable)] diff --git a/trust-quorum/protocol/src/messages.rs b/trust-quorum/protocol/src/messages.rs index af8c65424cd..ea298fa3558 100644 --- a/trust-quorum/protocol/src/messages.rs +++ b/trust-quorum/protocol/src/messages.rs @@ -4,6 +4,8 @@ //! Messsages for the trust quorum protocol +#[cfg(feature = "testing")] +use crate::configuration::configurations_equal_except_for_crypto_data; use crate::crypto::LrtqShare; use crate::{BaseboardId, Configuration, Epoch, Threshold}; use gfss::shamir::Share; @@ -113,14 +115,14 @@ impl PeerMsgKind { ( Self::Prepare { config: config1, .. }, Self::Prepare { config: config2, .. }, - ) => config1.equal_except_for_crypto_data(config2), + ) => configurations_equal_except_for_crypto_data(config1, config2), ( Self::Share { epoch: epoch1, .. }, Self::Share { epoch: epoch2, .. }, ) => epoch1 == epoch2, (Self::LrtqShare(_), Self::LrtqShare(_)) => true, (Self::CommitAdvance(config1), Self::CommitAdvance(config2)) => { - config1.equal_except_for_crypto_data(config2) + configurations_equal_except_for_crypto_data(config1, config2) } (s, o) => s == o, } diff --git a/trust-quorum/protocol/src/persistent_state.rs b/trust-quorum/protocol/src/persistent_state.rs index ea450492ac4..06c25e11c98 100644 --- a/trust-quorum/protocol/src/persistent_state.rs +++ b/trust-quorum/protocol/src/persistent_state.rs @@ -7,7 +7,7 @@ //! Note that this state is not necessarily directly serialized and saved. use crate::crypto::LrtqShare; -use crate::{BaseboardId, Configuration, Epoch}; +use crate::{Configuration, Epoch}; use bootstore::schemes::v0::SharePkgCommon as LrtqShareData; use daft::Diffable; use gfss::shamir::Share; @@ -16,6 +16,12 @@ use omicron_uuid_kinds::{GenericUuid, RackUuid}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; +// Re-export API types from trust-quorum-types +pub use trust_quorum_types::persistent_state::{ + ExpungedMetadata, PersistentStateSummary, +}; +pub use trust_quorum_types::status::NodePersistentStateSummary; + /// All the persistent state for this protocol #[derive(Debug, Clone, Serialize, Deserialize, Default, Diffable)] #[cfg_attr(feature = "danger_partial_eq_ct_wrapper", derive(PartialEq, Eq))] @@ -127,29 +133,6 @@ impl PersistentState { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ExpungedMetadata { - /// The committed epoch, later than its current configuration at which the - /// node learned that it had been expunged. - pub epoch: Epoch, - - /// Which node this commit information was learned from - pub from: BaseboardId, -} - -/// A subset of information stored in [`PersistentState`] that is useful -/// for validation, testing, and informational purposes. -#[derive(Debug, Clone)] -pub struct PersistentStateSummary { - pub rack_id: Option, - pub is_lrtq_only: bool, - pub is_uninitialized: bool, - pub latest_config: Option, - pub latest_committed_config: Option, - pub latest_share: Option, - pub expunged: Option, -} - impl From<&PersistentState> for PersistentStateSummary { fn from(value: &PersistentState) -> Self { PersistentStateSummary { @@ -163,3 +146,15 @@ impl From<&PersistentState> for PersistentStateSummary { } } } + +impl From<&PersistentState> for NodePersistentStateSummary { + fn from(value: &PersistentState) -> Self { + NodePersistentStateSummary { + has_lrtq_share: value.lrtq.is_some(), + configs: value.configs.iter().map(|c| c.epoch).collect(), + shares: value.shares.keys().cloned().collect(), + commits: value.commits.clone(), + expunged: value.expunged.clone(), + } + } +} diff --git a/trust-quorum/protocol/src/rack_secret_loader.rs b/trust-quorum/protocol/src/rack_secret_loader.rs index f355187fabb..3dde6e4207a 100644 --- a/trust-quorum/protocol/src/rack_secret_loader.rs +++ b/trust-quorum/protocol/src/rack_secret_loader.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; -use crate::crypto::ReconstructedRackSecret; +use crate::crypto::{ReconstructedRackSecret, decrypt_rack_secrets}; use crate::{ Alarm, BaseboardId, Configuration, Epoch, NodeHandlerCtx, PeerMsgKind, RackSecret, Share, @@ -281,7 +281,8 @@ impl ShareCollector { "epoch" => %self.config.epoch ); if let Some(encrypted) = &self.config.encrypted_rack_secrets { - match encrypted.decrypt( + match decrypt_rack_secrets( + encrypted, self.config.rack_id, self.config.epoch, &secret, diff --git a/trust-quorum/protocol/src/validators.rs b/trust-quorum/protocol/src/validators.rs index ccc8049f82b..84e2ba7fea5 100644 --- a/trust-quorum/protocol/src/validators.rs +++ b/trust-quorum/protocol/src/validators.rs @@ -4,11 +4,10 @@ //! Various validation functions to be used by a [`crate::Node`] -use crate::configuration::{ConfigurationError, NewConfigParams}; use crate::messages::ReconfigureMsg; use crate::{ - BaseboardId, Epoch, LrtqUpgradeMsg, NodeHandlerCtx, PersistentStateSummary, - Threshold, + BaseboardId, ConfigurationError, Epoch, LrtqUpgradeMsg, NewConfigParams, + NodeHandlerCtx, PersistentStateSummary, Threshold, }; use daft::{BTreeSetDiff, Diffable, Leaf}; use omicron_uuid_kinds::RackUuid; diff --git a/trust-quorum/src/proxy.rs b/trust-quorum/src/proxy.rs index c44bcd59b91..37bfeb8982d 100644 --- a/trust-quorum/src/proxy.rs +++ b/trust-quorum/src/proxy.rs @@ -12,10 +12,7 @@ //! This proxy mechanism is also useful during RSS and for general debugging //! purposes. -use crate::{ - CommitStatus, - task::{NodeApiRequest, NodeStatus}, -}; +use crate::task::{CommitStatus, NodeApiRequest, NodeStatus}; use debug_ignore::DebugIgnore; use derive_more::From; use iddqd::{IdHashItem, IdHashMap, id_upcast}; diff --git a/trust-quorum/src/task.rs b/trust-quorum/src/task.rs index e14776755b1..dd66a68be16 100644 --- a/trust-quorum/src/task.rs +++ b/trust-quorum/src/task.rs @@ -13,7 +13,6 @@ use crate::ledgers::PersistentStateLedger; use crate::proxy; use camino::Utf8PathBuf; use omicron_uuid_kinds::RackUuid; -use serde::{Deserialize, Serialize}; use slog::{Logger, debug, error, info, o, warn}; use slog_error_chain::SlogInlineError; use sprockets_tls::keys::SprocketsConfig; @@ -24,23 +23,21 @@ use tokio::sync::mpsc::error::SendError; use tokio::sync::oneshot::error::RecvError; use tokio::sync::{mpsc, oneshot}; use trust_quorum_protocol::{ - Alarm, BaseboardId, CommitError, Configuration, Epoch, ExpungedMetadata, - LoadRackSecretError, LrtqUpgradeError, LrtqUpgradeMsg, Node, NodeCallerCtx, - NodeCommonCtx, NodeCtx, PersistentState, PrepareAndCommitError, - ReconfigurationError, ReconfigureMsg, ReconstructedRackSecret, + BaseboardId, CommitError, Configuration, Epoch, LoadRackSecretError, + LrtqUpgradeError, LrtqUpgradeMsg, Node, NodeCallerCtx, NodeCommonCtx, + NodeCtx, PrepareAndCommitError, ReconfigurationError, ReconfigureMsg, + ReconstructedRackSecret, +}; + +// Re-export types that need to be visible from this crate +pub use trust_quorum_protocol::{ + CommitStatus, CoordinatorStatus, NodeStatus, }; // TODO: Move to this crate // https://github.com/oxidecomputer/omicron/issues/9311 use bootstore::schemes::v0::NetworkConfig; -/// Whether or not a configuration has committed or is still underway. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CommitStatus { - Committed, - Pending, -} - /// We only expect a handful of messages at a time. const API_CHANNEL_BOUND: usize = 32; @@ -62,46 +59,6 @@ pub struct Config { pub sprockets: SprocketsConfig, } -/// Status of the node coordinating the `Prepare` phase of a reconfiguration or -/// LRTQ upgrade. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CoordinatorStatus { - pub config: Configuration, - pub acked_prepares: BTreeSet, -} - -// Details about a given node's status -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct NodeStatus { - pub connected_peers: BTreeSet, - pub alarms: BTreeSet, - pub persistent_state: NodePersistentStateSummary, - pub proxied_requests: u64, -} - -/// A summary of a node's persistent state, leaving out things like key shares -/// and hashes. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct NodePersistentStateSummary { - pub has_lrtq_share: bool, - pub configs: BTreeSet, - pub shares: BTreeSet, - pub commits: BTreeSet, - pub expunged: Option, -} - -impl From<&PersistentState> for NodePersistentStateSummary { - fn from(value: &PersistentState) -> Self { - Self { - has_lrtq_share: value.lrtq.is_some(), - configs: value.configs.iter().map(|c| c.epoch).collect(), - shares: value.shares.keys().cloned().collect(), - commits: value.commits.clone(), - expunged: value.expunged.clone(), - } - } -} - /// A request sent to the `NodeTask` from the `NodeTaskHandle` pub enum NodeApiRequest { /// Inform the `Node` of currently known IP addresses on the bootstrap network diff --git a/trust-quorum/types/Cargo.toml b/trust-quorum/types/Cargo.toml new file mode 100644 index 00000000000..bb9eca0cffe --- /dev/null +++ b/trust-quorum/types/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "trust-quorum-types" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +trust-quorum-types-versions.workspace = true +omicron-workspace-hack.workspace = true diff --git a/trust-quorum/types/src/lib.rs b/trust-quorum/types/src/lib.rs new file mode 100644 index 00000000000..6b13fdb5bb1 --- /dev/null +++ b/trust-quorum/types/src/lib.rs @@ -0,0 +1,11 @@ +// 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/. + +//! Trust quorum types. +//! +//! This crate re-exports types from `trust-quorum-types-versions::latest`, +//! providing a stable interface for consumers who want the latest versions +//! of all types. + +pub use trust_quorum_types_versions::latest::*; diff --git a/trust-quorum/types/versions/Cargo.toml b/trust-quorum/types/versions/Cargo.toml new file mode 100644 index 00000000000..ef5583cf669 --- /dev/null +++ b/trust-quorum/types/versions/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "trust-quorum-types-versions" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +daft.workspace = true +derive_more.workspace = true +gfss.workspace = true +iddqd.workspace = true +omicron-uuid-kinds.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true +serde_with = { workspace = true, features = ["hex", "schemars_0_8"] } +sled-hardware-types.workspace = true +slog.workspace = true +slog-error-chain.workspace = true +thiserror.workspace = true + +[features] +testing = [] diff --git a/trust-quorum/types/versions/src/impls/epoch.rs b/trust-quorum/types/versions/src/impls/epoch.rs new file mode 100644 index 00000000000..fe5dfad7ba4 --- /dev/null +++ b/trust-quorum/types/versions/src/impls/epoch.rs @@ -0,0 +1,16 @@ +// 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/. + +//! Implementations for Epoch. + +use crate::latest::types::Epoch; + +impl Epoch { + /// Returns the next epoch. + /// + /// Panics if the epoch counter would overflow (more than 2^64 epochs). + pub fn next(&self) -> Epoch { + Epoch(self.0.checked_add(1).expect("fewer than 2^64 epochs")) + } +} diff --git a/trust-quorum/types/versions/src/impls/mod.rs b/trust-quorum/types/versions/src/impls/mod.rs new file mode 100644 index 00000000000..8bb6f94afa3 --- /dev/null +++ b/trust-quorum/types/versions/src/impls/mod.rs @@ -0,0 +1,13 @@ +// 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/. + +//! Functional implementations for trust quorum types. +//! +//! Per RFD 619, inherent methods, Display implementations, and other +//! functional code lives here, using `latest::` identifiers. However, pulling inherent methods on, +//! e.g. `Configuration` would end up transitively pulling in a lot of the rest of the trust quorum +//! protocol, which we *don't* want, so instead the inherent methods have been converted to free +//! functions when necessary. + +mod epoch; diff --git a/trust-quorum/protocol/src/alarm.rs b/trust-quorum/types/versions/src/initial/alarm.rs similarity index 80% rename from trust-quorum/protocol/src/alarm.rs rename to trust-quorum/types/versions/src/initial/alarm.rs index a7392184153..2e1e282da37 100644 --- a/trust-quorum/protocol/src/alarm.rs +++ b/trust-quorum/types/versions/src/initial/alarm.rs @@ -2,21 +2,24 @@ // 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/. -//! Mechanism for reporting protocol invariant violations +//! Mechanism for reporting protocol invariant violations. +use gfss::shamir::CombineError; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - Configuration, Epoch, - crypto::{DecryptionError, RackSecretReconstructError}, -}; +use super::configuration::Configuration; +use super::crypto::{DecryptionError, RackSecretReconstructError}; +use super::types::Epoch; +/// An alarm indicating a protocol invariant violation. #[allow(clippy::large_enum_variant)] #[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, )] +#[serde(rename_all = "snake_case")] pub enum Alarm { - /// Different configurations found for the same epoch + /// Different configurations found for the same epoch. /// /// Reason: Nexus creates configurations and stores them in CRDB before /// sending them to a coordinator of its choosing. Nexus will not send the @@ -27,16 +30,16 @@ pub enum Alarm { MismatchedConfigurations { config1: Configuration, config2: Configuration, - // Either a stringified `BaseboardId` or "Nexus" + /// Either a stringified `BaseboardId` or "Nexus". from: String, }, - /// The `keyShareComputer` could not compute this node's share + /// The `keyShareComputer` could not compute this node's share. /// /// Reason: A threshold of valid key shares were received based on the the /// share digests in the Configuration. However, computation of the share /// still failed. This should be impossible. - ShareComputationFailed { epoch: Epoch, err: gfss::shamir::CombineError }, + ShareComputationFailed { epoch: Epoch, err: CombineError }, /// We started collecting shares for a committed configuration, /// but we no longer have that configuration in our persistent state. @@ -50,7 +53,7 @@ pub enum Alarm { /// /// `Configuration` membership contains the hashes of each valid share. All /// shares utilized to reconstruct the rack secret were validated against - /// these hashes, and the rack seceret was reconstructed. However, using + /// these hashes, and the rack secret was reconstructed. However, using /// the rack secret to derive encryption keys and decrypt the secrets from /// old configurations still failed. This should never be possible, and /// therefore we raise an alarm. diff --git a/trust-quorum/types/versions/src/initial/configuration.rs b/trust-quorum/types/versions/src/initial/configuration.rs new file mode 100644 index 00000000000..a7620743be2 --- /dev/null +++ b/trust-quorum/types/versions/src/initial/configuration.rs @@ -0,0 +1,104 @@ +// 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/. + +//! Trust quorum configuration types. + +use std::collections::{BTreeMap, BTreeSet}; + +use daft::Diffable; +use gfss::shamir::SplitError; +use iddqd::{IdOrdItem, id_upcast}; +use omicron_uuid_kinds::RackUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use slog_error_chain::SlogInlineError; +pub use sled_hardware_types::BaseboardId; + +use super::crypto::{EncryptedRackSecrets, Sha3_256Digest}; +use super::types::{Epoch, Threshold}; + +/// A member entry in a trust quorum configuration. +/// +/// This type is used for OpenAPI schema generation since OpenAPI v3.0.x +/// doesn't support tuple arrays. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ConfigurationMember { + /// The baseboard ID of the member. + pub id: BaseboardId, + /// The SHA3-256 hash of the member's key share. + pub share_digest: Sha3_256Digest, +} + +/// Error creating a configuration. +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ConfigurationError { + #[error("rack secret split error")] + #[schemars(with = "SplitError")] + RackSecretSplit( + #[from] + #[source] + SplitError, + ), + #[error("too many members: must be fewer than 255")] + TooManyMembers, +} + +/// The configuration for a given epoch. +/// +/// Only valid for non-lrtq configurations. +#[serde_as] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Diffable, + JsonSchema, +)] +pub struct Configuration { + /// Unique Id of the rack. + pub rack_id: RackUuid, + + /// Unique, monotonically increasing identifier for a configuration. + pub epoch: Epoch, + + /// Who was the coordinator of this reconfiguration? + pub coordinator: BaseboardId, + + /// All members of the current configuration and the hash of their key shares. + #[serde_as(as = "Vec<(_, _)>")] + #[schemars(with = "Vec")] + pub members: BTreeMap, + + /// The number of sleds required to reconstruct the rack secret. + pub threshold: Threshold, + + /// There are no encrypted rack secrets for the initial configuration. + pub encrypted_rack_secrets: Option, +} + +impl IdOrdItem for Configuration { + type Key<'a> = Epoch; + + fn key(&self) -> Self::Key<'_> { + self.epoch + } + + id_upcast!(); +} + +/// Parameters for creating a new configuration. +pub struct NewConfigParams<'a> { + pub rack_id: RackUuid, + pub epoch: Epoch, + pub members: &'a BTreeSet, + pub threshold: Threshold, + pub coordinator_id: &'a BaseboardId, +} diff --git a/trust-quorum/types/versions/src/initial/crypto.rs b/trust-quorum/types/versions/src/initial/crypto.rs new file mode 100644 index 00000000000..8d99c5d07c2 --- /dev/null +++ b/trust-quorum/types/versions/src/initial/crypto.rs @@ -0,0 +1,153 @@ +// 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/. + +//! Cryptographic types for trust quorum. + +use gfss::shamir::CombineError; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::{hex::Hex, serde_as}; +use slog_error_chain::SlogInlineError; + +/// A SHA3-256 digest (32 bytes). +#[serde_as] +#[derive( + Default, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[schemars(transparent)] +pub struct Sha3_256Digest( + #[serde_as(as = "Hex")] + #[schemars(with = "String")] + pub [u8; 32], +); + +impl std::fmt::Debug for Sha3_256Digest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "sha3 digest: ")?; + for v in self.0.as_slice() { + write!(f, "{:x?}", v)?; + } + Ok(()) + } +} + +/// Some public randomness for cryptographic operations. +#[serde_as] +#[derive( + Debug, + Clone, + Copy, + Eq, + PartialEq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[schemars(transparent)] +pub struct Salt( + #[serde_as(as = "Hex")] + #[schemars(with = "String")] + pub [u8; 32], +); + +/// All possibly relevant __encrypted__ rack secrets for _prior_ committed +/// configurations. +#[serde_as] +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, +)] +pub struct EncryptedRackSecrets { + /// A random value used to derive the key to encrypt the rack secrets for + /// prior committed epochs. + pub salt: Salt, + #[serde_as(as = "Hex")] + #[schemars(with = "String")] + pub data: Box<[u8]>, +} + +impl EncryptedRackSecrets { + pub fn new(salt: Salt, data: Box<[u8]>) -> Self { + EncryptedRackSecrets { salt, data } + } +} + +/// Error indicating the rack secret has an invalid size. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + thiserror::Error, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[error("invalid rack secret size")] +pub struct InvalidRackSecretSizeError; + +/// Error reconstructing a rack secret from shares. +#[derive( + Debug, + Clone, + thiserror::Error, + PartialEq, + Eq, + SlogInlineError, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum RackSecretReconstructError { + #[error("share combine error")] + #[schemars(with = "CombineError")] + Combine( + #[from] + #[source] + CombineError, + ), + #[error(transparent)] + #[schemars(with = "InvalidRackSecretSizeError")] + Size(#[from] InvalidRackSecretSizeError), +} + +/// Error decrypting rack secrets. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + thiserror::Error, + SlogInlineError, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum DecryptionError { + /// An opaque error indicating decryption failed. + #[error("Failed to decrypt rack secrets")] + Aead, + + /// The length of the plaintext is not the correct size and cannot + /// be decoded. + #[error("Plaintext length is invalid")] + InvalidLength, +} diff --git a/trust-quorum/types/versions/src/initial/mod.rs b/trust-quorum/types/versions/src/initial/mod.rs new file mode 100644 index 00000000000..d472721adbf --- /dev/null +++ b/trust-quorum/types/versions/src/initial/mod.rs @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `INITIAL` of trust-quorum types. +//! +//! This immediately follows the pre-TQ "low-rent trust quorum", which is not versioned here. + +pub mod alarm; +pub mod configuration; +pub mod crypto; +pub mod persistent_state; +pub mod status; +pub mod types; diff --git a/trust-quorum/types/versions/src/initial/persistent_state.rs b/trust-quorum/types/versions/src/initial/persistent_state.rs new file mode 100644 index 00000000000..7fe507de0fc --- /dev/null +++ b/trust-quorum/types/versions/src/initial/persistent_state.rs @@ -0,0 +1,36 @@ +// 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/. + +//! Persistent state types. + +use omicron_uuid_kinds::RackUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::configuration::BaseboardId; +use super::types::Epoch; + +/// Metadata about a node being expunged from the trust quorum. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExpungedMetadata { + /// The committed epoch, later than its current configuration at which the + /// node learned that it had been expunged. + pub epoch: Epoch, + + /// Which node this commit information was learned from. + pub from: BaseboardId, +} + +/// A subset of information stored in persistent state that is useful +/// for validation, testing, and informational purposes. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct PersistentStateSummary { + pub rack_id: Option, + pub is_lrtq_only: bool, + pub is_uninitialized: bool, + pub latest_config: Option, + pub latest_committed_config: Option, + pub latest_share: Option, + pub expunged: Option, +} diff --git a/trust-quorum/types/versions/src/initial/status.rs b/trust-quorum/types/versions/src/initial/status.rs new file mode 100644 index 00000000000..8fa7f425188 --- /dev/null +++ b/trust-quorum/types/versions/src/initial/status.rs @@ -0,0 +1,49 @@ +// 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/. + +//! Status report types for trust quorum nodes. + +use std::collections::BTreeSet; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::alarm::Alarm; +use super::configuration::{BaseboardId, Configuration}; +use super::persistent_state::ExpungedMetadata; +use super::types::Epoch; + +/// Whether or not a configuration has been committed or is still underway. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CommitStatus { + Committed, + Pending, +} + +/// Status of the node coordinating the reconfiguration or LRTQ upgrade. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct CoordinatorStatus { + pub config: Configuration, + pub acked_prepares: BTreeSet, +} + +/// Details about a given node's status. +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct NodeStatus { + pub connected_peers: BTreeSet, + pub alarms: BTreeSet, + pub persistent_state: NodePersistentStateSummary, + pub proxied_requests: u64, +} + +/// A summary of a node's persistent state. +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct NodePersistentStateSummary { + pub has_lrtq_share: bool, + pub configs: BTreeSet, + pub shares: BTreeSet, + pub commits: BTreeSet, + pub expunged: Option, +} diff --git a/trust-quorum/types/versions/src/initial/types.rs b/trust-quorum/types/versions/src/initial/types.rs new file mode 100644 index 00000000000..b210ee2d10f --- /dev/null +++ b/trust-quorum/types/versions/src/initial/types.rs @@ -0,0 +1,51 @@ +// 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/. + +//! Basic trust quorum types. + +use daft::Diffable; +use derive_more::Display; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// A unique, sequentially increasing identifier for a configuration. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + Display, + Diffable, + JsonSchema, +)] +#[daft(leaf)] +#[schemars(transparent)] +pub struct Epoch(pub u64); + +/// The number of shares required to reconstruct the rack secret. +/// +/// Typically referred to as `k` in the docs. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Display, + Diffable, + JsonSchema, +)] +#[daft(leaf)] +#[schemars(transparent)] +pub struct Threshold(pub u8); diff --git a/trust-quorum/types/versions/src/latest.rs b/trust-quorum/types/versions/src/latest.rs new file mode 100644 index 00000000000..a99233f1c48 --- /dev/null +++ b/trust-quorum/types/versions/src/latest.rs @@ -0,0 +1,46 @@ +// 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 version of each type. +//! +//! Per RFD 619, these re-exports are explicit (no wildcards) to make it clear +//! which types are available at the latest version. + +pub mod alarm { + pub use crate::v1::alarm::Alarm; +} + +pub mod configuration { + pub use crate::v1::configuration::BaseboardId; + pub use crate::v1::configuration::Configuration; + pub use crate::v1::configuration::ConfigurationDiff; + pub use crate::v1::configuration::ConfigurationError; + pub use crate::v1::configuration::NewConfigParams; +} + +pub mod crypto { + pub use crate::v1::crypto::DecryptionError; + pub use crate::v1::crypto::EncryptedRackSecrets; + pub use crate::v1::crypto::InvalidRackSecretSizeError; + pub use crate::v1::crypto::RackSecretReconstructError; + pub use crate::v1::crypto::Salt; + pub use crate::v1::crypto::Sha3_256Digest; +} + +pub mod persistent_state { + pub use crate::v1::persistent_state::ExpungedMetadata; + pub use crate::v1::persistent_state::PersistentStateSummary; +} + +pub mod status { + pub use crate::v1::status::CommitStatus; + pub use crate::v1::status::CoordinatorStatus; + pub use crate::v1::status::NodePersistentStateSummary; + pub use crate::v1::status::NodeStatus; +} + +pub mod types { + pub use crate::v1::types::Epoch; + pub use crate::v1::types::Threshold; +} diff --git a/trust-quorum/types/versions/src/lib.rs b/trust-quorum/types/versions/src/lib.rs new file mode 100644 index 00000000000..c9c36d5c4c9 --- /dev/null +++ b/trust-quorum/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 Trust Quorum protocol. +//! +//! # 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. +//! +//! 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 [RFD 619], off which this approach is modeled. +//! +//! [RFD 619]: https://rfd.shared.oxide.computer/rfd/619 + +mod impls; +pub mod latest; + +#[path = "initial/mod.rs"] +pub mod v1; From 720e481e126fefcb8893d284f534583e55ebba3f Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 16:36:30 -0500 Subject: [PATCH 5/9] Address PR review feedback for trust quorum API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses feedback from PR #9556 review: - Changed all idempotent mutating endpoints from POST to PUT - `/trust-quorum/reconfigure` → `/trust-quorum/configuration` - `/trust-quorum/upgrade-from-lrtq` → `/trust-quorum/upgrade` - `trust_quorum_commit` now returns `HttpResponseUpdatedNoContent` and returns an error if `CommitStatus::Pending` is returned (which indicates an unexpected state) - `trust_quorum_proxy_commit` follows the same pattern - `trust_quorum_prepare_and_commit` continues to return `CommitStatus` (per discussion, `Pending` is valid here, unlike above) - Changed from `PUT` with `TypedBody` to `GET` with `Query` - The destination baseboard ID is now passed as query parameters instead of a request body, which is more appropriate for a read-only operation Added `#[serde(rename_all = "snake_case")]` to enum types for consistent JSON serialization: - `Alarm` variants - `CommitStatus` variants - `ConfigurationError` variants - `RackSecretReconstructError` variants - `DecryptionError` variants - `SplitError` and `CombineError` (in gfss) Co-Authored-By: Claude Opus 4.5 --- .../src/test_util/host_phase_2_test_state.rs | 48 +- ...232.json => sled-agent-13.0.0-4e865c.json} | 457 +++++++++--------- openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 61 +-- sled-agent/src/http_entrypoints.rs | 48 +- sled-agent/src/sim/http_entrypoints.rs | 29 +- .../src/add_trust_quorum/trust_quorum.rs | 19 +- sled-agent/types/versions/src/latest.rs | 13 +- .../protocol/src/coordinator_state.rs | 7 +- trust-quorum/protocol/src/crypto.rs | 48 +- trust-quorum/protocol/src/lib.rs | 2 +- trust-quorum/src/task.rs | 4 +- .../types/versions/src/initial/alarm.rs | 10 +- .../versions/src/initial/configuration.rs | 6 +- .../types/versions/src/initial/crypto.rs | 10 +- 15 files changed, 375 insertions(+), 389 deletions(-) rename openapi/sled-agent/{sled-agent-13.0.0-065232.json => sled-agent-13.0.0-4e865c.json} (99%) diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index 3611acbdce0..0ad6d7dc708 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -940,7 +940,7 @@ mod api_impl { async fn trust_quorum_reconfigure( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumReconfigureRequest, + sled_agent_types::trust_quorum::ReconfigureRequest, >, ) -> Result { unimplemented!() @@ -949,7 +949,7 @@ mod api_impl { async fn trust_quorum_upgrade_from_lrtq( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumLrtqUpgradeRequest, + sled_agent_types::trust_quorum::LrtqUpgradeRequest, >, ) -> Result { unimplemented!() @@ -957,15 +957,8 @@ mod api_impl { async fn trust_quorum_commit( _request_context: RequestContext, - _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumCommitRequest, - >, - ) -> Result< - HttpResponseOk< - sled_agent_types::trust_quorum::TrustQuorumCommitResponse, - >, - HttpError, - > { + _body: TypedBody, + ) -> Result { unimplemented!() } @@ -973,22 +966,20 @@ mod api_impl { _request_context: RequestContext, ) -> Result< HttpResponseOk< - Option, + Option, >, HttpError, - >{ + > { unimplemented!() } async fn trust_quorum_prepare_and_commit( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumPrepareAndCommitRequest, + sled_agent_types::trust_quorum::PrepareAndCommitRequest, >, ) -> Result< - HttpResponseOk< - sled_agent_types::trust_quorum::TrustQuorumCommitResponse, - >, + HttpResponseOk, HttpError, > { unimplemented!() @@ -997,26 +988,19 @@ mod api_impl { async fn trust_quorum_proxy_commit( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumProxyCommitRequest, + sled_agent_types::trust_quorum::ProxyCommitRequest, >, - ) -> Result< - HttpResponseOk< - sled_agent_types::trust_quorum::TrustQuorumCommitResponse, - >, - HttpError, - > { + ) -> Result { unimplemented!() } async fn trust_quorum_proxy_prepare_and_commit( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest, + sled_agent_types::trust_quorum::ProxyPrepareAndCommitRequest, >, ) -> Result< - HttpResponseOk< - sled_agent_types::trust_quorum::TrustQuorumCommitResponse, - >, + HttpResponseOk, HttpError, > { unimplemented!() @@ -1024,13 +1008,9 @@ mod api_impl { async fn trust_quorum_proxy_status( _request_context: RequestContext, - _body: TypedBody< - sled_agent_types::trust_quorum::TrustQuorumProxyStatusRequest, - >, + _query_params: Query, ) -> Result< - HttpResponseOk< - sled_agent_types::trust_quorum::TrustQuorumNodeStatus, - >, + HttpResponseOk, HttpError, > { unimplemented!() diff --git a/openapi/sled-agent/sled-agent-13.0.0-065232.json b/openapi/sled-agent/sled-agent-13.0.0-4e865c.json similarity index 99% rename from openapi/sled-agent/sled-agent-13.0.0-065232.json rename to openapi/sled-agent/sled-agent-13.0.0-4e865c.json index a91d8eac525..80eaccf2fa4 100644 --- a/openapi/sled-agent/sled-agent-13.0.0-065232.json +++ b/openapi/sled-agent/sled-agent-13.0.0-4e865c.json @@ -1548,30 +1548,50 @@ } }, "/trust-quorum/commit": { - "post": { + "put": { "summary": "Commit a trust quorum configuration", "operationId": "trust_quorum_commit", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumCommitRequest" + "$ref": "#/components/schemas/CommitRequest" } } }, "required": true }, "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CommitStatus" - } + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/trust-quorum/configuration": { + "put": { + "summary": "Initiate a trust quorum reconfiguration", + "operationId": "trust_quorum_reconfigure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReconfigureRequest" } } }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, "4XX": { "$ref": "#/components/responses/Error" }, @@ -1606,14 +1626,14 @@ } }, "/trust-quorum/prepare-and-commit": { - "post": { + "put": { "summary": "Attempt to prepare and commit a trust quorum configuration", "operationId": "trust_quorum_prepare_and_commit", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumPrepareAndCommitRequest" + "$ref": "#/components/schemas/PrepareAndCommitRequest" } } }, @@ -1640,29 +1660,22 @@ } }, "/trust-quorum/proxy/commit": { - "post": { + "put": { "summary": "Proxy a commit operation to another trust quorum node", "operationId": "trust_quorum_proxy_commit", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumProxyCommitRequest" + "$ref": "#/components/schemas/ProxyCommitRequest" } } }, "required": true }, "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CommitStatus" - } - } - } + "204": { + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -1674,14 +1687,14 @@ } }, "/trust-quorum/proxy/prepare-and-commit": { - "post": { + "put": { "summary": "Proxy a prepare-and-commit operation to another trust quorum node", "operationId": "trust_quorum_proxy_prepare_and_commit", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumProxyPrepareAndCommitRequest" + "$ref": "#/components/schemas/ProxyPrepareAndCommitRequest" } } }, @@ -1708,19 +1721,29 @@ } }, "/trust-quorum/proxy/status": { - "post": { + "get": { "summary": "Proxy a status request to another trust quorum node", "operationId": "trust_quorum_proxy_status", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TrustQuorumProxyStatusRequest" - } + "parameters": [ + { + "in": "query", + "name": "part_number", + "description": "Oxide Part Number", + "required": true, + "schema": { + "type": "string" } }, - "required": true - }, + { + "in": "query", + "name": "serial_number", + "description": "Serial number (unique for a given part number)", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "successful operation", @@ -1741,42 +1764,15 @@ } } }, - "/trust-quorum/reconfigure": { - "post": { - "summary": "Initiate a trust quorum reconfiguration", - "operationId": "trust_quorum_reconfigure", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TrustQuorumReconfigureRequest" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/trust-quorum/upgrade-from-lrtq": { - "post": { + "/trust-quorum/upgrade": { + "put": { "summary": "Initiate an upgrade from LRTQ", "operationId": "trust_quorum_upgrade_from_lrtq", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrustQuorumLrtqUpgradeRequest" + "$ref": "#/components/schemas/LrtqUpgradeRequest" } } }, @@ -3668,6 +3664,24 @@ "invalid_share_id" ] }, + "CommitRequest": { + "description": "Request to commit a trust quorum configuration at a given epoch.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + } + }, + "required": [ + "epoch", + "rack_id" + ] + }, "CommitStatus": { "description": "Whether or not a configuration has been committed or is still underway.", "type": "string", @@ -6427,6 +6441,38 @@ "volume_size" ] }, + "LrtqUpgradeRequest": { + "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + }, + "threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "epoch", + "members", + "rack_id", + "threshold" + ] + }, "M2Slot": { "description": "Describes an M.2 slot, often in the context of writing a system image to it.", "type": "string", @@ -7941,6 +7987,18 @@ "speed400_g" ] }, + "PrepareAndCommitRequest": { + "description": "Request to prepare and commit a trust quorum configuration.\n\nThis is the `Configuration` sent to a node that missed the `Prepare` phase.", + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/Configuration" + } + }, + "required": [ + "config" + ] + }, "PriorityDimension": { "description": "A dimension along with bundles can be sorted, to determine priority.", "oneOf": [ @@ -8186,6 +8244,65 @@ "type": "string", "format": "uuid" }, + "ProxyCommitRequest": { + "description": "Request to proxy a commit operation to another trust quorum node.", + "type": "object", + "properties": { + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "epoch": { + "description": "The epoch to commit.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rack_id": { + "description": "Unique ID of the rack.", + "allOf": [ + { + "$ref": "#/components/schemas/RackUuid" + } + ] + } + }, + "required": [ + "destination", + "epoch", + "rack_id" + ] + }, + "ProxyPrepareAndCommitRequest": { + "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", + "type": "object", + "properties": { + "config": { + "description": "The configuration to prepare and commit.", + "allOf": [ + { + "$ref": "#/components/schemas/Configuration" + } + ] + }, + "destination": { + "description": "The target node to proxy the request to.", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + } + }, + "required": [ + "config", + "destination" + ] + }, "QemuPvpanic": { "type": "object", "properties": { @@ -8285,6 +8402,44 @@ "type": "string", "format": "uuid" }, + "ReconfigureRequest": { + "description": "Reconfigure message for trust quorum changes.", + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "last_committed_epoch": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseboardId" + }, + "uniqueItems": true + }, + "rack_id": { + "$ref": "#/components/schemas/RackUuid" + }, + "threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "epoch", + "members", + "rack_id", + "threshold" + ] + }, "RemoveMupdateOverrideBootSuccessInventory": { "description": "Status of removing the mupdate override on the boot disk.", "oneOf": [ @@ -9232,182 +9387,6 @@ "uplinks" ] }, - "TrustQuorumCommitRequest": { - "description": "Request to commit a trust quorum configuration at a given epoch.", - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "rack_id": { - "$ref": "#/components/schemas/RackUuid" - } - }, - "required": [ - "epoch", - "rack_id" - ] - }, - "TrustQuorumLrtqUpgradeRequest": { - "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BaseboardId" - }, - "uniqueItems": true - }, - "rack_id": { - "$ref": "#/components/schemas/RackUuid" - }, - "threshold": { - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "epoch", - "members", - "rack_id", - "threshold" - ] - }, - "TrustQuorumPrepareAndCommitRequest": { - "description": "Request to prepare and commit a trust quorum configuration.\n\nThis is the `Configuration` sent to a node that missed the `Prepare` phase.", - "type": "object", - "properties": { - "config": { - "$ref": "#/components/schemas/Configuration" - } - }, - "required": [ - "config" - ] - }, - "TrustQuorumProxyCommitRequest": { - "description": "Request to proxy a commit operation to another trust quorum node.", - "type": "object", - "properties": { - "destination": { - "description": "The target node to proxy the request to.", - "allOf": [ - { - "$ref": "#/components/schemas/BaseboardId" - } - ] - }, - "epoch": { - "description": "The epoch to commit.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "rack_id": { - "description": "Unique ID of the rack.", - "allOf": [ - { - "$ref": "#/components/schemas/RackUuid" - } - ] - } - }, - "required": [ - "destination", - "epoch", - "rack_id" - ] - }, - "TrustQuorumProxyPrepareAndCommitRequest": { - "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", - "type": "object", - "properties": { - "config": { - "description": "The configuration to prepare and commit.", - "allOf": [ - { - "$ref": "#/components/schemas/Configuration" - } - ] - }, - "destination": { - "description": "The target node to proxy the request to.", - "allOf": [ - { - "$ref": "#/components/schemas/BaseboardId" - } - ] - } - }, - "required": [ - "config", - "destination" - ] - }, - "TrustQuorumProxyStatusRequest": { - "description": "Request to proxy a status request to another trust quorum node.", - "type": "object", - "properties": { - "destination": { - "description": "The target node to get the status from.", - "allOf": [ - { - "$ref": "#/components/schemas/BaseboardId" - } - ] - } - }, - "required": [ - "destination" - ] - }, - "TrustQuorumReconfigureRequest": { - "description": "Reconfigure message for trust quorum changes.", - "type": "object", - "properties": { - "epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "last_committed_epoch": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BaseboardId" - }, - "uniqueItems": true - }, - "rack_id": { - "$ref": "#/components/schemas/RackUuid" - }, - "threshold": { - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "epoch", - "members", - "rack_id", - "threshold" - ] - }, "TxEqConfig": { "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", "type": "object", diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 2518ddb6e44..210b9fdf63b 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-13.0.0-065232.json \ No newline at end of file +sled-agent-13.0.0-4e865c.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index c3646624cc2..ed420c11cff 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -1069,39 +1069,36 @@ pub trait SledAgentApi { /// Initiate a trust quorum reconfiguration #[endpoint { - method = POST, - path = "/trust-quorum/reconfigure", + method = PUT, + path = "/trust-quorum/configuration", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_reconfigure( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result; /// Initiate an upgrade from LRTQ #[endpoint { - method = POST, - path = "/trust-quorum/upgrade-from-lrtq", + method = PUT, + path = "/trust-quorum/upgrade", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_upgrade_from_lrtq( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result; /// Commit a trust quorum configuration #[endpoint { - method = POST, + method = PUT, path = "/trust-quorum/commit", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_commit( request_context: RequestContext, - body: TypedBody, - ) -> Result< - HttpResponseOk, - HttpError, - >; + body: TypedBody, + ) -> Result; /// Get the coordinator status if this node is coordinating a reconfiguration #[endpoint { @@ -1118,61 +1115,45 @@ pub trait SledAgentApi { /// Attempt to prepare and commit a trust quorum configuration #[endpoint { - method = POST, + method = PUT, path = "/trust-quorum/prepare-and-commit", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_prepare_and_commit( request_context: RequestContext, - body: TypedBody< - latest::trust_quorum::TrustQuorumPrepareAndCommitRequest, - >, - ) -> Result< - HttpResponseOk, - HttpError, - >; + body: TypedBody, + ) -> Result, HttpError>; /// Proxy a commit operation to another trust quorum node #[endpoint { - method = POST, + method = PUT, path = "/trust-quorum/proxy/commit", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_proxy_commit( request_context: RequestContext, - body: TypedBody, - ) -> Result< - HttpResponseOk, - HttpError, - >; + body: TypedBody, + ) -> Result; /// Proxy a prepare-and-commit operation to another trust quorum node #[endpoint { - method = POST, + method = PUT, path = "/trust-quorum/proxy/prepare-and-commit", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_proxy_prepare_and_commit( request_context: RequestContext, - body: TypedBody< - latest::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest, - >, - ) -> Result< - HttpResponseOk, - HttpError, - >; + body: TypedBody, + ) -> Result, HttpError>; /// Proxy a status request to another trust quorum node #[endpoint { - method = POST, + method = GET, path = "/trust-quorum/proxy/status", versions = VERSION_ADD_TRUST_QUORUM.., }] async fn trust_quorum_proxy_status( request_context: RequestContext, - body: TypedBody, - ) -> Result< - HttpResponseOk, - HttpError, - >; + query_params: Query, + ) -> Result, HttpError>; } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 79ab2c24441..94614109810 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -49,6 +49,7 @@ use sled_agent_types::instance::{ use sled_agent_types::inventory::{Inventory, OmicronSledConfig}; use sled_agent_types::probes::ProbeSet; use sled_agent_types::sled::AddSledRequest; +use sled_agent_types::sled::BaseboardId; use sled_agent_types::support_bundle::{ RangeRequestHeaders, SupportBundleFilePathParam, SupportBundleFinalizeQueryParams, SupportBundleListPathParam, @@ -56,10 +57,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitStatus, CoordinatorStatus, NodeStatus, TrustQuorumCommitRequest, - TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, - TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, - TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeRequest, + NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, + ProxyPrepareAndCommitRequest, ReconfigureRequest, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -1187,7 +1187,7 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_reconfigure( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = request_context.context(); let request = body.into_inner(); @@ -1210,7 +1210,7 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_upgrade_from_lrtq( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = request_context.context(); let request = body.into_inner(); @@ -1232,8 +1232,8 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_commit( request_context: RequestContext, - body: TypedBody, - ) -> Result, HttpError> { + body: TypedBody, + ) -> Result { let sa = request_context.context(); let request = body.into_inner(); @@ -1243,7 +1243,14 @@ impl SledAgentApi for SledAgentImpl { .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Ok(HttpResponseOk(status)) + // Pending is not expected for commit operations - it indicates an error + if status == CommitStatus::Pending { + return Err(HttpError::for_internal_error( + "commit returned Pending, which is unexpected".to_string(), + )); + } + + Ok(HttpResponseUpdatedNoContent()) } async fn trust_quorum_coordinator_status( @@ -1262,7 +1269,7 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_prepare_and_commit( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); @@ -1278,8 +1285,8 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_proxy_commit( request_context: RequestContext, - body: TypedBody, - ) -> Result, HttpError> { + body: TypedBody, + ) -> Result { let sa = request_context.context(); let request = body.into_inner(); @@ -1290,12 +1297,19 @@ impl SledAgentApi for SledAgentImpl { .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Ok(HttpResponseOk(status)) + // Pending is not expected for commit operations - it indicates an error + if status == CommitStatus::Pending { + return Err(HttpError::for_internal_error( + "commit returned Pending, which is unexpected".to_string(), + )); + } + + Ok(HttpResponseUpdatedNoContent()) } async fn trust_quorum_proxy_prepare_and_commit( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result, HttpError> { let sa = request_context.context(); let request = body.into_inner(); @@ -1312,15 +1326,15 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_proxy_status( request_context: RequestContext, - body: TypedBody, + query_params: Query, ) -> Result, HttpError> { let sa = request_context.context(); - let request = body.into_inner(); + let destination = query_params.into_inner(); let status = sa .trust_quorum() .proxy() - .status(request.destination) + .status(destination) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 484721ed525..d549c1d29e9 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -61,6 +61,7 @@ use sled_agent_types::instance::{ use sled_agent_types::inventory::{Inventory, OmicronSledConfig}; use sled_agent_types::probes::ProbeSet; use sled_agent_types::sled::AddSledRequest; +use sled_agent_types::sled::BaseboardId; use sled_agent_types::support_bundle::{ RangeRequestHeaders, SupportBundleFilePathParam, SupportBundleFinalizeQueryParams, SupportBundleListPathParam, @@ -68,10 +69,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitStatus, CoordinatorStatus, NodeStatus, TrustQuorumCommitRequest, - TrustQuorumLrtqUpgradeRequest, TrustQuorumPrepareAndCommitRequest, - TrustQuorumProxyCommitRequest, TrustQuorumProxyPrepareAndCommitRequest, - TrustQuorumProxyStatusRequest, TrustQuorumReconfigureRequest, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeRequest, + NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, + ProxyPrepareAndCommitRequest, ReconfigureRequest, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -931,56 +931,55 @@ impl SledAgentApi for SledAgentSimImpl { async fn trust_quorum_reconfigure( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result { method_unimplemented() } async fn trust_quorum_upgrade_from_lrtq( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result { method_unimplemented() } async fn trust_quorum_commit( _request_context: RequestContext, - _body: TypedBody, - ) -> Result, HttpError> { + _body: TypedBody, + ) -> Result { method_unimplemented() } async fn trust_quorum_coordinator_status( _request_context: RequestContext, - ) -> Result>, HttpError> - { + ) -> Result>, HttpError> { method_unimplemented() } async fn trust_quorum_prepare_and_commit( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_proxy_commit( _request_context: RequestContext, - _body: TypedBody, - ) -> Result, HttpError> { + _body: TypedBody, + ) -> Result { method_unimplemented() } async fn trust_quorum_proxy_prepare_and_commit( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result, HttpError> { method_unimplemented() } async fn trust_quorum_proxy_status( _request_context: RequestContext, - _body: TypedBody, + _query_params: Query, ) -> Result, HttpError> { method_unimplemented() } diff --git a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs index 08c305dd501..ed460038f97 100644 --- a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs +++ b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs @@ -28,7 +28,7 @@ pub use trust_quorum_types_versions::v1::types::{Epoch, Threshold}; /// Reconfigure message for trust quorum changes. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumReconfigureRequest { +pub struct ReconfigureRequest { pub rack_id: RackUuid, pub epoch: Epoch, pub last_committed_epoch: Option, @@ -38,7 +38,7 @@ pub struct TrustQuorumReconfigureRequest { /// Request to upgrade from LRTQ (Legacy Rack Trust Quorum). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumLrtqUpgradeRequest { +pub struct LrtqUpgradeRequest { pub rack_id: RackUuid, pub epoch: Epoch, pub members: BTreeSet, @@ -47,7 +47,7 @@ pub struct TrustQuorumLrtqUpgradeRequest { /// Request to commit a trust quorum configuration at a given epoch. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumCommitRequest { +pub struct CommitRequest { pub rack_id: RackUuid, pub epoch: Epoch, } @@ -56,13 +56,13 @@ pub struct TrustQuorumCommitRequest { /// /// This is the `Configuration` sent to a node that missed the `Prepare` phase. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumPrepareAndCommitRequest { +pub struct PrepareAndCommitRequest { pub config: Configuration, } /// Request to proxy a commit operation to another trust quorum node. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumProxyCommitRequest { +pub struct ProxyCommitRequest { /// The target node to proxy the request to. pub destination: BaseboardId, /// Unique ID of the rack. @@ -73,16 +73,9 @@ pub struct TrustQuorumProxyCommitRequest { /// Request to proxy a prepare-and-commit operation to another trust quorum node. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumProxyPrepareAndCommitRequest { +pub struct ProxyPrepareAndCommitRequest { /// The target node to proxy the request to. pub destination: BaseboardId, /// The configuration to prepare and commit. pub config: Configuration, } - -/// Request to proxy a status request to another trust quorum node. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TrustQuorumProxyStatusRequest { - /// The target node to get the status from. - pub destination: BaseboardId, -} diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index ad1597efa9c..3c952fac8bd 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -169,13 +169,12 @@ pub mod trust_quorum { pub use crate::v13::trust_quorum::Threshold; // HTTP request types specific to the sled-agent API - pub use crate::v13::trust_quorum::TrustQuorumCommitRequest; - pub use crate::v13::trust_quorum::TrustQuorumLrtqUpgradeRequest; - pub use crate::v13::trust_quorum::TrustQuorumPrepareAndCommitRequest; - pub use crate::v13::trust_quorum::TrustQuorumProxyCommitRequest; - pub use crate::v13::trust_quorum::TrustQuorumProxyPrepareAndCommitRequest; - pub use crate::v13::trust_quorum::TrustQuorumProxyStatusRequest; - pub use crate::v13::trust_quorum::TrustQuorumReconfigureRequest; + pub use crate::v13::trust_quorum::CommitRequest; + pub use crate::v13::trust_quorum::LrtqUpgradeRequest; + pub use crate::v13::trust_quorum::PrepareAndCommitRequest; + pub use crate::v13::trust_quorum::ProxyCommitRequest; + pub use crate::v13::trust_quorum::ProxyPrepareAndCommitRequest; + pub use crate::v13::trust_quorum::ReconfigureRequest; } pub mod zone_bundle { diff --git a/trust-quorum/protocol/src/coordinator_state.rs b/trust-quorum/protocol/src/coordinator_state.rs index 3b3a5243ee0..bae5f1751bc 100644 --- a/trust-quorum/protocol/src/coordinator_state.rs +++ b/trust-quorum/protocol/src/coordinator_state.rs @@ -4,23 +4,24 @@ //! State of a reconfiguration coordinator inside a [`crate::Node`] +use crate::ConfigurationError; use crate::NodeHandlerCtx; use crate::configuration::new_configuration; use crate::crypto::{ - LrtqShare, PlaintextRackSecrets, ReconstructedRackSecret, decrypt_rack_secrets, + LrtqShare, PlaintextRackSecrets, ReconstructedRackSecret, + decrypt_rack_secrets, }; -use crate::{ConfigurationError}; use crate::validators::{ ReconfigurationError, ValidatedLrtqUpgradeMsg, ValidatedReconfigureMsg, }; use crate::{BaseboardId, Configuration, Epoch, PeerMsgKind, RackSecret}; -use trust_quorum_types::configuration::ConfigurationDiff; use bootstore::trust_quorum::RackSecret as LrtqRackSecret; use daft::{Diffable, Leaf}; use gfss::shamir::Share; use slog::{Logger, error, info, o, warn}; use std::collections::{BTreeMap, BTreeSet}; use std::mem; +use trust_quorum_types::configuration::ConfigurationDiff; // A coordinator can be upgrading from LRTQ or reconfiguring a TQ config. #[derive(Clone, Debug, PartialEq, Eq, Diffable)] diff --git a/trust-quorum/protocol/src/crypto.rs b/trust-quorum/protocol/src/crypto.rs index 54b5b61415d..77f23847c71 100644 --- a/trust-quorum/protocol/src/crypto.rs +++ b/trust-quorum/protocol/src/crypto.rs @@ -89,7 +89,7 @@ pub struct ShareDigestLrtq(Sha3_256Digest); impl From for bootstore::Sha3_256Digest { fn from(value: ShareDigestLrtq) -> Self { - bootstore::Sha3_256Digest::new(value.0 .0) + bootstore::Sha3_256Digest::new(value.0.0) } } @@ -131,7 +131,6 @@ impl PartialEq for ReconstructedRackSecret { } } - impl TryFrom> for ReconstructedRackSecret { type Error = InvalidRackSecretSizeError; fn try_from(value: SecretBox<[u8]>) -> Result { @@ -174,7 +173,6 @@ impl From for ReconstructedRackSecret { } } - /// A shared secret based on GF256 #[derive(Debug)] pub struct RackSecret { @@ -473,8 +471,13 @@ mod tests { let new_rack_secret = RackSecret::new().into(); let encrypted = plaintext.encrypt(rack_id, new_epoch, &new_rack_secret).unwrap(); - let decrypted = - decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &new_rack_secret).unwrap(); + let decrypted = decrypt_rack_secrets( + &encrypted, + rack_id, + new_epoch, + &new_rack_secret, + ) + .unwrap(); // We don't actually do any comparisons of rack secrets outside this // test and we don't want to derive `PartialEq` due to data dependent @@ -503,26 +506,47 @@ mod tests { // Decrypting with wrong rack_id fails. assert!( - decrypt_rack_secrets(&encrypted, RackUuid::new_v4(), new_epoch, &new_rack_secret) - .is_err() + decrypt_rack_secrets( + &encrypted, + RackUuid::new_v4(), + new_epoch, + &new_rack_secret + ) + .is_err() ); // Decrypting with wrong epoch fails. assert!( - decrypt_rack_secrets(&encrypted, RackUuid::new_v4(), Epoch(99), &new_rack_secret) - .is_err() + decrypt_rack_secrets( + &encrypted, + RackUuid::new_v4(), + Epoch(99), + &new_rack_secret + ) + .is_err() ); // Decrypting with the wrong secret fails assert!( - decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &RackSecret::new().into()) - .is_err() + decrypt_rack_secrets( + &encrypted, + rack_id, + new_epoch, + &RackSecret::new().into() + ) + .is_err() ); // Decrypting with corrupted plaintext is invalid encrypted.data = vec![0u8, 1u8].into_boxed_slice(); assert!( - decrypt_rack_secrets(&encrypted, rack_id, new_epoch, &new_rack_secret).is_err() + decrypt_rack_secrets( + &encrypted, + rack_id, + new_epoch, + &new_rack_secret + ) + .is_err() ); } } diff --git a/trust-quorum/protocol/src/lib.rs b/trust-quorum/protocol/src/lib.rs index 2d52479fb61..11f2717baed 100644 --- a/trust-quorum/protocol/src/lib.rs +++ b/trust-quorum/protocol/src/lib.rs @@ -57,9 +57,9 @@ pub use validators::{ // These crypto types and functions are NOT in trust-quorum-types because they // contain sensitive data or have complex implementations tied to this crate. -pub use configuration::new_configuration; #[cfg(feature = "testing")] pub use configuration::configurations_equal_except_for_crypto_data; +pub use configuration::new_configuration; pub use crypto::{ PlaintextRackSecrets, RackSecret, ReconstructedRackSecret, SECRET_LEN, decrypt_rack_secrets, new_salt, diff --git a/trust-quorum/src/task.rs b/trust-quorum/src/task.rs index dd66a68be16..a979d2b13ca 100644 --- a/trust-quorum/src/task.rs +++ b/trust-quorum/src/task.rs @@ -30,9 +30,7 @@ use trust_quorum_protocol::{ }; // Re-export types that need to be visible from this crate -pub use trust_quorum_protocol::{ - CommitStatus, CoordinatorStatus, NodeStatus, -}; +pub use trust_quorum_protocol::{CommitStatus, CoordinatorStatus, NodeStatus}; // TODO: Move to this crate // https://github.com/oxidecomputer/omicron/issues/9311 diff --git a/trust-quorum/types/versions/src/initial/alarm.rs b/trust-quorum/types/versions/src/initial/alarm.rs index 2e1e282da37..dd59901680f 100644 --- a/trust-quorum/types/versions/src/initial/alarm.rs +++ b/trust-quorum/types/versions/src/initial/alarm.rs @@ -15,7 +15,15 @@ use super::types::Epoch; /// An alarm indicating a protocol invariant violation. #[allow(clippy::large_enum_variant)] #[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, )] #[serde(rename_all = "snake_case")] pub enum Alarm { diff --git a/trust-quorum/types/versions/src/initial/configuration.rs b/trust-quorum/types/versions/src/initial/configuration.rs index a7620743be2..424ede2af14 100644 --- a/trust-quorum/types/versions/src/initial/configuration.rs +++ b/trust-quorum/types/versions/src/initial/configuration.rs @@ -13,8 +13,8 @@ use omicron_uuid_kinds::RackUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use slog_error_chain::SlogInlineError; pub use sled_hardware_types::BaseboardId; +use slog_error_chain::SlogInlineError; use super::crypto::{EncryptedRackSecrets, Sha3_256Digest}; use super::types::{Epoch, Threshold}; @@ -32,7 +32,9 @@ pub struct ConfigurationMember { } /// Error creating a configuration. -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError, JsonSchema)] +#[derive( + Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError, JsonSchema, +)] #[serde(rename_all = "snake_case")] pub enum ConfigurationError { #[error("rack secret split error")] diff --git a/trust-quorum/types/versions/src/initial/crypto.rs b/trust-quorum/types/versions/src/initial/crypto.rs index 8d99c5d07c2..e619c2b3dd6 100644 --- a/trust-quorum/types/versions/src/initial/crypto.rs +++ b/trust-quorum/types/versions/src/initial/crypto.rs @@ -65,7 +65,15 @@ pub struct Salt( /// configurations. #[serde_as] #[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + JsonSchema, )] pub struct EncryptedRackSecrets { /// A random value used to derive the key to encrypt the rack secrets for From 2706f6695b7d71b3681778533df6e553daba7fb0 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 17:29:59 -0500 Subject: [PATCH 6/9] Move trust quorum request types to trust-quorum-types-versions Factor ReconfigureMsg, LrtqUpgradeMsg, CommitRequest, and PrepareAndCommitRequest into trust-quorum-types-versions so they can be shared between the protocol implementation and the sled-agent API without duplication. This simplifies the sled-agent HTTP handlers since the API types and protocol types are now identical - no conversion needed. The proxy request types (ProxyCommitRequest, ProxyPrepareAndCommitRequest) remain in sled-agent-types-versions since they add the proxy-specific destination field, and now embed the canonical request types. Co-Authored-By: Claude --- ...65c.json => sled-agent-13.0.0-d88437.json} | 43 +++++++-------- openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 4 +- sled-agent/src/http_entrypoints.rs | 31 +++-------- sled-agent/src/sim/http_entrypoints.rs | 8 +-- .../src/add_trust_quorum/trust_quorum.rs | 50 +++-------------- sled-agent/types/versions/src/latest.rs | 4 +- trust-quorum/protocol/src/messages.rs | 33 +++--------- .../types/versions/src/initial/messages.rs | 54 +++++++++++++++++++ .../types/versions/src/initial/mod.rs | 1 + trust-quorum/types/versions/src/latest.rs | 7 +++ 11 files changed, 111 insertions(+), 126 deletions(-) rename openapi/sled-agent/{sled-agent-13.0.0-4e865c.json => sled-agent-13.0.0-d88437.json} (99%) create mode 100644 trust-quorum/types/versions/src/initial/messages.rs diff --git a/openapi/sled-agent/sled-agent-13.0.0-4e865c.json b/openapi/sled-agent/sled-agent-13.0.0-d88437.json similarity index 99% rename from openapi/sled-agent/sled-agent-13.0.0-4e865c.json rename to openapi/sled-agent/sled-agent-13.0.0-d88437.json index 80eaccf2fa4..978eac24e3d 100644 --- a/openapi/sled-agent/sled-agent-13.0.0-4e865c.json +++ b/openapi/sled-agent/sled-agent-13.0.0-d88437.json @@ -1582,7 +1582,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReconfigureRequest" + "$ref": "#/components/schemas/ReconfigureMsg" } } }, @@ -1772,7 +1772,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LrtqUpgradeRequest" + "$ref": "#/components/schemas/LrtqUpgradeMsg" } } }, @@ -6441,8 +6441,8 @@ "volume_size" ] }, - "LrtqUpgradeRequest": { - "description": "Request to upgrade from LRTQ (Legacy Rack Trust Quorum).", + "LrtqUpgradeMsg": { + "description": "A request from Nexus informing a node to start coordinating an upgrade from LRTQ.", "type": "object", "properties": { "epoch": { @@ -8256,51 +8256,44 @@ } ] }, - "epoch": { - "description": "The epoch to commit.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "rack_id": { - "description": "Unique ID of the rack.", + "request": { + "description": "The commit request to proxy.", "allOf": [ { - "$ref": "#/components/schemas/RackUuid" + "$ref": "#/components/schemas/CommitRequest" } ] } }, "required": [ "destination", - "epoch", - "rack_id" + "request" ] }, "ProxyPrepareAndCommitRequest": { "description": "Request to proxy a prepare-and-commit operation to another trust quorum node.", "type": "object", "properties": { - "config": { - "description": "The configuration to prepare and commit.", + "destination": { + "description": "The target node to proxy the request to.", "allOf": [ { - "$ref": "#/components/schemas/Configuration" + "$ref": "#/components/schemas/BaseboardId" } ] }, - "destination": { - "description": "The target node to proxy the request to.", + "request": { + "description": "The prepare-and-commit request to proxy.", "allOf": [ { - "$ref": "#/components/schemas/BaseboardId" + "$ref": "#/components/schemas/PrepareAndCommitRequest" } ] } }, "required": [ - "config", - "destination" + "destination", + "request" ] }, "QemuPvpanic": { @@ -8402,8 +8395,8 @@ "type": "string", "format": "uuid" }, - "ReconfigureRequest": { - "description": "Reconfigure message for trust quorum changes.", + "ReconfigureMsg": { + "description": "A request from Nexus informing a node to start coordinating a reconfiguration.", "type": "object", "properties": { "epoch": { diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 210b9fdf63b..80525c91bce 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-13.0.0-4e865c.json \ No newline at end of file +sled-agent-13.0.0-d88437.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index ed420c11cff..64cefa70a13 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -1075,7 +1075,7 @@ pub trait SledAgentApi { }] async fn trust_quorum_reconfigure( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result; /// Initiate an upgrade from LRTQ @@ -1086,7 +1086,7 @@ pub trait SledAgentApi { }] async fn trust_quorum_upgrade_from_lrtq( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result; /// Commit a trust quorum configuration diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 94614109810..bf5167bff71 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -57,9 +57,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeRequest, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, - ProxyPrepareAndCommitRequest, ReconfigureRequest, + ProxyPrepareAndCommitRequest, ReconfigureMsg, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -1187,18 +1187,10 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_reconfigure( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = request_context.context(); - let request = body.into_inner(); - - let msg = trust_quorum_protocol::ReconfigureMsg { - rack_id: request.rack_id, - epoch: request.epoch, - last_committed_epoch: request.last_committed_epoch, - members: request.members, - threshold: request.threshold, - }; + let msg = body.into_inner(); sa.trust_quorum() .reconfigure(msg) @@ -1210,17 +1202,10 @@ impl SledAgentApi for SledAgentImpl { async fn trust_quorum_upgrade_from_lrtq( request_context: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = request_context.context(); - let request = body.into_inner(); - - let msg = trust_quorum_protocol::LrtqUpgradeMsg { - rack_id: request.rack_id, - epoch: request.epoch, - members: request.members, - threshold: request.threshold, - }; + let msg = body.into_inner(); sa.trust_quorum() .upgrade_from_lrtq(msg) @@ -1293,7 +1278,7 @@ impl SledAgentApi for SledAgentImpl { let status = sa .trust_quorum() .proxy() - .commit(request.destination, request.rack_id, request.epoch) + .commit(request.destination, request.request.rack_id, request.request.epoch) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; @@ -1317,7 +1302,7 @@ impl SledAgentApi for SledAgentImpl { let status = sa .trust_quorum() .proxy() - .prepare_and_commit(request.destination, request.config) + .prepare_and_commit(request.destination, request.request.config) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index d549c1d29e9..ec1b50e7c1d 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -69,9 +69,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeRequest, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, - ProxyPrepareAndCommitRequest, ReconfigureRequest, + ProxyPrepareAndCommitRequest, ReconfigureMsg, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -931,14 +931,14 @@ impl SledAgentApi for SledAgentSimImpl { async fn trust_quorum_reconfigure( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result { method_unimplemented() } async fn trust_quorum_upgrade_from_lrtq( _request_context: RequestContext, - _body: TypedBody, + _body: TypedBody, ) -> Result { method_unimplemented() } diff --git a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs index ed460038f97..98ccb72a860 100644 --- a/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs +++ b/sled-agent/types/versions/src/add_trust_quorum/trust_quorum.rs @@ -7,9 +7,6 @@ //! Core types are re-exported from `trust-quorum-types-versions` to ensure //! consistency with the trust quorum protocol implementation. -use std::collections::BTreeSet; - -use omicron_uuid_kinds::RackUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,55 +17,22 @@ use super::super::v1::sled::BaseboardId; pub use trust_quorum_types_versions::v1::alarm::Alarm; pub use trust_quorum_types_versions::v1::configuration::Configuration; pub use trust_quorum_types_versions::v1::crypto::EncryptedRackSecrets; +pub use trust_quorum_types_versions::v1::messages::{ + CommitRequest, LrtqUpgradeMsg, PrepareAndCommitRequest, ReconfigureMsg, +}; pub use trust_quorum_types_versions::v1::persistent_state::ExpungedMetadata; pub use trust_quorum_types_versions::v1::status::{ CommitStatus, CoordinatorStatus, NodePersistentStateSummary, NodeStatus, }; pub use trust_quorum_types_versions::v1::types::{Epoch, Threshold}; -/// Reconfigure message for trust quorum changes. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct ReconfigureRequest { - pub rack_id: RackUuid, - pub epoch: Epoch, - pub last_committed_epoch: Option, - pub members: BTreeSet, - pub threshold: Threshold, -} - -/// Request to upgrade from LRTQ (Legacy Rack Trust Quorum). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct LrtqUpgradeRequest { - pub rack_id: RackUuid, - pub epoch: Epoch, - pub members: BTreeSet, - pub threshold: Threshold, -} - -/// Request to commit a trust quorum configuration at a given epoch. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct CommitRequest { - pub rack_id: RackUuid, - pub epoch: Epoch, -} - -/// Request to prepare and commit a trust quorum configuration. -/// -/// This is the `Configuration` sent to a node that missed the `Prepare` phase. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct PrepareAndCommitRequest { - pub config: Configuration, -} - /// Request to proxy a commit operation to another trust quorum node. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct ProxyCommitRequest { /// The target node to proxy the request to. pub destination: BaseboardId, - /// Unique ID of the rack. - pub rack_id: RackUuid, - /// The epoch to commit. - pub epoch: Epoch, + /// The commit request to proxy. + pub request: CommitRequest, } /// Request to proxy a prepare-and-commit operation to another trust quorum node. @@ -76,6 +40,6 @@ pub struct ProxyCommitRequest { pub struct ProxyPrepareAndCommitRequest { /// The target node to proxy the request to. pub destination: BaseboardId, - /// The configuration to prepare and commit. - pub config: Configuration, + /// The prepare-and-commit request to proxy. + pub request: PrepareAndCommitRequest, } diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 3c952fac8bd..59757c7c5ee 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -170,11 +170,11 @@ pub mod trust_quorum { // HTTP request types specific to the sled-agent API pub use crate::v13::trust_quorum::CommitRequest; - pub use crate::v13::trust_quorum::LrtqUpgradeRequest; + pub use crate::v13::trust_quorum::LrtqUpgradeMsg; pub use crate::v13::trust_quorum::PrepareAndCommitRequest; pub use crate::v13::trust_quorum::ProxyCommitRequest; pub use crate::v13::trust_quorum::ProxyPrepareAndCommitRequest; - pub use crate::v13::trust_quorum::ReconfigureRequest; + pub use crate::v13::trust_quorum::ReconfigureMsg; } pub mod zone_bundle { diff --git a/trust-quorum/protocol/src/messages.rs b/trust-quorum/protocol/src/messages.rs index ea298fa3558..d0ff554f23d 100644 --- a/trust-quorum/protocol/src/messages.rs +++ b/trust-quorum/protocol/src/messages.rs @@ -7,36 +7,17 @@ #[cfg(feature = "testing")] use crate::configuration::configurations_equal_except_for_crypto_data; use crate::crypto::LrtqShare; -use crate::{BaseboardId, Configuration, Epoch, Threshold}; +use crate::{Configuration, Epoch}; use gfss::shamir::Share; use omicron_uuid_kinds::RackUuid; use serde::{Deserialize, Serialize}; -use std::collections::BTreeSet; -/// A request from nexus informing a node to start coordinating a -/// reconfiguration. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ReconfigureMsg { - pub rack_id: RackUuid, - pub epoch: Epoch, - pub last_committed_epoch: Option, - pub members: BTreeSet, - pub threshold: Threshold, -} - -/// A request from nexus informing a node to start coordinating an upgrade from -/// LRTQ -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct LrtqUpgradeMsg { - pub rack_id: RackUuid, - pub epoch: Epoch, - // The members of the LRTQ cluster must be the same as the members of the - // upgraded trust quorum cluster. This is implicit, as the membership of the - // LRTQ cluster is computed based on the existing control plane sleds known - // to Nexus. - pub members: BTreeSet, - pub threshold: Threshold, -} +// Re-export message types from trust-quorum-types for backward compatibility. +// These types were previously defined here but have been moved to support +// API versioning per RFD 619. +pub use trust_quorum_types::messages::{ + CommitRequest, LrtqUpgradeMsg, PrepareAndCommitRequest, ReconfigureMsg, +}; /// Messages sent between trust quorum members over a sprockets channel #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/trust-quorum/types/versions/src/initial/messages.rs b/trust-quorum/types/versions/src/initial/messages.rs new file mode 100644 index 00000000000..3f066d33ac2 --- /dev/null +++ b/trust-quorum/types/versions/src/initial/messages.rs @@ -0,0 +1,54 @@ +// 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/. + +//! Trust quorum protocol messages and API request types. + +use std::collections::BTreeSet; + +use omicron_uuid_kinds::RackUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::configuration::{BaseboardId, Configuration}; +use super::types::{Epoch, Threshold}; + +/// A request from Nexus informing a node to start coordinating a +/// reconfiguration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ReconfigureMsg { + pub rack_id: RackUuid, + pub epoch: Epoch, + pub last_committed_epoch: Option, + pub members: BTreeSet, + pub threshold: Threshold, +} + +/// A request from Nexus informing a node to start coordinating an upgrade from +/// LRTQ. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct LrtqUpgradeMsg { + pub rack_id: RackUuid, + pub epoch: Epoch, + // The members of the LRTQ cluster must be the same as the members of the + // upgraded trust quorum cluster. This is implicit, as the membership of the + // LRTQ cluster is computed based on the existing control plane sleds known + // to Nexus. + pub members: BTreeSet, + pub threshold: Threshold, +} + +/// Request to commit a trust quorum configuration at a given epoch. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct CommitRequest { + pub rack_id: RackUuid, + pub epoch: Epoch, +} + +/// Request to prepare and commit a trust quorum configuration. +/// +/// This is the `Configuration` sent to a node that missed the `Prepare` phase. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct PrepareAndCommitRequest { + pub config: Configuration, +} diff --git a/trust-quorum/types/versions/src/initial/mod.rs b/trust-quorum/types/versions/src/initial/mod.rs index d472721adbf..8d17768e9bd 100644 --- a/trust-quorum/types/versions/src/initial/mod.rs +++ b/trust-quorum/types/versions/src/initial/mod.rs @@ -9,6 +9,7 @@ pub mod alarm; pub mod configuration; pub mod crypto; +pub mod messages; pub mod persistent_state; pub mod status; pub mod types; diff --git a/trust-quorum/types/versions/src/latest.rs b/trust-quorum/types/versions/src/latest.rs index a99233f1c48..568c17459b2 100644 --- a/trust-quorum/types/versions/src/latest.rs +++ b/trust-quorum/types/versions/src/latest.rs @@ -28,6 +28,13 @@ pub mod crypto { pub use crate::v1::crypto::Sha3_256Digest; } +pub mod messages { + pub use crate::v1::messages::CommitRequest; + pub use crate::v1::messages::LrtqUpgradeMsg; + pub use crate::v1::messages::PrepareAndCommitRequest; + pub use crate::v1::messages::ReconfigureMsg; +} + pub mod persistent_state { pub use crate::v1::persistent_state::ExpungedMetadata; pub use crate::v1::persistent_state::PersistentStateSummary; From b2f83b517e2710c8c48fb49135a86544faaa2134 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 17:29:59 -0500 Subject: [PATCH 7/9] Move trust quorum request types to trust-quorum-types-versions Factor ReconfigureMsg, LrtqUpgradeMsg, CommitRequest, and PrepareAndCommitRequest into trust-quorum-types-versions so they can be shared between the protocol implementation and the sled-agent API without duplication. This simplifies the sled-agent HTTP handlers since the API types and protocol types are now identical - no conversion needed. The proxy request types (ProxyCommitRequest, ProxyPrepareAndCommitRequest) remain in sled-agent-types-versions since they add the proxy-specific destination field, and now embed the canonical request types. Co-Authored-By: Claude --- sled-agent/src/http_entrypoints.rs | 12 ++++++++---- sled-agent/src/sim/http_entrypoints.rs | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index bf5167bff71..637ca078479 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -57,9 +57,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, - NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, - ProxyPrepareAndCommitRequest, ReconfigureMsg, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, NodeStatus, + PrepareAndCommitRequest, ProxyCommitRequest, ProxyPrepareAndCommitRequest, + ReconfigureMsg, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, @@ -1278,7 +1278,11 @@ impl SledAgentApi for SledAgentImpl { let status = sa .trust_quorum() .proxy() - .commit(request.destination, request.request.rack_id, request.request.epoch) + .commit( + request.destination, + request.request.rack_id, + request.request.epoch, + ) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index ec1b50e7c1d..cbabd91afb5 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -69,9 +69,9 @@ use sled_agent_types::support_bundle::{ SupportBundleTransferQueryParams, }; use sled_agent_types::trust_quorum::{ - CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, - NodeStatus, PrepareAndCommitRequest, ProxyCommitRequest, - ProxyPrepareAndCommitRequest, ReconfigureMsg, + CommitRequest, CommitStatus, CoordinatorStatus, LrtqUpgradeMsg, NodeStatus, + PrepareAndCommitRequest, ProxyCommitRequest, ProxyPrepareAndCommitRequest, + ReconfigureMsg, }; use sled_agent_types::zone_bundle::{ BundleUtilization, CleanupContext, CleanupContextUpdate, CleanupCount, From 36b83b7ae9ba7d9ded23ada587bf1ee3a85abc6b Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 17:29:59 -0500 Subject: [PATCH 8/9] Move trust quorum request types to trust-quorum-types-versions Factor ReconfigureMsg, LrtqUpgradeMsg, CommitRequest, and PrepareAndCommitRequest into trust-quorum-types-versions so they can be shared between the protocol implementation and the sled-agent API without duplication. This simplifies the sled-agent HTTP handlers since the API types and protocol types are now identical - no conversion needed. The proxy request types (ProxyCommitRequest, ProxyPrepareAndCommitRequest) remain in sled-agent-types-versions since they add the proxy-specific destination field, and now embed the canonical request types. Co-Authored-By: Claude --- nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index 0ad6d7dc708..01503be90c6 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -940,7 +940,7 @@ mod api_impl { async fn trust_quorum_reconfigure( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::ReconfigureRequest, + sled_agent_types::trust_quorum::ReconfigureMsg, >, ) -> Result { unimplemented!() @@ -949,7 +949,7 @@ mod api_impl { async fn trust_quorum_upgrade_from_lrtq( _request_context: RequestContext, _body: TypedBody< - sled_agent_types::trust_quorum::LrtqUpgradeRequest, + sled_agent_types::trust_quorum::LrtqUpgradeMsg, >, ) -> Result { unimplemented!() From 6e3119fd021c96d080c17b44b116621cb406a2cf Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 24 Dec 2025 17:46:30 -0500 Subject: [PATCH 9/9] Move trust quorum request types to trust-quorum-types-versions Factor ReconfigureMsg, LrtqUpgradeMsg, CommitRequest, and PrepareAndCommitRequest into trust-quorum-types-versions so they can be shared between the protocol implementation and the sled-agent API without duplication. This simplifies the sled-agent HTTP handlers since the API types and protocol types are now identical - no conversion needed. The proxy request types (ProxyCommitRequest, ProxyPrepareAndCommitRequest) remain in sled-agent-types-versions since they add the proxy-specific destination field, and now embed the canonical request types. Co-Authored-By: Claude --- .../mgs-updates/src/test_util/host_phase_2_test_state.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index 01503be90c6..0744b9269a6 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -939,18 +939,14 @@ mod api_impl { async fn trust_quorum_reconfigure( _request_context: RequestContext, - _body: TypedBody< - sled_agent_types::trust_quorum::ReconfigureMsg, - >, + _body: TypedBody, ) -> Result { unimplemented!() } async fn trust_quorum_upgrade_from_lrtq( _request_context: RequestContext, - _body: TypedBody< - sled_agent_types::trust_quorum::LrtqUpgradeMsg, - >, + _body: TypedBody, ) -> Result { unimplemented!() }