Skip to content

Commit a05fa49

Browse files
authored
[API] Bounds-check that disks are a minimum size, and use a minimum granularity size (#1029) (#1150)
* [API] Bounds-check that disks are a minimum size, and use a minimum granularity size (#1029) - Requires the user to select a size of at least one gibibyte when creating a new disk. - Add a test which attempts to create a disk that is 512 mebibytes in size. * Add MIN_DISK_SIZE parameter - Add MIN_DISK_SIZE constant. - Modify tests to create disks with a size of MIN_DISK_SIZE / 2. * Add disk size granularity The disk size must be divisible by 1 GiB. * Append _BYTES to MIN_DISK_SIZE * Add disk minimum & granularity to GlobalImage case for project_create_disk * Change disk size errors to read MIN_DISK_SIZE_BYTES instead of 1 GiB * Change MIN_DISK_SIZE error to utilize ByteCount::from * Expand min_disk_size test coverage to include error's returned. Changed disk granularity test to be a multiple of the block_size.
1 parent efbb453 commit a05fa49

File tree

3 files changed

+144
-1
lines changed

3 files changed

+144
-1
lines changed

nexus/src/app/disk.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::db;
1313
use crate::db::lookup::LookupPath;
1414
use crate::db::model::Name;
1515
use crate::external_api::params;
16+
use omicron_common::api::external::ByteCount;
1617
use omicron_common::api::external::CreateResult;
1718
use omicron_common::api::external::DataPageParams;
1819
use omicron_common::api::external::DeleteResult;
@@ -54,6 +55,32 @@ impl super::Nexus {
5455
),
5556
});
5657
}
58+
59+
// Reject disks where the size isn't at least
60+
// MIN_DISK_SIZE_BYTES
61+
if params.size.to_bytes() < params::MIN_DISK_SIZE_BYTES as u64 {
62+
return Err(Error::InvalidValue {
63+
label: String::from("size"),
64+
message: format!(
65+
"total size must be at least {}",
66+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
67+
),
68+
});
69+
}
70+
71+
// Reject disks where the MIN_DISK_SIZE_BYTES doesn't evenly divide
72+
// the size
73+
if (params.size.to_bytes() % params::MIN_DISK_SIZE_BYTES as u64)
74+
!= 0
75+
{
76+
return Err(Error::InvalidValue {
77+
label: String::from("size"),
78+
message: format!(
79+
"total size must be a multiple of {}",
80+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
81+
),
82+
});
83+
}
5784
}
5885
params::DiskSource::Snapshot { snapshot_id: _ } => {
5986
// Until we implement snapshots, do not allow disks to be
@@ -105,6 +132,32 @@ impl super::Nexus {
105132
),
106133
));
107134
}
135+
136+
// Reject disks where the size isn't at least
137+
// MIN_DISK_SIZE_BYTES
138+
if params.size.to_bytes() < params::MIN_DISK_SIZE_BYTES as u64 {
139+
return Err(Error::InvalidValue {
140+
label: String::from("size"),
141+
message: format!(
142+
"total size must be at least {}",
143+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
144+
),
145+
});
146+
}
147+
148+
// Reject disks where the MIN_DISK_SIZE_BYTES doesn't evenly divide
149+
// the size
150+
if (params.size.to_bytes() % params::MIN_DISK_SIZE_BYTES as u64)
151+
!= 0
152+
{
153+
return Err(Error::InvalidValue {
154+
label: String::from("size"),
155+
message: format!(
156+
"total size must be a multiple of {}",
157+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
158+
),
159+
});
160+
}
108161
}
109162
}
110163

