Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,68 @@ impl From<ByteCount> 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<u32> for BlockSize {
type Error = anyhow::Error;
fn try_from(x: u32) -> Result<BlockSize, Self::Error> {
if ![512, 2048, 4096].contains(&x) {
anyhow::bail!("invalid block size {}", x);
}

Ok(BlockSize(x))
}
}

impl From<u64> for BlockSize {
fn from(bc: u64) -> BlockSize {
BlockSize(bc as u32)
}
}

impl From<BlockSize> for ByteCount {
fn from(bs: BlockSize) -> ByteCount {
ByteCount::from(bs.0)
}
}

impl From<BlockSize> for u64 {
fn from(bs: BlockSize) -> u64 {
u64::from(bs.0)
}
}

/// Generation numbers stored in the database, used for optimistic concurrency
/// control
//
Expand Down Expand Up @@ -1435,7 +1497,7 @@ pub struct Disk {
/// ID of image from which disk was created, if any
pub image_id: Option<Uuid>,
pub size: ByteCount,
pub block_size: ByteCount,
pub block_size: BlockSize,
pub state: DiskState,
pub device_path: String,
pub disk_type: DiskType,
Expand Down
4 changes: 2 additions & 2 deletions nexus/db-queries/src/db/datastore/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ impl Into<api::external::Disk> 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,
Expand All @@ -388,7 +388,7 @@ impl Into<api::external::Disk> 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,
Expand Down
165 changes: 157 additions & 8 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<Self::Context>,
Expand All @@ -3313,6 +3315,32 @@ pub trait NexusExternalApi {
>,
) -> Result<HttpResponseOk<ResultsPage<Disk>>, 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<Self::Context>,
query_params: Query<
PaginatedByNameOrId<latest::project::ProjectSelector>,
>,
) -> Result<
HttpResponseOk<ResultsPage<v2026_05_20_00_local::Disk>>,
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",
Expand Down Expand Up @@ -3376,14 +3404,34 @@ 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<Self::Context>,
query_params: Query<latest::project::ProjectSelector>,
new_disk: TypedBody<latest::disk::DiskCreate>,
) -> Result<HttpResponseCreated<Disk>, 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<Self::Context>,
query_params: Query<latest::project::ProjectSelector>,
new_disk: TypedBody<latest::disk::DiskCreate>,
) -> Result<HttpResponseCreated<v2026_05_20_00_local::Disk>, 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 {
Expand All @@ -3397,8 +3445,14 @@ pub trait NexusExternalApi {
rqctx: RequestContext<Self::Context>,
query_params: Query<v2025_11_20_00::project::ProjectSelector>,
new_disk: TypedBody<v2026_01_30_01::disk::DiskCreate>,
) -> Result<HttpResponseCreated<Disk>, HttpError> {
Self::disk_create(rqctx, query_params, new_disk.map(Into::into)).await
) -> Result<HttpResponseCreated<v2026_05_20_00_local::Disk>, 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.
Expand Down Expand Up @@ -3451,14 +3505,32 @@ 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<Self::Context>,
path_params: Path<latest::path_params::DiskPath>,
query_params: Query<latest::project::OptionalProjectSelector>,
) -> Result<HttpResponseOk<Disk>, 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<Self::Context>,
path_params: Path<latest::path_params::DiskPath>,
query_params: Query<latest::project::OptionalProjectSelector>,
) -> Result<HttpResponseOk<v2026_05_20_00_local::Disk>, HttpError> {
Self::disk_view(rqctx, path_params, query_params)
.await
.map(|resp| resp.map(Into::into))
}

/// Fetch disk
#[endpoint {
operation_id = "disk_view",
Expand Down Expand Up @@ -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<Self::Context>,
Expand All @@ -3887,6 +3959,33 @@ pub trait NexusExternalApi {
path_params: Path<latest::path_params::InstancePath>,
) -> Result<HttpResponseOk<ResultsPage<Disk>>, 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<Self::Context>,
query_params: Query<
PaginatedByNameOrId<latest::project::OptionalProjectSelector>,
>,
path_params: Path<latest::path_params::InstancePath>,
) -> Result<
HttpResponseOk<ResultsPage<v2026_05_20_00_local::Disk>>,
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",
Expand Down Expand Up @@ -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<Self::Context>,
Expand All @@ -3964,6 +4063,31 @@ pub trait NexusExternalApi {
disk_to_attach: TypedBody<latest::path_params::DiskPath>,
) -> Result<HttpResponseAccepted<Disk>, 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<Self::Context>,
path_params: Path<latest::path_params::InstancePath>,
query_params: Query<latest::project::OptionalProjectSelector>,
disk_to_attach: TypedBody<latest::path_params::DiskPath>,
) -> Result<HttpResponseAccepted<v2026_05_20_00_local::Disk>, 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",
Expand Down Expand Up @@ -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<Self::Context>,
Expand All @@ -4028,6 +4152,31 @@ pub trait NexusExternalApi {
disk_to_detach: TypedBody<latest::path_params::DiskPath>,
) -> Result<HttpResponseAccepted<Disk>, 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<Self::Context>,
path_params: Path<latest::path_params::InstancePath>,
query_params: Query<latest::project::OptionalProjectSelector>,
disk_to_detach: TypedBody<latest::path_params::DiskPath>,
) -> Result<HttpResponseAccepted<v2026_05_20_00_local::Disk>, 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",
Expand Down
4 changes: 2 additions & 2 deletions nexus/external-api/src/v2025_11_20_00_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl From<Disk> 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(),
Expand All @@ -70,7 +70,7 @@ impl TryFrom<external::Disk> 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 {
Expand Down
Loading
Loading