From 26b7d31c117a37d33718c0b7856fa1455255e83d Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 27 Oct 2025 19:41:41 +0000 Subject: [PATCH 01/16] Add two features for local storage support Bump the sled-agent API to add two new features for local storage support: - Add an endpoint for Nexus to create and destroy local storage datasets. These will be allocated and deallocated as part of the higher level Disk lifecycle for the forthcoming local storage disk type. - Add the ability to delegate a specific zvol to a Propolis zone. This required accepting a new `DelegatedZvol` parameter during vmm registration. --- common/src/api/internal/shared.rs | 10 + illumos-utils/src/zfs.rs | 42 + .../src/test_util/host_phase_2_test_state.rs | 15 + nexus/src/app/instance.rs | 1 + .../sled-agent/sled-agent-7.0.0-b2bd61.json | 8728 +++++++++++++++++ openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 64 +- sled-agent/api/src/v6.rs | 101 + sled-agent/src/http_entrypoints.rs | 35 + sled-agent/src/instance.rs | 44 +- sled-agent/src/sim/http_entrypoints.rs | 38 + sled-agent/src/sim/sled_agent.rs | 47 +- sled-agent/src/sim/storage.rs | 64 +- sled-agent/src/sled_agent.rs | 74 +- sled-agent/types/src/instance.rs | 19 +- 15 files changed, 9264 insertions(+), 20 deletions(-) create mode 100644 openapi/sled-agent/sled-agent-7.0.0-b2bd61.json create mode 100644 sled-agent/api/src/v6.rs diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index 65c9a47e554..e1d1b4d4b4c 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -1109,6 +1109,16 @@ pub struct SledIdentifiers { pub serial: String, } +/// Delegate a ZFS volume to a zone +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct DelegatedZvol { + /// The fully qualified name of the parent dataset + pub parent_dataset: String, + + /// The volume name + pub name: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index beeb8df40ec..bfc2aad7ebe 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -220,6 +220,15 @@ pub struct DestroySnapshotError { err: crate::ExecutionError, } +/// Error returned by [`Zfs::ensure_dataset_volume`]. +#[derive(thiserror::Error, Debug)] +#[error("Failed to ensure volume '{name}': {err}")] +pub struct EnsureDatasetVolumeError { + name: String, + #[source] + err: crate::ExecutionError, +} + /// Wraps commands for interacting with ZFS. pub struct Zfs {} @@ -1339,6 +1348,39 @@ impl Zfs { } Ok(result) } + + pub async fn ensure_dataset_volume( + name: String, + size: ByteCount, + block_size: u32, + ) -> Result<(), EnsureDatasetVolumeError> { + let mut command = Command::new(PFEXEC); + let cmd = command.args(&[ZFS, "create"]); + + cmd.args(&[ + "-V", + &size.to_bytes().to_string(), + "-o", + &format!("volblocksize={}", block_size), + &name, + ]); + + // The command to create a dataset is not idempotent and will fail with + // "dataset already exists" if the volume is created already. Eat this + // and return Ok instead. + + match execute_async(cmd).await { + Ok(_) => Ok(()), + + Err(crate::ExecutionError::CommandFailure(info)) + if info.stderr.contains("dataset already exists") => + { + Ok(()) + } + + Err(err) => Err(EnsureDatasetVolumeError { name, err }), + } + } } /// A read-only snapshot of a ZFS filesystem. 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 5501449a345..bda37358020 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 @@ -847,5 +847,20 @@ mod api_impl { ) -> Result { unimplemented!() } + + async fn local_storage_dataset_ensure( + _request_context: RequestContext, + _path_params: Path, + _body: TypedBody, + ) -> Result { + unimplemented!() + } + + async fn local_storage_dataset_delete( + _request_context: RequestContext, + _path_params: Path, + ) -> Result { + unimplemented!() + } } } diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 0bbd1012b14..19edc3251c0 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -1293,6 +1293,7 @@ impl super::Nexus { host_domain: None, search_domains: Vec::new(), }, + delegated_zvols: vec![], }; let instance_id = InstanceUuid::from_untyped_uuid(db_instance.id()); diff --git a/openapi/sled-agent/sled-agent-7.0.0-b2bd61.json b/openapi/sled-agent/sled-agent-7.0.0-b2bd61.json new file mode 100644 index 00000000000..459f2c27a22 --- /dev/null +++ b/openapi/sled-agent/sled-agent-7.0.0-b2bd61.json @@ -0,0 +1,8728 @@ +{ + "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": "7.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" + } + } + } + }, + "/sled-role": { + "get": { + "operationId": "sled_role_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledRole" + } + } + } + }, + "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" + } + } + } + }, + "/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}/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": { + "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": { + "type": "object", + "properties": { + "depot_base_url": { + "type": "string" + } + }, + "required": [ + "depot_base_url" + ] + }, + "ArtifactCopyFromDepotResponse": { + "type": "object" + }, + "ArtifactListResponse": { + "type": "object", + "properties": { + "generation": { + "$ref": "#/components/schemas/Generation" + }, + "list": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + }, + "required": [ + "generation", + "list" + ] + }, + "ArtifactPutResponse": { + "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": { + "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", + "type": "object", + "properties": { + "name": { + "description": "The volume name", + "type": "string" + }, + "parent_dataset": { + "description": "The fully qualified name of the parent dataset", + "type": "string" + } + }, + "required": [ + "name", + "parent_dataset" + ] + }, + "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 [`crate::rack_init::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": { + "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" + ] + }, + "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" + ] + }, + "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 + }, + "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.", + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorityDimension" + }, + "minItems": 2, + "maxItems": 2 + }, + "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" + ] + }, + "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 + }, + "SourceNatConfig": { + "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" + ] + }, + "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": { + "type": "string", + "enum": [ + "complete", + "incomplete" + ] + }, + "SupportBundleUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SupportBundleUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "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" + ] + }, + "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": { + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmIssueDiskSnapshotRequestResponse": { + "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.\n\nSled-agent expects that when an instance spec is provided alongside an `InstanceSledLocalConfig` to initialize a new instance, the NIC IDs in that config's network interface list will match the IDs of the virtio network backends in the instance spec.", + "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 [`ZoneManifestBootInventory`].", + "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/ZoneManifestInventory" + } + ] + } + }, + "required": [ + "mupdate_override", + "zone_manifest" + ] + }, + "ZoneManifestBootInventory": { + "description": "Inventory representation of zone artifacts on the boot disk.\n\nPart of [`ZoneManifestInventory`].", + "type": "object", + "properties": { + "artifacts": { + "title": "IdOrdMap", + "description": "The artifacts on disk.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ZoneArtifactInventory" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneArtifactInventory" + }, + "uniqueItems": true + }, + "source": { + "description": "The manifest source.\n\nIn production this is [`OmicronZoneManifestSource::Installinator`], but in some development and testing flows Sled Agent synthesizes zone manifests. In those cases, the source is [`OmicronZoneManifestSource::SledAgent`].", + "allOf": [ + { + "$ref": "#/components/schemas/OmicronZoneManifestSource" + } + ] + } + }, + "required": [ + "artifacts", + "source" + ] + }, + "ZoneManifestInventory": { + "description": "Inventory representation of a zone manifest.\n\nPart of [`ZoneImageResolverInventory`].\n\nA zone manifest is a listing of all the zones present in a system's install dataset. This struct contains information about the install dataset gathered from a system.", + "type": "object", + "properties": { + "boot_disk_path": { + "description": "The full path to the zone manifest file on the boot disk.", + "type": "string", + "format": "Utf8PathBuf" + }, + "boot_inventory": { + "description": "The manifest read from the boot disk, and whether the manifest is valid.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/ZoneManifestBootInventory" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/ZoneManifestBootInventory" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "non_boot_status": { + "title": "IdOrdMap", + "description": "Information about the install dataset on non-boot disks.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ZoneManifestNonBootInventory" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneManifestNonBootInventory" + }, + "uniqueItems": true + } + }, + "required": [ + "boot_disk_path", + "boot_inventory", + "non_boot_status" + ] + }, + "ZoneManifestNonBootInventory": { + "description": "Inventory representation of a zone manifest on a non-boot disk.\n\nUnlike [`ZoneManifestBootInventory`] which is structured since Reconfigurator makes decisions based on it, information about non-boot disks is purely advisory. For simplicity, we store information in an unstructured format.", + "type": "object", + "properties": { + "is_valid": { + "description": "Whether the status is valid.", + "type": "boolean" + }, + "message": { + "description": "A message describing the status.\n\nIf `is_valid` is true, then the message describes the list of artifacts found and their hashes.\n\nIf `is_valid` is false, then this message describes the reason for the invalid status. This could include errors reading the zone manifest, or zone file mismatches.", + "type": "string" + }, + "path": { + "description": "The full path to the zone manifest JSON on the non-boot disk.", + "type": "string", + "format": "Utf8PathBuf" + }, + "zpool_id": { + "description": "The ID of the non-boot zpool.", + "allOf": [ + { + "$ref": "#/components/schemas/InternalZpoolUuid" + } + ] + } + }, + "required": [ + "is_valid", + "message", + "path", + "zpool_id" + ] + }, + "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" + }, + "ExternalZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ExternalZpoolUuid", + "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 a3a44e12369..f80336b12d0 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-6.0.0-d37dd7.json \ No newline at end of file +sled-agent-7.0.0-b2bd61.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 4ee3de420cf..1f64b4bad0f 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -17,6 +17,7 @@ use nexus_sled_agent_shared::inventory::{ Inventory, OmicronSledConfig, SledRole, }; use omicron_common::{ + api::external::ByteCount, api::external::Generation, api::internal::{ nexus::{DiskRuntimeState, SledVmmState}, @@ -29,7 +30,7 @@ use omicron_common::{ ledger::Ledgerable, }; use omicron_uuid_kinds::{ - DatasetUuid, PropolisUuid, SupportBundleUuid, ZpoolUuid, + DatasetUuid, ExternalZpoolUuid, PropolisUuid, SupportBundleUuid, ZpoolUuid, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -55,6 +56,8 @@ use uuid::Uuid; /// Copies of data types that changed between v3 and v4. mod v3; +/// Copies of data types that changed between v6 and v7. +mod v6; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -68,6 +71,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (7, DELEGATE_ZVOL_TO_PROPOLIS), (6, ADD_PROBE_PUT_ENDPOINT), (5, NEWTYPE_UUID_BUMP), (4, ADD_NEXUS_LOCKSTEP_PORT_TO_INVENTORY), @@ -357,8 +361,23 @@ pub trait SledAgentApi { ) -> Result, HttpError>; #[endpoint { + operation_id = "vmm_register", method = PUT, path = "/vmms/{propolis_id}", + versions = ..VERSION_DELEGATE_ZVOL_TO_PROPOLIS, + }] + async fn v6_vmm_register( + rqctx: RequestContext, + path_params: Path, + body: TypedBody, + ) -> Result, HttpError> { + Self::vmm_register(rqctx, path_params, body.map(Into::into)).await + } + + #[endpoint { + method = PUT, + path = "/vmms/{propolis_id}", + versions = VERSION_DELEGATE_ZVOL_TO_PROPOLIS.. }] async fn vmm_register( rqctx: RequestContext, @@ -809,6 +828,29 @@ pub trait SledAgentApi { request_context: RequestContext, body: TypedBody, ) -> Result; + + /// Create a local storage dataset + #[endpoint { + method = POST, + path = "/local-storage/{zpool_id}/{dataset_id}", + versions = VERSION_DELEGATE_ZVOL_TO_PROPOLIS.., + }] + async fn local_storage_dataset_ensure( + request_context: RequestContext, + path_params: Path, + body: TypedBody, + ) -> Result; + + /// Delete a local storage dataset + #[endpoint { + method = DELETE, + path = "/local-storage/{zpool_id}/{dataset_id}", + versions = VERSION_DELEGATE_ZVOL_TO_PROPOLIS.., + }] + async fn local_storage_dataset_delete( + request_context: RequestContext, + path_params: Path, + ) -> Result; } #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] @@ -1053,3 +1095,23 @@ pub enum OperatorSwitchZonePolicy { /// Even if a switch zone is present, stop the switch zone. StopDespiteSwitchPresence, } + +/// Path parameters for Local Storage dataset related requests +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct LocalStoragePathParam { + pub zpool_id: ExternalZpoolUuid, + pub dataset_id: DatasetUuid, +} + +/// Dataset and Volume details for a Local Storage dataset ensure request +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct LocalStorageDatasetEnsureRequest { + /// Size of the parent dataset + pub dataset_size: ByteCount, + + /// Size of the zvol + pub volume_size: ByteCount, + + /// Block size for the zvol + pub block_size: u32, +} diff --git a/sled-agent/api/src/v6.rs b/sled-agent/api/src/v6.rs new file mode 100644 index 00000000000..c9c66aae666 --- /dev/null +++ b/sled-agent/api/src/v6.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/. + +use omicron_common::api::external::Hostname; +use omicron_common::api::internal::nexus::VmmRuntimeState; +use omicron_common::api::internal::shared::DhcpConfig; +use omicron_common::api::internal::shared::NetworkInterface; +use omicron_common::api::internal::shared::ResolvedVpcFirewallRule; +use omicron_common::api::internal::shared::SourceNatConfig; +use omicron_uuid_kinds::InstanceUuid; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use sled_agent_types::instance::InstanceMetadata; +use sled_agent_types::instance::VmmSpec; +use std::net::IpAddr; +use std::net::SocketAddr; +use uuid::Uuid; + +/// The body of a request to ensure that a instance and VMM are known to a sled +/// agent. +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct InstanceEnsureBody { + /// The virtual hardware configuration this virtual machine should have when + /// it is started. + pub vmm_spec: VmmSpec, + + /// Information about the sled-local configuration that needs to be + /// established to make the VM's virtual hardware fully functional. + pub local_config: InstanceSledLocalConfig, + + /// The initial VMM runtime state for the VMM being registered. + pub vmm_runtime: VmmRuntimeState, + + /// The ID of the instance for which this VMM is being created. + pub instance_id: InstanceUuid, + + /// The ID of the migration in to this VMM, if this VMM is being + /// ensured is part of a migration in. If this is `None`, the VMM is not + /// being created due to a migration. + pub migration_id: Option, + + /// The address at which this VMM should serve a Propolis server API. + pub propolis_addr: SocketAddr, + + /// Metadata used to track instance statistics. + pub metadata: InstanceMetadata, +} + +/// Describes sled-local configuration that a sled-agent must establish to make +/// the instance's virtual hardware fully functional. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct InstanceSledLocalConfig { + pub hostname: Hostname, + pub nics: Vec, + pub source_nat: SourceNatConfig, + /// Zero or more external IP addresses (either floating or ephemeral), + /// provided to an instance to allow inbound connectivity. + pub ephemeral_ip: Option, + pub floating_ips: Vec, + pub firewall_rules: Vec, + pub dhcp_config: DhcpConfig, +} + +impl From + for sled_agent_types::instance::InstanceEnsureBody +{ + fn from( + v6: InstanceEnsureBody, + ) -> sled_agent_types::instance::InstanceEnsureBody { + sled_agent_types::instance::InstanceEnsureBody { + vmm_spec: v6.vmm_spec, + local_config: v6.local_config.into(), + vmm_runtime: v6.vmm_runtime, + instance_id: v6.instance_id, + migration_id: v6.migration_id, + propolis_addr: v6.propolis_addr, + metadata: v6.metadata, + } + } +} + +impl From + for sled_agent_types::instance::InstanceSledLocalConfig +{ + fn from( + v6: InstanceSledLocalConfig, + ) -> sled_agent_types::instance::InstanceSledLocalConfig { + sled_agent_types::instance::InstanceSledLocalConfig { + hostname: v6.hostname, + nics: v6.nics, + source_nat: v6.source_nat, + ephemeral_ip: v6.ephemeral_ip, + floating_ips: v6.floating_ips, + firewall_rules: v6.firewall_rules, + dhcp_config: v6.dhcp_config, + delegated_zvols: vec![], + } + } +} diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 2a9da41f2bb..a1f400480de 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -1096,4 +1096,39 @@ impl SledAgentApi for SledAgentImpl { request_context.context().set_probes(body.into_inner().probes); Ok(HttpResponseUpdatedNoContent()) } + + async fn local_storage_dataset_ensure( + request_context: RequestContext, + path_params: Path, + body: TypedBody, + ) -> Result { + let sa = request_context.context(); + let path_params = path_params.into_inner(); + let request = body.into_inner(); + + sa.create_local_storage_dataset( + path_params.zpool_id, + path_params.dataset_id, + request, + ) + .await?; + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn local_storage_dataset_delete( + request_context: RequestContext, + path_params: Path, + ) -> Result { + let sa = request_context.context(); + let path_params = path_params.into_inner(); + + sa.delete_local_storage_dataset( + path_params.zpool_id, + path_params.dataset_id, + ) + .await?; + + Ok(HttpResponseUpdatedNoContent()) + } } diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index 12e1c39adf1..1df45ea3fcc 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -23,7 +23,8 @@ use illumos_utils::zone::PROPOLIS_ZONE_PREFIX; use illumos_utils::zpool::ZpoolOrRamdisk; use omicron_common::api::internal::nexus::{SledVmmState, VmmRuntimeState}; use omicron_common::api::internal::shared::{ - NetworkInterface, ResolvedVpcFirewallRule, SledIdentifiers, SourceNatConfig, + DelegatedZvol, NetworkInterface, ResolvedVpcFirewallRule, SledIdentifiers, + SourceNatConfig, }; use omicron_common::backoff; use omicron_common::backoff::BackoffError; @@ -541,6 +542,9 @@ struct InstanceRunner { // Queue to notify the sled agent's metrics task about our VNICs. metrics_queue: MetricsRequestQueue, + + // Zvols to delegate to the Propolis zone + delegated_zvols: Vec, } impl InstanceRunner { @@ -1649,6 +1653,7 @@ impl Instance { zone_builder_factory, zone_bundler, metrics_queue, + delegated_zvols: local_config.delegated_zvols, }; let runner_handle = tokio::task::spawn(async move { @@ -1987,6 +1992,32 @@ impl InstanceRunner { opte_ports.push(port); } + // Each delegated Zvol requires delegating the parent dataset + let datasets: Vec<_> = self + .delegated_zvols + .iter() + .map(|delegated_zvol| zone::Dataset { + name: delegated_zvol.parent_dataset.clone(), + }) + .collect(); + + // For delegated devices, include the default list plus any for the + // delegated zvol devices. + let mut devices = vec![ + zone::Device { name: "/dev/vmm/*".to_string() }, + zone::Device { name: "/dev/vmmctl".to_string() }, + zone::Device { name: "/dev/viona".to_string() }, + ]; + + for delegated_zvol in &self.delegated_zvols { + devices.push(zone::Device { + name: format!( + "/dev/zvol/rdsk/{}/{}", + delegated_zvol.parent_dataset, delegated_zvol.name, + ), + }); + } + // Create a zone for the propolis instance, using the previously // configured VNICs. let zname = propolis_zone_name(&self.propolis_id); @@ -1998,6 +2029,7 @@ impl InstanceRunner { .into_iter() .choose(&mut rng) .ok_or_else(|| Error::U2NotFound)?; + let installed_zone = self .zone_builder_factory .builder() @@ -2009,14 +2041,10 @@ impl InstanceRunner { .with_unique_name(OmicronZoneUuid::from_untyped_uuid( self.propolis_id.into_untyped_uuid(), )) - .with_datasets(&[]) + .with_datasets(&datasets) .with_filesystems(&[]) .with_data_links(&[]) - .with_devices(&[ - zone::Device { name: "/dev/vmm/*".to_string() }, - zone::Device { name: "/dev/vmmctl".to_string() }, - zone::Device { name: "/dev/viona".to_string() }, - ]) + .with_devices(&devices) .with_opte_ports(opte_ports) .with_links(vec![]) .with_limit_priv(vec![]) @@ -2492,6 +2520,7 @@ mod tests { host_domain: None, search_domains: vec![], }, + delegated_zvols: vec![], }; InstanceInitialState { @@ -3102,6 +3131,7 @@ mod tests { zone_builder_factory, zone_bundler, metrics_queue, + delegated_zvols: local_config.delegated_zvols, } } } diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 17750f5ba79..cfc52fd15c6 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -35,6 +35,8 @@ use omicron_common::api::internal::shared::VirtualNetworkInterfaceHost; use omicron_common::api::internal::shared::{ ResolvedVpcRouteSet, ResolvedVpcRouteState, SwitchPorts, }; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::ZpoolUuid; use range_requests::PotentialRange; use sled_agent_api::*; use sled_agent_types::bootstore::BootstoreStatus; @@ -605,6 +607,42 @@ impl SledAgentApi for SledAgentSimImpl { Ok(HttpResponseDeleted()) } + async fn local_storage_dataset_ensure( + rqctx: RequestContext, + path_params: Path, + body: TypedBody, + ) -> Result { + let sa = rqctx.context(); + + let LocalStoragePathParam { zpool_id, dataset_id } = + path_params.into_inner(); + + sa.ensure_local_storage_dataset( + zpool_id, + dataset_id, + body.into_inner(), + ); + + Ok(HttpResponseUpdatedNoContent()) + } + + async fn local_storage_dataset_delete( + rqctx: RequestContext, + path_params: Path, + ) -> Result { + let sa = rqctx.context(); + + let LocalStoragePathParam { zpool_id, dataset_id } = + path_params.into_inner(); + + sa.drop_dataset( + ZpoolUuid::from_untyped_uuid(zpool_id.into_untyped_uuid()), + dataset_id, + ); + + Ok(HttpResponseUpdatedNoContent()) + } + // --- Unimplemented endpoints --- async fn set_eip_gateways( diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index 4648490b2a0..5c4189308f8 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -46,15 +46,17 @@ use omicron_common::disk::{ DisksManagementResult, OmicronPhysicalDisksConfig, }; use omicron_uuid_kinds::{ - DatasetUuid, GenericUuid, PhysicalDiskUuid, PropolisUuid, SledUuid, - SupportBundleUuid, ZpoolUuid, + DatasetUuid, ExternalZpoolUuid, GenericUuid, PhysicalDiskUuid, + PropolisUuid, SledUuid, SupportBundleUuid, ZpoolUuid, }; use oxnet::Ipv6Net; +use propolis_client::instance_spec::FileStorageBackend; use propolis_client::instance_spec::SpecKey; use propolis_client::{ Client as PropolisClient, types::InstanceInitializationMethod, }; use range_requests::PotentialRange; +use sled_agent_api::LocalStorageDatasetEnsureRequest; use sled_agent_api::SupportBundleMetadata; use sled_agent_types::disk::DiskStateRequested; use sled_agent_types::early_networking::{ @@ -220,6 +222,36 @@ impl SledAgent { )); }; + // Make sure each file backend was ensured before changing any state + for (_id, disk) in vmm_spec.file_backends() { + let FileStorageBackend { block_size, path, .. } = disk; + + // The FileStorageBackend path will be the full device path, so + // strip the beginning, including the first part of the external + // pool name. + let dataset = path.strip_prefix("/dev/zvol/rdsk/oxp_").unwrap(); + + // what remains is: UUID/crypt/local_storage/UUID/vol + let parts: Vec<&str> = dataset.split("/").collect(); + let zpool_id: ZpoolUuid = parts[0].parse().unwrap(); + let dataset_id: DatasetUuid = parts[3].parse().unwrap(); + + // This panics if this dataset was not already created + let request = self + .storage + .lock() + .get_local_storage_dataset(zpool_id, dataset_id); + + // Treat a mismatch here as an error + if request.block_size != *block_size { + return Err(Error::internal_error(&format!( + "request block_size {} does not match FileStorageBackend \ + block_size {block_size}", + request.block_size, + ))); + } + } + for (id, _disk) in vmm_spec.crucible_backends() { let SpecKey::Uuid(id) = id else { return Err(Error::invalid_value( @@ -1024,6 +1056,17 @@ impl SledAgent { ); } } + + pub fn ensure_local_storage_dataset( + &self, + zpool_id: ExternalZpoolUuid, + dataset_id: DatasetUuid, + request: LocalStorageDatasetEnsureRequest, + ) { + self.storage + .lock() + .ensure_local_storage_dataset(zpool_id, dataset_id, request); + } } /// Stored routes (and usage count) for a given VPC/subnet. diff --git a/sled-agent/src/sim/storage.rs b/sled-agent/src/sim/storage.rs index aae6f06f71b..63414cb7309 100644 --- a/sled-agent/src/sim/storage.rs +++ b/sled-agent/src/sim/storage.rs @@ -35,12 +35,14 @@ use omicron_common::disk::DisksManagementResult; use omicron_common::disk::OmicronPhysicalDisksConfig; use omicron_common::disk::SharedDatasetConfig; use omicron_uuid_kinds::DatasetUuid; +use omicron_uuid_kinds::ExternalZpoolUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::ZpoolUuid; use propolis_client::VolumeConstructionRequest; use serde::Serialize; +use sled_agent_api::LocalStorageDatasetEnsureRequest; use sled_agent_types::support_bundle::NESTED_DATASET_NOT_FOUND; use sled_storage::nested_dataset::NestedDatasetConfig; use sled_storage::nested_dataset::NestedDatasetListOptions; @@ -1137,6 +1139,7 @@ pub(crate) struct PhysicalDisk { /// Describes data being simulated within a dataset. pub(crate) enum DatasetContents { Crucible(CrucibleServer), + LocalStorage(LocalStorageDatasetEnsureRequest), } pub(crate) struct Zpool { @@ -1172,10 +1175,15 @@ impl Zpool { end_port, )), ); + let DatasetContents::Crucible(crucible) = self .datasets .get(&id) - .expect("Failed to get the dataset we just inserted"); + .expect("Failed to get the dataset we just inserted") + else { + panic!("just inserted this variant!"); + }; + crucible } @@ -1188,7 +1196,9 @@ impl Zpool { region_id: Uuid, ) -> Option> { for dataset in self.datasets.values() { - let DatasetContents::Crucible(dataset) = dataset; + let DatasetContents::Crucible(dataset) = dataset else { + continue; + }; for region in &dataset.data().list() { let id = Uuid::from_str(®ion.id.0).unwrap(); if id == region_id { @@ -1204,7 +1214,9 @@ impl Zpool { let mut regions = vec![]; for dataset in self.datasets.values() { - let DatasetContents::Crucible(dataset) = dataset; + let DatasetContents::Crucible(dataset) = dataset else { + continue; + }; for region in &dataset.data().list() { if region.state == State::Destroyed { continue; @@ -1225,6 +1237,14 @@ impl Zpool { pub fn drop_dataset(&mut self, id: DatasetUuid) { let _ = self.datasets.remove(&id).expect("Failed to get the dataset"); } + + fn insert_local_storage_dataset( + &mut self, + id: DatasetUuid, + request: LocalStorageDatasetEnsureRequest, + ) { + self.datasets.insert(id, DatasetContents::LocalStorage(request)); + } } /// Represents a nested dataset @@ -1853,8 +1873,11 @@ impl StorageInner { zpool .datasets .iter() - .map(|(id, dataset)| match dataset { - DatasetContents::Crucible(server) => (*id, server.address()), + .filter_map(|(id, dataset)| match dataset { + DatasetContents::Crucible(server) => { + Some((*id, server.address())) + } + DatasetContents::LocalStorage(_) => None, }) .collect() } @@ -1883,6 +1906,9 @@ impl StorageInner { ) -> Arc { match self.get_dataset(zpool_id, dataset_id) { DatasetContents::Crucible(crucible) => crucible.data.clone(), + DatasetContents::LocalStorage(_) => { + panic!("asked for Crucible, got LocalStorage!") + } } } @@ -1923,6 +1949,34 @@ impl StorageInner { .expect("Zpool does not exist") .drop_dataset(dataset_id) } + + pub fn ensure_local_storage_dataset( + &mut self, + zpool_id: ExternalZpoolUuid, + dataset_id: DatasetUuid, + request: LocalStorageDatasetEnsureRequest, + ) { + let zpool_id = + ZpoolUuid::from_untyped_uuid(zpool_id.into_untyped_uuid()); + self.zpools + .get_mut(&zpool_id) + .expect("Zpool does not exist") + .insert_local_storage_dataset(dataset_id, request); + } + + pub fn get_local_storage_dataset( + &self, + zpool_id: ZpoolUuid, + dataset_id: DatasetUuid, + ) -> LocalStorageDatasetEnsureRequest { + match self.get_dataset(zpool_id, dataset_id) { + DatasetContents::Crucible(_) => { + panic!("asked for LocalStorage, got Crucible!") + } + + DatasetContents::LocalStorage(request) => request.clone(), + } + } } pub struct PantryVolume { diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index a17bb48d6b4..25ba3967a8f 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -32,6 +32,11 @@ use futures::stream::FuturesUnordered; use iddqd::IdHashMap; use illumos_utils::opte::PortManager; use illumos_utils::running_zone::RunningZone; +use illumos_utils::zfs::CanMount; +use illumos_utils::zfs::DatasetEnsureArgs; +use illumos_utils::zfs::Mountpoint; +use illumos_utils::zfs::SizeDetails; +use illumos_utils::zfs::Zfs; use illumos_utils::zpool::PathInPool; use itertools::Itertools as _; use nexus_sled_agent_shared::inventory::{ @@ -50,9 +55,11 @@ use omicron_common::api::internal::shared::{ use omicron_common::backoff::{ BackoffError, retry_notify, retry_policy_internal_service_aggressive, }; +use omicron_common::zpool_name::ZpoolName; use omicron_ddm_admin_client::Client as DdmAdminClient; use omicron_uuid_kinds::{ - GenericUuid, MupdateOverrideUuid, PropolisUuid, SledUuid, + DatasetUuid, ExternalZpoolUuid, GenericUuid, MupdateOverrideUuid, + PropolisUuid, SledUuid, }; use sled_agent_config_reconciler::{ ConfigReconcilerHandle, ConfigReconcilerSpawnToken, InternalDisks, @@ -1188,6 +1195,71 @@ impl SledAgent { pub(crate) fn set_probes(&self, probes: IdHashMap) { self.inner.probes.set_probes(probes); } + + pub(crate) async fn create_local_storage_dataset( + &self, + zpool_id: ExternalZpoolUuid, + dataset_id: DatasetUuid, + request: sled_agent_api::LocalStorageDatasetEnsureRequest, + ) -> Result<(), HttpError> { + let zpool_name = ZpoolName::External(zpool_id); + + let name = format!("{zpool_name}/crypt/local_storage/{dataset_id}"); + + let sled_agent_api::LocalStorageDatasetEnsureRequest { + dataset_size, + volume_size, + block_size, + } = request; + + Zfs::ensure_dataset(DatasetEnsureArgs { + name: &name, + // dataset will never be mounted but a unique value is required here + // just in case. + mountpoint: Mountpoint(format!("/{dataset_id}").into()), + can_mount: CanMount::Off, + zoned: true, + // encryption details not required, will inherit from parent + // "oxp_UUID/crypt/local_storage", which inherits from + // "oxp_UUID/crypt" + encryption_details: None, + size_details: Some(SizeDetails { + quota: Some(dataset_size), + reservation: Some(dataset_size), + compression: omicron_common::disk::CompressionAlgorithm::Off, + }), + id: None, + additional_options: None, + }) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + Zfs::ensure_dataset_volume( + format!("{}/vol", name), + volume_size, + block_size, + ) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + Ok(()) + } + + pub(crate) async fn delete_local_storage_dataset( + &self, + zpool_id: ExternalZpoolUuid, + dataset_id: DatasetUuid, + ) -> Result<(), HttpError> { + let zpool_name = ZpoolName::External(zpool_id); + + let name = format!("{zpool_name}/crypt/local_storage/{dataset_id}"); + + Zfs::destroy_dataset(&name) + .await + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + Ok(()) + } } #[derive(From, thiserror::Error, Debug, SlogInlineError)] diff --git a/sled-agent/types/src/instance.rs b/sled-agent/types/src/instance.rs index b2de8311dbb..4a382f041dc 100644 --- a/sled-agent/types/src/instance.rs +++ b/sled-agent/types/src/instance.rs @@ -14,14 +14,15 @@ use omicron_common::api::{ internal::{ nexus::{SledVmmState, VmmRuntimeState}, shared::{ - DhcpConfig, NetworkInterface, ResolvedVpcFirewallRule, - SourceNatConfig, + DelegatedZvol, DhcpConfig, NetworkInterface, + ResolvedVpcFirewallRule, SourceNatConfig, }, }, }; use omicron_uuid_kinds::InstanceUuid; use propolis_client::instance_spec::{ - ComponentV0, CrucibleStorageBackend, SpecKey, VirtioNetworkBackend, + ComponentV0, CrucibleStorageBackend, FileStorageBackend, SpecKey, + VirtioNetworkBackend, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -70,6 +71,7 @@ pub struct InstanceSledLocalConfig { pub floating_ips: Vec, pub firewall_rules: Vec, pub dhcp_config: DhcpConfig, + pub delegated_zvols: Vec, } /// Metadata used to track statistics about an instance. @@ -206,4 +208,15 @@ impl VmmSpec { }, ) } + + pub fn file_backends( + &self, + ) -> impl Iterator { + self.0.components.iter().filter_map( + |(key, component)| match component { + ComponentV0::FileStorageBackend(be) => Some((key, be)), + _ => None, + }, + ) + } } From 7b7d7c1a69924c4adf62c800820d3c82132c20c7 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Thu, 13 Nov 2025 14:36:34 +0000 Subject: [PATCH 02/16] wip not managed from non-global zone --- sled-agent/src/sled_agent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 25ba3967a8f..2737e5b81e4 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -1218,7 +1218,7 @@ impl SledAgent { // just in case. mountpoint: Mountpoint(format!("/{dataset_id}").into()), can_mount: CanMount::Off, - zoned: true, + zoned: false, // encryption details not required, will inherit from parent // "oxp_UUID/crypt/local_storage", which inherits from // "oxp_UUID/crypt" From 1ffd876599abf9ca1a841018269083c4cec26759 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Fri, 14 Nov 2025 22:48:32 +0000 Subject: [PATCH 03/16] Use an enum for DelegatedZvol, and use it! - change DelegatedZvol to an enum - create a specific variant for local storage - use functions implemented on DelegatedZvol instead of hard coded strings (those functions still have hard coded strings but it's wrapped up in the type itself at least) - use the new DelegatedZvol type and functions when ensuring and deleting local storage datasets --- common/src/api/internal/shared.rs | 79 +++++++++++++++++-- ...bd61.json => sled-agent-7.0.0-7df795.json} | 54 +++++++------ openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/src/instance.rs | 17 ++-- sled-agent/src/sled_agent.rs | 22 +++--- 5 files changed, 126 insertions(+), 48 deletions(-) rename openapi/sled-agent/{sled-agent-7.0.0-b2bd61.json => sled-agent-7.0.0-7df795.json} (99%) diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index e1d1b4d4b4c..439ec469e27 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -7,8 +7,12 @@ use crate::{ address::NUM_SOURCE_NAT_PORTS, api::external::{self, BfdMode, ImportExportPolicy, Name, Vni}, + disk::DatasetName, + zpool_name::ZpoolName, }; use daft::Diffable; +use omicron_uuid_kinds::DatasetUuid; +use omicron_uuid_kinds::ExternalZpoolUuid; use oxnet::{IpNet, Ipv4Net, Ipv6Net}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; @@ -1111,12 +1115,58 @@ pub struct SledIdentifiers { /// Delegate a ZFS volume to a zone #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct DelegatedZvol { - /// The fully qualified name of the parent dataset - pub parent_dataset: String, +#[serde(tag = "type", rename_all = "snake_case")] +pub enum DelegatedZvol { + /// Delegate a slice of the local storage dataset present on this pool into + /// the zone. + LocalStorage { zpool_id: ExternalZpoolUuid, dataset_id: DatasetUuid }, +} + +impl DelegatedZvol { + /// Return the fully qualified dataset name that the volume is in. + pub fn parent_dataset_name(&self) -> String { + match &self { + DelegatedZvol::LocalStorage { zpool_id, dataset_id } => { + // The local storage dataset is the parent for an allocation + let local_storage_parent = DatasetName::new( + ZpoolName::External(*zpool_id), + DatasetKind::LocalStorage, + ); + + format!("{}/{}", local_storage_parent.full_name(), dataset_id) + } + } + } + + /// Return the mountpoint for the parent dataset in the zone + pub fn parent_dataset_mountpoint(&self) -> String { + match &self { + DelegatedZvol::LocalStorage { dataset_id, .. } => { + format!("/{}", dataset_id) + } + } + } + + /// Return the fully qualified volume name + pub fn volume_name(&self) -> String { + match &self { + DelegatedZvol::LocalStorage { .. } => { + // For now, all local storage zvols use the same name + format!("{}/vol", self.parent_dataset_name()) + } + } + } - /// The volume name - pub name: String, + /// Return the device that should be delegated into the zone + pub fn zvol_device(&self) -> String { + match &self { + DelegatedZvol::LocalStorage { .. } => { + // Use the `rdsk` device to avoid interacting with an additional + // buffer cache that would be used if we used `dsk`. + format!("/dev/zvol/rdsk/{}", self.volume_name()) + } + } + } } #[cfg(test)] @@ -1212,4 +1262,23 @@ mod tests { ); } } + + #[test] + fn test_delegated_zvol_device_name() { + let delegated_zvol = DelegatedZvol::LocalStorage { + zpool_id: "cb832c2e-fa94-4911-89a9-895ac8b1e8f3".parse().unwrap(), + dataset_id: "2bbf0908-21da-4bc3-882b-1a1e715c54bd".parse().unwrap(), + }; + + assert_eq!( + delegated_zvol.zvol_device(), + [ + String::from("/dev/zvol/rdsk"), + String::from("oxp_cb832c2e-fa94-4911-89a9-895ac8b1e8f3/crypt"), + String::from("local_storage"), + String::from("2bbf0908-21da-4bc3-882b-1a1e715c54bd/vol"), + ] + .join("/"), + ); + } } diff --git a/openapi/sled-agent/sled-agent-7.0.0-b2bd61.json b/openapi/sled-agent/sled-agent-7.0.0-7df795.json similarity index 99% rename from openapi/sled-agent/sled-agent-7.0.0-b2bd61.json rename to openapi/sled-agent/sled-agent-7.0.0-7df795.json index 459f2c27a22..19efe7c7a50 100644 --- a/openapi/sled-agent/sled-agent-7.0.0-b2bd61.json +++ b/openapi/sled-agent/sled-agent-7.0.0-7df795.json @@ -4006,20 +4006,30 @@ }, "DelegatedZvol": { "description": "Delegate a ZFS volume to a zone", - "type": "object", - "properties": { - "name": { - "description": "The volume name", - "type": "string" - }, - "parent_dataset": { - "description": "The fully qualified name of the parent dataset", - "type": "string" + "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" + ] } - }, - "required": [ - "name", - "parent_dataset" ] }, "DhcpConfig": { @@ -4578,6 +4588,15 @@ "mappings" ] }, + "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", @@ -8693,15 +8712,6 @@ "type": "string", "format": "uuid" }, - "ExternalZpoolUuid": { - "x-rust-type": { - "crate": "omicron-uuid-kinds", - "path": "omicron_uuid_kinds::ExternalZpoolUuid", - "version": "*" - }, - "type": "string", - "format": "uuid" - }, "PropolisUuid": { "x-rust-type": { "crate": "omicron-uuid-kinds", diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index f80336b12d0..7ae97813497 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-7.0.0-b2bd61.json \ No newline at end of file +sled-agent-7.0.0-7df795.json \ No newline at end of file diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index 1df45ea3fcc..eefcb361a26 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -1992,12 +1992,16 @@ impl InstanceRunner { opte_ports.push(port); } - // Each delegated Zvol requires delegating the parent dataset + // Each delegated Zvol requires (optionally) delegating the parent dataset let datasets: Vec<_> = self .delegated_zvols .iter() - .map(|delegated_zvol| zone::Dataset { - name: delegated_zvol.parent_dataset.clone(), + .filter_map(|delegated_zvol| match delegated_zvol { + DelegatedZvol::LocalStorage { .. } => { + // Delegating the rdsk device does _not_ require delegating + // the parent dataset. + None + } }) .collect(); @@ -2010,12 +2014,7 @@ impl InstanceRunner { ]; for delegated_zvol in &self.delegated_zvols { - devices.push(zone::Device { - name: format!( - "/dev/zvol/rdsk/{}/{}", - delegated_zvol.parent_dataset, delegated_zvol.name, - ), - }); + devices.push(zone::Device { name: delegated_zvol.zvol_device() }); } // Create a zone for the propolis instance, using the previously diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 2737e5b81e4..d308cd074db 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -55,7 +55,6 @@ use omicron_common::api::internal::shared::{ use omicron_common::backoff::{ BackoffError, retry_notify, retry_policy_internal_service_aggressive, }; -use omicron_common::zpool_name::ZpoolName; use omicron_ddm_admin_client::Client as DdmAdminClient; use omicron_uuid_kinds::{ DatasetUuid, ExternalZpoolUuid, GenericUuid, MupdateOverrideUuid, @@ -66,6 +65,7 @@ use sled_agent_config_reconciler::{ InternalDisksReceiver, LedgerNewConfigError, LedgerTaskError, ReconcilerInventory, SledAgentArtifactStore, SledAgentFacilities, }; +use omicron_common::api::internal::shared::DelegatedZvol; use sled_agent_types::disk::DiskStateRequested; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::instance::{ @@ -1202,9 +1202,8 @@ impl SledAgent { dataset_id: DatasetUuid, request: sled_agent_api::LocalStorageDatasetEnsureRequest, ) -> Result<(), HttpError> { - let zpool_name = ZpoolName::External(zpool_id); - - let name = format!("{zpool_name}/crypt/local_storage/{dataset_id}"); + let delegated_zvol = + DelegatedZvol::LocalStorage { zpool_id, dataset_id }; let sled_agent_api::LocalStorageDatasetEnsureRequest { dataset_size, @@ -1213,10 +1212,12 @@ impl SledAgent { } = request; Zfs::ensure_dataset(DatasetEnsureArgs { - name: &name, + name: &delegated_zvol.parent_dataset_name(), // dataset will never be mounted but a unique value is required here // just in case. - mountpoint: Mountpoint(format!("/{dataset_id}").into()), + mountpoint: Mountpoint( + delegated_zvol.parent_dataset_mountpoint().into(), + ), can_mount: CanMount::Off, zoned: false, // encryption details not required, will inherit from parent @@ -1235,7 +1236,7 @@ impl SledAgent { .map_err(|e| HttpError::for_internal_error(e.to_string()))?; Zfs::ensure_dataset_volume( - format!("{}/vol", name), + delegated_zvol.volume_name(), volume_size, block_size, ) @@ -1250,11 +1251,10 @@ impl SledAgent { zpool_id: ExternalZpoolUuid, dataset_id: DatasetUuid, ) -> Result<(), HttpError> { - let zpool_name = ZpoolName::External(zpool_id); - - let name = format!("{zpool_name}/crypt/local_storage/{dataset_id}"); + let delegated_zvol = + DelegatedZvol::LocalStorage { zpool_id, dataset_id }; - Zfs::destroy_dataset(&name) + Zfs::destroy_dataset(&delegated_zvol.parent_dataset_name()) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; From f340e58dc70d76084db259b89f3fdc22c93e3060 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 16:38:21 +0000 Subject: [PATCH 04/16] not transient, not ephemeral either! --- common/src/api/internal/shared.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index 439ec469e27..fe282b03afc 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -948,7 +948,7 @@ pub enum DatasetKind { // Other datasets Debug, - /// Used for transient storage, contains volumes delegated to VMMs + /// Used for local storage disk types, contains volumes delegated to VMMs LocalStorage, } From 634ee2ad6a4a1c2f0e1f33991e3e68f6eb6cfc2b Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 16:38:38 +0000 Subject: [PATCH 05/16] ensure that the local storage dataset we want to use is still present --- sled-agent/config-reconciler/src/handle.rs | 27 +++++++++++ .../config-reconciler/src/reconciler_task.rs | 26 ++++++---- sled-agent/src/sled_agent.rs | 48 ++++++++++++++++++- 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/sled-agent/config-reconciler/src/handle.rs b/sled-agent/config-reconciler/src/handle.rs index c98e8d15041..b0832341b3d 100644 --- a/sled-agent/config-reconciler/src/handle.rs +++ b/sled-agent/config-reconciler/src/handle.rs @@ -31,6 +31,8 @@ use illumos_utils::zpool::ZpoolName; #[cfg(feature = "testing")] use illumos_utils::zpool::ZpoolOrRamdisk; #[cfg(feature = "testing")] +use omicron_common::api::internal::shared::DatasetKind; +#[cfg(feature = "testing")] use sled_storage::dataset::U2_DEBUG_DATASET; #[cfg(feature = "testing")] use sled_storage::dataset::ZONE_DATASET; @@ -500,6 +502,31 @@ impl AvailableDatasetsReceiver { .collect(), } } + + pub fn all_mounted_local_storage_datasets(&self) -> Vec { + match &self.inner { + AvailableDatasetsReceiverInner::Real(receiver) => { + receiver.borrow().all_mounted_local_storage_datasets().collect() + } + #[cfg(feature = "testing")] + AvailableDatasetsReceiverInner::FakeTempDir { zpool, tempdir } => { + vec![PathInPool { + pool: zpool.clone(), + path: tempdir + .path() + .join(DatasetKind::LocalStorage.to_string()), + }] + } + #[cfg(feature = "testing")] + AvailableDatasetsReceiverInner::FakeStatic(pools) => pools + .iter() + .map(|(pool, path)| PathInPool { + pool: ZpoolOrRamdisk::Zpool(*pool), + path: path.join(DatasetKind::LocalStorage.to_string()), + }) + .collect(), + } + } } #[derive(Debug, Clone)] diff --git a/sled-agent/config-reconciler/src/reconciler_task.rs b/sled-agent/config-reconciler/src/reconciler_task.rs index ce314b5fa61..1e3bade39ba 100644 --- a/sled-agent/config-reconciler/src/reconciler_task.rs +++ b/sled-agent/config-reconciler/src/reconciler_task.rs @@ -128,28 +128,28 @@ impl ReconcilerResult { .unwrap_or(TimeSyncStatus::NotYetChecked) } - pub(crate) fn all_mounted_debug_datasets( + pub(crate) fn all_mounted_datasets_of_kind( &self, + kind: DatasetKind, ) -> impl Iterator + '_ { let Some(latest_result) = &self.latest_result else { return Either::Left(std::iter::empty()); }; Either::Right( - latest_result - .all_mounted_datasets(&self.mount_config, DatasetKind::Debug), + latest_result.all_mounted_datasets(&self.mount_config, kind), ) } + pub(crate) fn all_mounted_debug_datasets( + &self, + ) -> impl Iterator + '_ { + self.all_mounted_datasets_of_kind(DatasetKind::Debug) + } + pub(crate) fn all_mounted_zone_root_datasets( &self, ) -> impl Iterator + '_ { - let Some(latest_result) = &self.latest_result else { - return Either::Left(std::iter::empty()); - }; - Either::Right(latest_result.all_mounted_datasets( - &self.mount_config, - DatasetKind::TransientZoneRoot, - )) + self.all_mounted_datasets_of_kind(DatasetKind::TransientZoneRoot) } pub(crate) fn to_inventory( @@ -161,6 +161,12 @@ impl ReconcilerResult { self.latest_result.as_ref().map(|r| r.to_inventory()); (status, latest_result) } + + pub(crate) fn all_mounted_local_storage_datasets( + &self, + ) -> impl Iterator + '_ { + self.all_mounted_datasets_of_kind(DatasetKind::LocalStorage) + } } #[derive(Debug, Clone)] diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index d308cd074db..010995cdc45 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -38,6 +38,7 @@ use illumos_utils::zfs::Mountpoint; use illumos_utils::zfs::SizeDetails; use illumos_utils::zfs::Zfs; use illumos_utils::zpool::PathInPool; +use illumos_utils::zpool::ZpoolOrRamdisk; use itertools::Itertools as _; use nexus_sled_agent_shared::inventory::{ Inventory, OmicronSledConfig, SledRole, @@ -47,6 +48,7 @@ use omicron_common::address::{ }; use omicron_common::api::external::{ByteCount, ByteCountRangeError, Vni}; use omicron_common::api::internal::nexus::{DiskRuntimeState, SledVmmState}; +use omicron_common::api::internal::shared::DelegatedZvol; use omicron_common::api::internal::shared::{ ExternalIpGatewayMap, HostPortConfig, RackNetworkConfig, ResolvedVpcFirewallRule, ResolvedVpcRouteSet, ResolvedVpcRouteState, @@ -55,6 +57,7 @@ use omicron_common::api::internal::shared::{ use omicron_common::backoff::{ BackoffError, retry_notify, retry_policy_internal_service_aggressive, }; +use omicron_common::zpool_name::ZpoolName; use omicron_ddm_admin_client::Client as DdmAdminClient; use omicron_uuid_kinds::{ DatasetUuid, ExternalZpoolUuid, GenericUuid, MupdateOverrideUuid, @@ -65,7 +68,6 @@ use sled_agent_config_reconciler::{ InternalDisksReceiver, LedgerNewConfigError, LedgerTaskError, ReconcilerInventory, SledAgentArtifactStore, SledAgentFacilities, }; -use omicron_common::api::internal::shared::DelegatedZvol; use sled_agent_types::disk::DiskStateRequested; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::instance::{ @@ -1202,6 +1204,28 @@ impl SledAgent { dataset_id: DatasetUuid, request: sled_agent_api::LocalStorageDatasetEnsureRequest, ) -> Result<(), HttpError> { + // Ensure that the local storage dataset we want to use is still present + let present = self + .inner + .config_reconciler + .available_datasets_rx() + .all_mounted_local_storage_datasets() + .into_iter() + .any(|path_in_pool| match path_in_pool.pool { + ZpoolOrRamdisk::Zpool(zpool_name) => { + zpool_name == ZpoolName::External(zpool_id) + } + ZpoolOrRamdisk::Ramdisk => false, + }); + + if !present { + // We cannot create a child dataset of the local storage dataset if + // it's not present! Return a 503. + let error = + format!("local storage dataset for pool {zpool_id} missing!"); + return Err(HttpError::for_unavail(Some(error.clone()), error)); + } + let delegated_zvol = DelegatedZvol::LocalStorage { zpool_id, dataset_id }; @@ -1251,6 +1275,28 @@ impl SledAgent { zpool_id: ExternalZpoolUuid, dataset_id: DatasetUuid, ) -> Result<(), HttpError> { + // Ensure that the local storage dataset we want to use is still present + let present = self + .inner + .config_reconciler + .available_datasets_rx() + .all_mounted_local_storage_datasets() + .into_iter() + .any(|path_in_pool| match path_in_pool.pool { + ZpoolOrRamdisk::Zpool(zpool_name) => { + zpool_name == ZpoolName::External(zpool_id) + } + ZpoolOrRamdisk::Ramdisk => false, + }); + + if !present { + // We cannot destroy a child dataset of the local storage dataset if + // it's not present! Return a 503. + let error = + format!("local storage dataset for pool {zpool_id} missing!"); + return Err(HttpError::for_unavail(Some(error.clone()), error)); + } + let delegated_zvol = DelegatedZvol::LocalStorage { zpool_id, dataset_id }; From f7c53ab164acd37f6d2a319e21a5338611037867 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 16:41:59 +0000 Subject: [PATCH 06/16] note which backend does not match --- sled-agent/src/sim/sled_agent.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index 5c4189308f8..f5e1477c171 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -223,7 +223,7 @@ impl SledAgent { }; // Make sure each file backend was ensured before changing any state - for (_id, disk) in vmm_spec.file_backends() { + for (id, disk) in vmm_spec.file_backends() { let FileStorageBackend { block_size, path, .. } = disk; // The FileStorageBackend path will be the full device path, so @@ -245,9 +245,9 @@ impl SledAgent { // Treat a mismatch here as an error if request.block_size != *block_size { return Err(Error::internal_error(&format!( - "request block_size {} does not match FileStorageBackend \ - block_size {block_size}", - request.block_size, + "request {} block_size {} does not match \ + FileStorageBackend block_size {block_size}", + id, request.block_size, ))); } } From 78468e6ddf5f40b400d7ef65f9cd6610a5a8d4a6 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 16:42:48 +0000 Subject: [PATCH 07/16] panic -> unreachable --- sled-agent/src/sim/storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sled-agent/src/sim/storage.rs b/sled-agent/src/sim/storage.rs index 63414cb7309..c53ff759416 100644 --- a/sled-agent/src/sim/storage.rs +++ b/sled-agent/src/sim/storage.rs @@ -1181,7 +1181,7 @@ impl Zpool { .get(&id) .expect("Failed to get the dataset we just inserted") else { - panic!("just inserted this variant!"); + unreachable!("just inserted this variant!"); }; crucible From 8724549ff5d38b3980b9d02fffbaaef00e24383e Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 17:45:14 +0000 Subject: [PATCH 08/16] validate volume size and block_size in ensure --- illumos-utils/src/zfs.rs | 114 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index bfc2aad7ebe..9d8cac5d3bd 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -220,13 +220,74 @@ pub struct DestroySnapshotError { err: crate::ExecutionError, } +#[derive(thiserror::Error, Debug)] +pub enum EnsureDatasetVolumeErrorInner { + #[error("{0}")] + Execution(#[from] crate::ExecutionError), + + #[error("{0}")] + GetValue(#[from] GetValueError), + + #[error("value {value_name} parse error: {value} not a number!")] + ValueParseError { value_name: String, value: String }, + + #[error("expected {value_name} to be {expected}, but saw {actual}")] + ValueMismatch { value_name: String, expected: u64, actual: u64 }, +} + /// Error returned by [`Zfs::ensure_dataset_volume`]. #[derive(thiserror::Error, Debug)] #[error("Failed to ensure volume '{name}': {err}")] pub struct EnsureDatasetVolumeError { name: String, #[source] - err: crate::ExecutionError, + err: EnsureDatasetVolumeErrorInner, +} + +impl EnsureDatasetVolumeError { + pub fn execution(name: String, err: crate::ExecutionError) -> Self { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::Execution(err), + } + } + + pub fn get_value(name: String, err: GetValueError) -> Self { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::GetValue(err), + } + } + + pub fn value_parse( + name: String, + value_name: String, + value: String, + ) -> Self { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::ValueParseError { + value_name, + value, + }, + } + } + + pub fn value_mismatch( + name: String, + value_name: String, + expected: u64, + actual: u64, + ) -> Self { + EnsureDatasetVolumeError { + name, + err: EnsureDatasetVolumeErrorInner::ValueMismatch { + value_name, + expected, + actual, + }, + } + } } /// Wraps commands for interacting with ZFS. @@ -1375,10 +1436,59 @@ impl Zfs { Err(crate::ExecutionError::CommandFailure(info)) if info.stderr.contains("dataset already exists") => { + // Validate that the total size and volblocksize are what is + // being requested: these cannot be changed once the volume is + // created. + + let [actual_size, actual_block_size] = + Self::get_values(&name, &["volsize", "volblocksize"], None) + .await + .map_err(|err| { + EnsureDatasetVolumeError::get_value( + name.clone(), + err, + ) + })?; + + let actual_size: u64 = actual_size.parse().map_err(|_| { + EnsureDatasetVolumeError::value_parse( + name.clone(), + String::from("volsize"), + actual_size, + ) + })?; + + let actual_block_size: u32 = + actual_block_size.parse().map_err(|_| { + EnsureDatasetVolumeError::value_parse( + name.clone(), + String::from("volblocksize"), + actual_block_size, + ) + })?; + + if actual_size != size.to_bytes() { + return Err(EnsureDatasetVolumeError::value_mismatch( + name.clone(), + String::from("volsize"), + size.to_bytes(), + actual_size, + )); + } + + if actual_block_size != block_size { + return Err(EnsureDatasetVolumeError::value_mismatch( + name.clone(), + String::from("volblocksize"), + u64::from(block_size), + u64::from(actual_block_size), + )); + } + Ok(()) } - Err(err) => Err(EnsureDatasetVolumeError { name, err }), + Err(err) => Err(EnsureDatasetVolumeError::execution(name, err)), } } } From 79c0bf3f40f7d07abaab6c7d8d10bb38b455e6cd Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 18:03:45 +0000 Subject: [PATCH 09/16] no more block size in LocalStorageDatasetEnsureRequest --- common/src/api/internal/shared.rs | 9 +++++++++ ...df795.json => sled-agent-7.0.0-a1497f.json} | 7 ------- openapi/sled-agent/sled-agent-latest.json | 2 +- sled-agent/api/src/lib.rs | 3 --- sled-agent/src/sim/sled_agent.rs | 18 +++--------------- sled-agent/src/sled_agent.rs | 3 +-- 6 files changed, 14 insertions(+), 28 deletions(-) rename openapi/sled-agent/{sled-agent-7.0.0-7df795.json => sled-agent-7.0.0-a1497f.json} (99%) diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index fe282b03afc..7363db77760 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -1167,6 +1167,15 @@ impl DelegatedZvol { } } } + + pub fn volblocksize(&self) -> u32 { + match &self { + DelegatedZvol::LocalStorage { .. } => { + // all Local storage zvols use 4096 byte blocks + 4096 + } + } + } } #[cfg(test)] diff --git a/openapi/sled-agent/sled-agent-7.0.0-7df795.json b/openapi/sled-agent/sled-agent-7.0.0-a1497f.json similarity index 99% rename from openapi/sled-agent/sled-agent-7.0.0-7df795.json rename to openapi/sled-agent/sled-agent-7.0.0-a1497f.json index 19efe7c7a50..ffbf38d4d5b 100644 --- a/openapi/sled-agent/sled-agent-7.0.0-7df795.json +++ b/openapi/sled-agent/sled-agent-7.0.0-a1497f.json @@ -5556,12 +5556,6 @@ "description": "Dataset and Volume details for a Local Storage dataset ensure request", "type": "object", "properties": { - "block_size": { - "description": "Block size for the zvol", - "type": "integer", - "format": "uint32", - "minimum": 0 - }, "dataset_size": { "description": "Size of the parent dataset", "allOf": [ @@ -5580,7 +5574,6 @@ } }, "required": [ - "block_size", "dataset_size", "volume_size" ] diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 7ae97813497..a5a919d4513 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-7.0.0-7df795.json \ No newline at end of file +sled-agent-7.0.0-a1497f.json \ No newline at end of file diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 1f64b4bad0f..d8c2f223eea 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -1111,7 +1111,4 @@ pub struct LocalStorageDatasetEnsureRequest { /// Size of the zvol pub volume_size: ByteCount, - - /// Block size for the zvol - pub block_size: u32, } diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index f5e1477c171..5267957f44d 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -223,8 +223,8 @@ impl SledAgent { }; // Make sure each file backend was ensured before changing any state - for (id, disk) in vmm_spec.file_backends() { - let FileStorageBackend { block_size, path, .. } = disk; + for (_id, disk) in vmm_spec.file_backends() { + let FileStorageBackend { path, .. } = disk; // The FileStorageBackend path will be the full device path, so // strip the beginning, including the first part of the external @@ -237,19 +237,7 @@ impl SledAgent { let dataset_id: DatasetUuid = parts[3].parse().unwrap(); // This panics if this dataset was not already created - let request = self - .storage - .lock() - .get_local_storage_dataset(zpool_id, dataset_id); - - // Treat a mismatch here as an error - if request.block_size != *block_size { - return Err(Error::internal_error(&format!( - "request {} block_size {} does not match \ - FileStorageBackend block_size {block_size}", - id, request.block_size, - ))); - } + self.storage.lock().get_local_storage_dataset(zpool_id, dataset_id); } for (id, _disk) in vmm_spec.crucible_backends() { diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 010995cdc45..6045fe569ca 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -1232,7 +1232,6 @@ impl SledAgent { let sled_agent_api::LocalStorageDatasetEnsureRequest { dataset_size, volume_size, - block_size, } = request; Zfs::ensure_dataset(DatasetEnsureArgs { @@ -1262,7 +1261,7 @@ impl SledAgent { Zfs::ensure_dataset_volume( delegated_zvol.volume_name(), volume_size, - block_size, + delegated_zvol.volblocksize(), ) .await .map_err(|e| HttpError::for_internal_error(e.to_string()))?; From 551a5280c4ff8bf25cbe17646fc20866b082fffa Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Mon, 17 Nov 2025 18:18:56 +0000 Subject: [PATCH 10/16] use entry -> insert_entry -> into_mut instead --- sled-agent/src/sim/storage.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/sled-agent/src/sim/storage.rs b/sled-agent/src/sim/storage.rs index c53ff759416..21744208510 100644 --- a/sled-agent/src/sim/storage.rs +++ b/sled-agent/src/sim/storage.rs @@ -1166,20 +1166,16 @@ impl Zpool { start_port: u16, end_port: u16, ) -> &CrucibleServer { - self.datasets.insert( - id, - DatasetContents::Crucible(CrucibleServer::new( + let DatasetContents::Crucible(crucible) = self + .datasets + .entry(id) + .insert_entry(DatasetContents::Crucible(CrucibleServer::new( log, crucible_ip, start_port, end_port, - )), - ); - - let DatasetContents::Crucible(crucible) = self - .datasets - .get(&id) - .expect("Failed to get the dataset we just inserted") + ))) + .into_mut() else { unreachable!("just inserted this variant!"); }; From c6110fcf16507483a778f3c9cec3cac62a365c6c Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 01:27:05 +0000 Subject: [PATCH 11/16] no double speak! --- illumos-utils/src/zfs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index 9d8cac5d3bd..c23fe8b5632 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -222,10 +222,10 @@ pub struct DestroySnapshotError { #[derive(thiserror::Error, Debug)] pub enum EnsureDatasetVolumeErrorInner { - #[error("{0}")] + #[error(transparent)] Execution(#[from] crate::ExecutionError), - #[error("{0}")] + #[error(transparent)] GetValue(#[from] GetValueError), #[error("value {value_name} parse error: {value} not a number!")] From bf71ddbeebce56e641672f0f8f103637bca53a9b Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 01:55:24 +0000 Subject: [PATCH 12/16] don't waste time computing empty vec --- sled-agent/src/instance.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index eefcb361a26..b2586c7e99e 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -1992,18 +1992,11 @@ impl InstanceRunner { opte_ports.push(port); } - // Each delegated Zvol requires (optionally) delegating the parent dataset - let datasets: Vec<_> = self - .delegated_zvols - .iter() - .filter_map(|delegated_zvol| match delegated_zvol { - DelegatedZvol::LocalStorage { .. } => { - // Delegating the rdsk device does _not_ require delegating - // the parent dataset. - None - } - }) - .collect(); + // When delegatng a zvol, delegating the associated rdsk device does + // _not_ require delegating the parent dataset. Future types of + // delegated zvol may want to delegate either the parent dataset or + // some other one. + let datasets: Vec = vec![]; // For delegated devices, include the default list plus any for the // delegated zvol devices. From 7e3d4843b4c2281bc0413248664023c80647f0ab Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 02:58:30 +0000 Subject: [PATCH 13/16] all variants explicitly --- sled-agent/config-reconciler/src/reconciler_task.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sled-agent/config-reconciler/src/reconciler_task.rs b/sled-agent/config-reconciler/src/reconciler_task.rs index 1e3bade39ba..dea66fd7b79 100644 --- a/sled-agent/config-reconciler/src/reconciler_task.rs +++ b/sled-agent/config-reconciler/src/reconciler_task.rs @@ -24,6 +24,7 @@ use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use sled_storage::config::MountConfig; +use sled_storage::dataset::LOCAL_STORAGE_DATASET; use sled_storage::dataset::U2_DEBUG_DATASET; use sled_storage::dataset::ZONE_DATASET; use sled_storage::disk::Disk; @@ -247,8 +248,17 @@ impl LatestReconciliationResult { // handle the specific `DatasetKind`s used by our callers. let mountpoint = match &kind { DatasetKind::Debug => U2_DEBUG_DATASET, + DatasetKind::LocalStorage => LOCAL_STORAGE_DATASET, DatasetKind::TransientZoneRoot => ZONE_DATASET, - _ => unreachable!( + + DatasetKind::Clickhouse | + DatasetKind::ClickhouseKeeper | + DatasetKind::ClickhouseServer | + DatasetKind::Cockroach | + DatasetKind::Crucible | + DatasetKind::ExternalDns | + DatasetKind::InternalDns | + DatasetKind::TransientZone { .. } => unreachable!( "private function called with unexpected kind {kind:?}" ), }; From b9855344050a49bfbbfaa9e2f351a6d2bfe9efea Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 15:10:10 +0000 Subject: [PATCH 14/16] fmt --- .../config-reconciler/src/reconciler_task.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sled-agent/config-reconciler/src/reconciler_task.rs b/sled-agent/config-reconciler/src/reconciler_task.rs index dea66fd7b79..f337e93b22f 100644 --- a/sled-agent/config-reconciler/src/reconciler_task.rs +++ b/sled-agent/config-reconciler/src/reconciler_task.rs @@ -251,14 +251,14 @@ impl LatestReconciliationResult { DatasetKind::LocalStorage => LOCAL_STORAGE_DATASET, DatasetKind::TransientZoneRoot => ZONE_DATASET, - DatasetKind::Clickhouse | - DatasetKind::ClickhouseKeeper | - DatasetKind::ClickhouseServer | - DatasetKind::Cockroach | - DatasetKind::Crucible | - DatasetKind::ExternalDns | - DatasetKind::InternalDns | - DatasetKind::TransientZone { .. } => unreachable!( + DatasetKind::Clickhouse + | DatasetKind::ClickhouseKeeper + | DatasetKind::ClickhouseServer + | DatasetKind::Cockroach + | DatasetKind::Crucible + | DatasetKind::ExternalDns + | DatasetKind::InternalDns + | DatasetKind::TransientZone { .. } => unreachable!( "private function called with unexpected kind {kind:?}" ), }; From c85bf91c7ddf17bced3dd7b291b6af5130343fe9 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 17:49:59 +0000 Subject: [PATCH 15/16] fmt --- sled-agent/api/src/v8.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sled-agent/api/src/v8.rs b/sled-agent/api/src/v8.rs index 2c015bc0210..30ddf00b31c 100644 --- a/sled-agent/api/src/v8.rs +++ b/sled-agent/api/src/v8.rs @@ -15,13 +15,12 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use sled_agent_types::instance::InstanceMetadata; -use sled_agent_types::instance::VmmSpec; use sled_agent_types::instance::InstanceMulticastMembership; +use sled_agent_types::instance::VmmSpec; use std::net::IpAddr; use std::net::SocketAddr; use uuid::Uuid; - /// The body of a request to ensure that a instance and VMM are known to a sled /// agent. #[derive(Serialize, Deserialize, JsonSchema)] From 504581ba44d2e073e8f167ecd471977a1ba2d74f Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 18 Nov 2025 20:53:55 +0000 Subject: [PATCH 16/16] restore old 7.0.0. --- ...d-agent-7.0.0-7fa308.json => sled-agent-7.0.0-62acb3.json} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename openapi/sled-agent/{sled-agent-7.0.0-7fa308.json => sled-agent-7.0.0-62acb3.json} (99%) diff --git a/openapi/sled-agent/sled-agent-7.0.0-7fa308.json b/openapi/sled-agent/sled-agent-7.0.0-62acb3.json similarity index 99% rename from openapi/sled-agent/sled-agent-7.0.0-7fa308.json rename to openapi/sled-agent/sled-agent-7.0.0-62acb3.json index 276e78b908d..ddb7f61f616 100644 --- a/openapi/sled-agent/sled-agent-7.0.0-7fa308.json +++ b/openapi/sled-agent/sled-agent-7.0.0-62acb3.json @@ -4863,7 +4863,7 @@ ] }, "InstanceEnsureBody": { - "description": "The body of a request to ensure that a instance and VMM are known to a sled agent.", + "description": "The body of a request to ensure that a instance and VMM are known to a sled agent (version 7, with multicast support).", "type": "object", "properties": { "instance_id": { @@ -5051,7 +5051,7 @@ ] }, "InstanceSledLocalConfig": { - "description": "Describes sled-local configuration that a sled-agent must establish to make the instance's virtual hardware fully functional.", + "description": "Describes sled-local configuration that a sled-agent must establish to make the instance's virtual hardware fully functional (version 7, with multicast).", "type": "object", "properties": { "dhcp_config": {