nexus/src/external_api/params.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ pub struct VpcRouterUpdate {
485485

486486
// DISKS
487487

488+
pub const MIN_DISK_SIZE_BYTES: u32 = 1 << 30; // 1 GiB
489+
488490
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
489491
#[serde(try_from = "u32")] // invoke the try_from validation routine below
490492
pub struct BlockSize(pub u32);

nexus/tests/integration_tests/disks.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,8 @@ async fn test_disk_invalid_block_size_rejected(
688688
.unwrap();
689689
}
690690

691-
// Tests that a disk is rejected if the total size isn't divided by the block size
691+
// Tests that a disk is rejected if the total size isn't divided by the
692+
// block size
692693
#[nexus_test]
693694
async fn test_disk_reject_total_size_not_divisible_by_block_size(
694695
cptestctx: &ControlPlaneTestContext,
@@ -732,6 +733,93 @@ async fn test_disk_reject_total_size_not_divisible_by_block_size(
732733
.unwrap();
733734
}
734735

736+
// Tests that a disk is rejected if the total size is less than MIN_DISK_SIZE
737+
#[nexus_test]
738+
async fn test_disk_reject_total_size_less_than_one_gibibyte(
739+
cptestctx: &ControlPlaneTestContext,
740+
) {
741+
let client = &cptestctx.external_client;
742+
create_org_and_project(client).await;
743+
744+
let disk_size = ByteCount::from(params::MIN_DISK_SIZE_BYTES / 2);
745+
746+
// Attempt to allocate the disk, observe a server error.
747+
let disks_url = get_disks_url();
748+
let new_disk = params::DiskCreate {
749+
identity: IdentityMetadataCreateParams {
750+
name: DISK_NAME.parse().unwrap(),
751+
description: String::from("sells rainsticks"),
752+
},
753+
disk_source: params::DiskSource::Blank {
754+
block_size: params::BlockSize::try_from(512).unwrap(),
755+
},
756+
size: disk_size,
757+
};
758+
759+
let error = NexusRequest::new(
760+
RequestBuilder::new(client, Method::POST, &disks_url)
761+
.body(Some(&new_disk))
762+
.expect_status(Some(StatusCode::BAD_REQUEST)),
763+
)
764+
.authn_as(AuthnMode::PrivilegedUser)
765+
.execute()
766+
.await
767+
.unwrap()
768+
.parsed_body::<dropshot::HttpErrorResponseBody>()
769+
.unwrap();
770+
assert_eq!(
771+
error.message,
772+
format!(
773+
"unsupported value for \"size\": total size must be at least {}",
774+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
775+
)
776+
);
777+
}
778+
779+
// Tests that a disk is rejected if the total size isn't divisible by
780+
// MIN_DISK_SIZE_BYTES
781+
#[nexus_test]
782+
async fn test_disk_reject_total_size_not_divisible_by_min_disk_size(
783+
cptestctx: &ControlPlaneTestContext,
784+
) {
785+
let client = &cptestctx.external_client;
786+
create_org_and_project(client).await;
787+
788+
let disk_size = ByteCount::from(1024 * 1024 * 1024 + 512);
789+
790+
// Attempt to allocate the disk, observe a server error.
791+
let disks_url = get_disks_url();
792+
let new_disk = params::DiskCreate {
793+
identity: IdentityMetadataCreateParams {
794+
name: DISK_NAME.parse().unwrap(),
795+
description: String::from("sells rainsticks"),
796+
},
797+
disk_source: params::DiskSource::Blank {
798+
block_size: params::BlockSize::try_from(512).unwrap(),
799+
},
800+
size: disk_size,
801+
};
802+
803+
let error = NexusRequest::new(
804+
RequestBuilder::new(client, Method::POST, &disks_url)
805+
.body(Some(&new_disk))
806+
.expect_status(Some(StatusCode::BAD_REQUEST)),
807+
)
808+
.authn_as(AuthnMode::PrivilegedUser)
809+
.execute()
810+
.await
811+
.unwrap()
812+
.parsed_body::<dropshot::HttpErrorResponseBody>()
813+
.unwrap();
814+
assert_eq!(
815+
error.message,
816+
format!(
817+
"unsupported value for \"size\": total size must be a multiple of {}",
818+
ByteCount::from(params::MIN_DISK_SIZE_BYTES)
819+
)
820+
);
821+
}
822+
735823
async fn disk_get(client: &ClientTestContext, disk_url: &str) -> Disk {
736824
NexusRequest::object_get(client, disk_url)
737825
.authn_as(AuthnMode::PrivilegedUser)

0 commit comments

Comments
 (0)