diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 8831d8c630b..f11917b90a1 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -643,6 +643,68 @@ impl From for i64 { } } +/// Size of blocks for a disk. Valid values are: 512, 2048, or 4096. +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(try_from = "u32")] +pub struct BlockSize(pub u32); + +impl schemars::JsonSchema for BlockSize { + fn schema_name() -> String { + "BlockSize".to_string() + } + + fn json_schema( + _: &mut schemars::r#gen::SchemaGenerator, + ) -> schemars::schema::Schema { + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + metadata: Some(Box::new(schemars::schema::Metadata { + id: None, + description: Some( + "Size of blocks for a disk. Valid values are: 512, 2048, or 4096.".to_string(), + ), + title: Some("Disk block size in bytes".to_string()), + ..Default::default() + })), + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + enum_values: Some(vec![ + serde_json::json!(512), + serde_json::json!(2048), + serde_json::json!(4096), + ]), + ..Default::default() + }) + } +} + +impl TryFrom for BlockSize { + type Error = anyhow::Error; + fn try_from(x: u32) -> Result { + if ![512, 2048, 4096].contains(&x) { + anyhow::bail!("invalid block size {}", x); + } + + Ok(BlockSize(x)) + } +} + +impl From for BlockSize { + fn from(bc: u64) -> BlockSize { + BlockSize(bc as u32) + } +} + +impl From for ByteCount { + fn from(bs: BlockSize) -> ByteCount { + ByteCount::from(bs.0) + } +} + +impl From for u64 { + fn from(bs: BlockSize) -> u64 { + u64::from(bs.0) + } +} + /// Generation numbers stored in the database, used for optimistic concurrency /// control // @@ -1435,7 +1497,7 @@ pub struct Disk { /// ID of image from which disk was created, if any pub image_id: Option, pub size: ByteCount, - pub block_size: ByteCount, + pub block_size: BlockSize, pub state: DiskState, pub device_path: String, pub disk_type: DiskType, diff --git a/nexus/db-queries/src/db/datastore/disk.rs b/nexus/db-queries/src/db/datastore/disk.rs index 201b4aee241..0b3ad6ed102 100644 --- a/nexus/db-queries/src/db/datastore/disk.rs +++ b/nexus/db-queries/src/db/datastore/disk.rs @@ -367,7 +367,7 @@ impl Into for Disk { snapshot_id: disk_type_crucible.create_snapshot_id, image_id: disk_type_crucible.create_image_id, size: disk.size.into(), - block_size: disk.block_size.into(), + block_size: disk.block_size.to_bytes().into(), state: disk.state().into(), device_path, disk_type: api::external::DiskType::Distributed, @@ -388,7 +388,7 @@ impl Into for Disk { snapshot_id: None, image_id: None, size: disk.size.into(), - block_size: disk.block_size.into(), + block_size: disk.block_size.to_bytes().into(), state: disk.state().into(), device_path, disk_type: api::external::DiskType::Local, diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index fdf511da6f8..0b062437958 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -53,6 +53,7 @@ mod v2025_11_20_00_local; mod v2026_01_01_00_local; mod v2026_01_30_00_local; mod v2026_03_24_00_local; +mod v2026_05_20_00_local; api_versions!([ // API versions are in the format YYYY_MM_DD_NN.0.0, defined below as @@ -83,6 +84,7 @@ api_versions!([ // | date-based version should be at the top of the list. // v // (next_yyyy_mm_dd_nn, IDENT), + (2026_05_21_00, DISK_BLOCK_SIZE_TYPE), (2026_05_20_00, ADD_CONTACT_SUPPORT_TO_UPDATE_STATUS), (2026_05_08_00, MANUAL_DISK_ADOPTION), (2026_05_07_00, REMOVE_DUPLICATED_NETWORKING_TYPES), @@ -3304,7 +3306,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/disks", tags = ["disks"], - versions = VERSION_READ_ONLY_DISKS.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn disk_list( rqctx: RequestContext, @@ -3313,6 +3315,32 @@ pub trait NexusExternalApi { >, ) -> Result>, HttpError>; + /// List disks + #[endpoint { + operation_id = "disk_list", + method = GET, + path = "/v1/disks", + tags = ["disks"], + versions = VERSION_READ_ONLY_DISKS..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn disk_list_v2026_01_30_01( + rqctx: RequestContext, + query_params: Query< + PaginatedByNameOrId, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + Self::disk_list(rqctx, query_params).await.map( + |HttpResponseOk(page)| { + let items: Vec<_> = + page.items.into_iter().map(Into::into).collect(); + HttpResponseOk(ResultsPage { next_page: page.next_page, items }) + }, + ) + } + /// List disks #[endpoint { operation_id = "disk_list", @@ -3376,7 +3404,7 @@ pub trait NexusExternalApi { method = POST, path = "/v1/disks", tags = ["disks"], - versions = VERSION_READ_ONLY_DISKS_NULLABLE.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn disk_create( rqctx: RequestContext, @@ -3384,6 +3412,26 @@ pub trait NexusExternalApi { new_disk: TypedBody, ) -> Result, HttpError>; + // TODO-correctness See note about instance create. This should be async. + /// Create disk + #[endpoint { + operation_id = "disk_create", + method = POST, + path = "/v1/disks", + tags = ["disks"], + versions = VERSION_READ_ONLY_DISKS_NULLABLE..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn disk_create_v2026_01_31_00( + rqctx: RequestContext, + query_params: Query, + new_disk: TypedBody, + ) -> Result, HttpError> + { + Self::disk_create(rqctx, query_params, new_disk) + .await + .map(|resp| resp.map(Into::into)) + } + // TODO-correctness See note about instance create. This should be async. /// Create disk #[endpoint { @@ -3397,8 +3445,14 @@ pub trait NexusExternalApi { rqctx: RequestContext, query_params: Query, new_disk: TypedBody, - ) -> Result, HttpError> { - Self::disk_create(rqctx, query_params, new_disk.map(Into::into)).await + ) -> Result, HttpError> + { + Self::disk_create_v2026_01_31_00( + rqctx, + query_params, + new_disk.map(Into::into), + ) + .await } // TODO-correctness See note about instance create. This should be async. @@ -3451,7 +3505,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/disks/{disk}", tags = ["disks"], - versions = VERSION_READ_ONLY_DISKS.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn disk_view( rqctx: RequestContext, @@ -3459,6 +3513,24 @@ pub trait NexusExternalApi { query_params: Query, ) -> Result, HttpError>; + /// Fetch disk + #[endpoint { + operation_id = "disk_view", + method = GET, + path = "/v1/disks/{disk}", + tags = ["disks"], + versions = VERSION_READ_ONLY_DISKS..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn disk_view_v2026_01_30_01( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result, HttpError> { + Self::disk_view(rqctx, path_params, query_params) + .await + .map(|resp| resp.map(Into::into)) + } + /// Fetch disk #[endpoint { operation_id = "disk_view", @@ -3877,7 +3949,7 @@ pub trait NexusExternalApi { method = GET, path = "/v1/instances/{instance}/disks", tags = ["instances"], - versions = VERSION_READ_ONLY_DISKS.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn instance_disk_list( rqctx: RequestContext, @@ -3887,6 +3959,33 @@ pub trait NexusExternalApi { path_params: Path, ) -> Result>, HttpError>; + /// List disks for instance + #[endpoint { + operation_id = "instance_disk_list", + method = GET, + path = "/v1/instances/{instance}/disks", + tags = ["instances"], + versions = VERSION_READ_ONLY_DISKS..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn instance_disk_list_v2026_01_30_01( + rqctx: RequestContext, + query_params: Query< + PaginatedByNameOrId, + >, + path_params: Path, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + Self::instance_disk_list(rqctx, query_params, path_params).await.map( + |HttpResponseOk(page)| { + let items: Vec<_> = + page.items.into_iter().map(Into::into).collect(); + HttpResponseOk(ResultsPage { next_page: page.next_page, items }) + }, + ) + } + /// List disks for instance #[endpoint { operation_id = "instance_disk_list", @@ -3955,7 +4054,7 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances/{instance}/disks/attach", tags = ["instances"], - versions = VERSION_READ_ONLY_DISKS.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn instance_disk_attach( rqctx: RequestContext, @@ -3964,6 +4063,31 @@ pub trait NexusExternalApi { disk_to_attach: TypedBody, ) -> Result, HttpError>; + /// Attach disk to instance + #[endpoint { + operation_id = "instance_disk_attach", + method = POST, + path = "/v1/instances/{instance}/disks/attach", + tags = ["instances"], + versions = VERSION_READ_ONLY_DISKS..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn instance_disk_attach_v2026_01_30_01( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + disk_to_attach: TypedBody, + ) -> Result, HttpError> + { + Self::instance_disk_attach( + rqctx, + path_params, + query_params, + disk_to_attach, + ) + .await + .map(|resp| resp.map(Into::into)) + } + /// Attach disk to instance #[endpoint { operation_id = "instance_disk_attach", @@ -4019,7 +4143,7 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances/{instance}/disks/detach", tags = ["instances"], - versions = VERSION_READ_ONLY_DISKS.., + versions = VERSION_DISK_BLOCK_SIZE_TYPE.., }] async fn instance_disk_detach( rqctx: RequestContext, @@ -4028,6 +4152,31 @@ pub trait NexusExternalApi { disk_to_detach: TypedBody, ) -> Result, HttpError>; + /// Detach disk from instance + #[endpoint { + operation_id = "instance_disk_detach", + method = POST, + path = "/v1/instances/{instance}/disks/detach", + tags = ["instances"], + versions = VERSION_READ_ONLY_DISKS..VERSION_DISK_BLOCK_SIZE_TYPE, + }] + async fn instance_disk_detach_v2026_01_30_01( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + disk_to_detach: TypedBody, + ) -> Result, HttpError> + { + Self::instance_disk_detach( + rqctx, + path_params, + query_params, + disk_to_detach, + ) + .await + .map(|resp| resp.map(Into::into)) + } + /// Detach disk from instance #[endpoint { operation_id = "instance_disk_detach", diff --git a/nexus/external-api/src/v2025_11_20_00_local.rs b/nexus/external-api/src/v2025_11_20_00_local.rs index 871e99e2fa4..7df6446434c 100644 --- a/nexus/external-api/src/v2025_11_20_00_local.rs +++ b/nexus/external-api/src/v2025_11_20_00_local.rs @@ -51,7 +51,7 @@ impl From for external::Disk { snapshot_id: old.snapshot_id, image_id: old.image_id, size: old.size, - block_size: old.block_size, + block_size: old.block_size.into(), state: old.state, device_path: old.device_path, disk_type: old.disk_type.into(), @@ -70,7 +70,7 @@ impl TryFrom for Disk { snapshot_id: new.snapshot_id, image_id: new.image_id, size: new.size, - block_size: new.block_size, + block_size: new.block_size.into(), state: new.state, device_path: new.device_path, disk_type: match new.disk_type { diff --git a/nexus/external-api/src/v2026_01_30_00_local.rs b/nexus/external-api/src/v2026_01_30_00_local.rs index 2cc21371593..b71a0514442 100644 --- a/nexus/external-api/src/v2026_01_30_00_local.rs +++ b/nexus/external-api/src/v2026_01_30_00_local.rs @@ -48,6 +48,34 @@ impl From for Disk { disk_type, read_only: _, // read_only doth not exist in v2026_01_30_00 } = new; + Self { + identity, + project_id, + snapshot_id, + image_id, + size, + block_size: block_size.into(), + state, + device_path, + disk_type, + } + } +} + +impl From for Disk { + fn from(new: crate::v2026_05_20_00_local::Disk) -> Self { + let crate::v2026_05_20_00_local::Disk { + identity, + project_id, + snapshot_id, + image_id, + size, + block_size, + state, + device_path, + disk_type, + read_only: _, // read_only doth not exist in v2026_01_30_00 + } = new; Self { identity, project_id, diff --git a/nexus/external-api/src/v2026_05_20_00_local.rs b/nexus/external-api/src/v2026_05_20_00_local.rs new file mode 100644 index 00000000000..ed1fadd135e --- /dev/null +++ b/nexus/external-api/src/v2026_05_20_00_local.rs @@ -0,0 +1,72 @@ +// 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/. + +//! Types from API version 2026_05_20_00 that cannot live in `nexus-types-versions` +//! because they convert to/from `omicron-common` types (orphan rule). +//! +//! This version pre-dates `DISK_BLOCK_SIZE_TYPE`, which changed the `Disk` +//! response field `block_size` from `ByteCount` to the constrained `BlockSize` +//! newtype. Older API versions in the range +//! `VERSION_READ_ONLY_DISKS_NULLABLE..VERSION_DISK_BLOCK_SIZE_TYPE` still +//! report `block_size` as a plain `ByteCount`. + +use api_identity::ObjectIdentity; +use omicron_common::api::external; +use omicron_common::api::external::ByteCount; +use omicron_common::api::external::DiskState; +use omicron_common::api::external::DiskType; +use omicron_common::api::external::IdentityMetadata; +use omicron_common::api::external::ObjectIdentity; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +/// View of a Disk +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Disk { + #[serde(flatten)] + pub identity: IdentityMetadata, + pub project_id: Uuid, + /// ID of snapshot from which disk was created, if any + pub snapshot_id: Option, + /// ID of image from which disk was created, if any + pub image_id: Option, + pub size: ByteCount, + pub block_size: ByteCount, + pub state: DiskState, + pub device_path: String, + pub disk_type: DiskType, + /// Whether or not this disk is read-only. + pub read_only: bool, +} + +impl From for Disk { + fn from(new: external::Disk) -> Self { + let external::Disk { + identity, + project_id, + snapshot_id, + image_id, + size, + block_size, + state, + device_path, + disk_type, + read_only, + } = new; + Self { + identity, + project_id, + snapshot_id, + image_id, + size, + block_size: block_size.into(), + state, + device_path, + disk_type, + read_only, + } + } +} diff --git a/nexus/types/versions/src/impls/disk.rs b/nexus/types/versions/src/impls/disk.rs deleted file mode 100644 index bf1acec44f6..00000000000 --- a/nexus/types/versions/src/impls/disk.rs +++ /dev/null @@ -1,31 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functional code for disk types. - -use crate::latest::disk::BlockSize; -use omicron_common::api::external::ByteCount; - -impl TryFrom for BlockSize { - type Error = anyhow::Error; - fn try_from(x: u32) -> Result { - if ![512, 2048, 4096].contains(&x) { - anyhow::bail!("invalid block size {}", x); - } - - Ok(BlockSize(x)) - } -} - -impl From for ByteCount { - fn from(bs: BlockSize) -> ByteCount { - ByteCount::from(bs.0) - } -} - -impl From for u64 { - fn from(bs: BlockSize) -> u64 { - u64::from(bs.0) - } -} diff --git a/nexus/types/versions/src/impls/mod.rs b/nexus/types/versions/src/impls/mod.rs index a71703a2e14..88c5cf0c18b 100644 --- a/nexus/types/versions/src/impls/mod.rs +++ b/nexus/types/versions/src/impls/mod.rs @@ -42,7 +42,6 @@ pub(crate) use id_path_param; pub(crate) use path_param; mod alert; -mod disk; mod hardware; mod instance; pub(crate) mod multicast; diff --git a/nexus/types/versions/src/initial/disk.rs b/nexus/types/versions/src/initial/disk.rs index 7148a7fedf4..14bfde0a927 100644 --- a/nexus/types/versions/src/initial/disk.rs +++ b/nexus/types/versions/src/initial/disk.rs @@ -4,6 +4,8 @@ //! Disk types for version INITIAL. +pub use omicron_common::api::external::BlockSize; + use omicron_common::api::external::{ ByteCount, DiskState, IdentityMetadata, IdentityMetadataCreateParams, }; @@ -11,35 +13,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -#[serde(try_from = "u32")] -pub struct BlockSize(pub u32); - -impl schemars::JsonSchema for BlockSize { - fn schema_name() -> String { - "BlockSize".to_string() - } - - fn json_schema( - _: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - id: None, - title: Some("Disk block size in bytes".to_string()), - ..Default::default() - })), - instance_type: Some(schemars::schema::InstanceType::Integer.into()), - enum_values: Some(vec![ - serde_json::json!(512), - serde_json::json!(2048), - serde_json::json!(4096), - ]), - ..Default::default() - }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DiskType { diff --git a/nexus/types/versions/src/instances_external_subnets/disk.rs b/nexus/types/versions/src/instances_external_subnets/disk.rs index 926814525ee..85c54231b0f 100644 --- a/nexus/types/versions/src/instances_external_subnets/disk.rs +++ b/nexus/types/versions/src/instances_external_subnets/disk.rs @@ -55,7 +55,7 @@ impl From for Disk { snapshot_id, image_id, size, - block_size, + block_size: block_size.into(), state, device_path, disk_type, diff --git a/nexus/types/versions/src/local_storage/disk.rs b/nexus/types/versions/src/local_storage/disk.rs index 605ae0e9178..83468ac459b 100644 --- a/nexus/types/versions/src/local_storage/disk.rs +++ b/nexus/types/versions/src/local_storage/disk.rs @@ -105,7 +105,7 @@ impl TryFrom snapshot_id: new.snapshot_id, image_id: new.image_id, size: new.size, - block_size: new.block_size, + block_size: new.block_size.into(), state: new.state, device_path: new.device_path, disk_type: match new.disk_type { @@ -148,7 +148,7 @@ impl From for omicron_common::api::external::Disk { snapshot_id: old.snapshot_id, image_id: old.image_id, size: old.size, - block_size: old.block_size, + block_size: old.block_size.to_bytes().into(), state: old.state, device_path: old.device_path, disk_type: old.disk_type.into(), diff --git a/openapi/nexus/nexus-2026052000.0.0-ced7df.json.gitstub b/openapi/nexus/nexus-2026052000.0.0-ced7df.json.gitstub new file mode 100644 index 00000000000..d265307f58f --- /dev/null +++ b/openapi/nexus/nexus-2026052000.0.0-ced7df.json.gitstub @@ -0,0 +1 @@ +4c614ceb0b05e7ab9b2a1f19504a2328dc87a087:openapi/nexus/nexus-2026052000.0.0-ced7df.json diff --git a/openapi/nexus/nexus-2026052000.0.0-ced7df.json b/openapi/nexus/nexus-2026052100.0.0-0172d0.json similarity index 99% rename from openapi/nexus/nexus-2026052000.0.0-ced7df.json rename to openapi/nexus/nexus-2026052100.0.0-0172d0.json index c3b359aed5a..70676a1e3ea 100644 --- a/openapi/nexus/nexus-2026052000.0.0-ced7df.json +++ b/openapi/nexus/nexus-2026052100.0.0-0172d0.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "2026052000.0.0" + "version": "2026052100.0.0" }, "paths": { "/device/auth": { @@ -18776,6 +18776,7 @@ }, "BlockSize": { "title": "Disk block size in bytes", + "description": "Size of blocks for a disk. Valid values are: 512, 2048, or 4096.", "type": "integer", "enum": [ 512, @@ -19773,7 +19774,7 @@ "type": "object", "properties": { "block_size": { - "$ref": "#/components/schemas/ByteCount" + "$ref": "#/components/schemas/BlockSize" }, "description": { "description": "Human-readable free-form text about a resource", diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index 867f4785de9..701cad29b72 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2026052000.0.0-ced7df.json \ No newline at end of file +nexus-2026052100.0.0-0172d0.json \ No newline at end of file