Skip to content
53 changes: 53 additions & 0 deletions nexus/src/app/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::db;
use crate::db::lookup::LookupPath;
use crate::db::model::Name;
use crate::external_api::params;
use omicron_common::api::external::ByteCount;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::DeleteResult;
Expand Down Expand Up @@ -54,6 +55,32 @@ impl super::Nexus {
),
});
}

// Reject disks where the size isn't at least
// MIN_DISK_SIZE_BYTES
if params.size.to_bytes() < params::MIN_DISK_SIZE_BYTES as u64 {
return Err(Error::InvalidValue {
label: String::from("size"),
message: format!(
"total size must be at least {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
),
});
}

// Reject disks where the MIN_DISK_SIZE_BYTES doesn't evenly divide
// the size
if (params.size.to_bytes() % params::MIN_DISK_SIZE_BYTES as u64)
!= 0
{
return Err(Error::InvalidValue {
label: String::from("size"),
message: format!(
"total size must be a multiple of {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
),
});
}
}
params::DiskSource::Snapshot { snapshot_id: _ } => {
// Until we implement snapshots, do not allow disks to be
Expand Down Expand Up @@ -105,6 +132,32 @@ impl super::Nexus {
),
));
}

// Reject disks where the size isn't at least
// MIN_DISK_SIZE_BYTES
if params.size.to_bytes() < params::MIN_DISK_SIZE_BYTES as u64 {
return Err(Error::InvalidValue {
label: String::from("size"),
message: format!(
"total size must be at least {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
),
});
}

// Reject disks where the MIN_DISK_SIZE_BYTES doesn't evenly divide
// the size
if (params.size.to_bytes() % params::MIN_DISK_SIZE_BYTES as u64)
!= 0
{
return Err(Error::InvalidValue {
label: String::from("size"),
message: format!(
"total size must be a multiple of {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
),
});
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions nexus/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ pub struct VpcRouterUpdate {

// DISKS

pub const MIN_DISK_SIZE_BYTES: u32 = 1 << 30; // 1 GiB

#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
#[serde(try_from = "u32")] // invoke the try_from validation routine below
pub struct BlockSize(pub u32);
Expand Down
90 changes: 89 additions & 1 deletion nexus/tests/integration_tests/disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,8 @@ async fn test_disk_invalid_block_size_rejected(
.unwrap();
}

// Tests that a disk is rejected if the total size isn't divided by the block size
// Tests that a disk is rejected if the total size isn't divided by the
// block size
#[nexus_test]
async fn test_disk_reject_total_size_not_divisible_by_block_size(
cptestctx: &ControlPlaneTestContext,
Expand Down Expand Up @@ -732,6 +733,93 @@ async fn test_disk_reject_total_size_not_divisible_by_block_size(
.unwrap();
}

// Tests that a disk is rejected if the total size is less than MIN_DISK_SIZE
#[nexus_test]
async fn test_disk_reject_total_size_less_than_one_gibibyte(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;
create_org_and_project(client).await;

let disk_size = ByteCount::from(params::MIN_DISK_SIZE_BYTES / 2);

// Attempt to allocate the disk, observe a server error.
let disks_url = get_disks_url();
let new_disk = params::DiskCreate {
identity: IdentityMetadataCreateParams {
name: DISK_NAME.parse().unwrap(),
description: String::from("sells rainsticks"),
},
disk_source: params::DiskSource::Blank {
block_size: params::BlockSize::try_from(512).unwrap(),
},
size: disk_size,
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &disks_url)
.body(Some(&new_disk))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body::<dropshot::HttpErrorResponseBody>()
.unwrap();
assert_eq!(
error.message,
format!(
"unsupported value for \"size\": total size must be at least {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
)
);
}

// Tests that a disk is rejected if the total size isn't divisible by
// MIN_DISK_SIZE_BYTES
#[nexus_test]
async fn test_disk_reject_total_size_not_divisible_by_min_disk_size(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;
create_org_and_project(client).await;

let disk_size = ByteCount::from(1024 * 1024 * 1024 + 512);

// Attempt to allocate the disk, observe a server error.
let disks_url = get_disks_url();
let new_disk = params::DiskCreate {
identity: IdentityMetadataCreateParams {
name: DISK_NAME.parse().unwrap(),
description: String::from("sells rainsticks"),
},
disk_source: params::DiskSource::Blank {
block_size: params::BlockSize::try_from(512).unwrap(),
},
size: disk_size,
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &disks_url)
.body(Some(&new_disk))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body::<dropshot::HttpErrorResponseBody>()
.unwrap();
assert_eq!(
error.message,
format!(
"unsupported value for \"size\": total size must be a multiple of {}",
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
)
);
}

async fn disk_get(client: &ClientTestContext, disk_url: &str) -> Disk {
NexusRequest::object_get(client, disk_url)
.authn_as(AuthnMode::PrivilegedUser)
Expand Down