Skip to content

Commit b352ffb

Browse files
authored
Do not allow snapshots of read-only disks (#9857)
1 parent fc8e00a commit b352ffb

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

nexus/src/app/sagas/snapshot_create.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ impl NexusSaga for SagaSnapshotCreate {
231231
params: &Self::Params,
232232
mut builder: steno::DagBuilder,
233233
) -> Result<steno::Dag, SagaInitError> {
234+
if params.disk.is_read_only() {
235+
return Err(SagaInitError::InvalidParameter(format!(
236+
"cannot snapshot read-only disk {}!",
237+
params.disk.id(),
238+
)));
239+
}
240+
234241
// Generate IDs
235242
builder.append(Node::action(
236243
"snapshot_id",

nexus/src/app/snapshot.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ impl super::Nexus {
108108
}
109109
};
110110

111+
// Do not allow snapshots of read-only disks. (Note that the snapshot
112+
// create saga will also not allow this, but will bubble up to the user
113+
// as a 500 level error, whereas this is a 400 level error).
114+
if disk.is_read_only() {
115+
return Err(Error::invalid_request(
116+
"can't create a snapshot of a read-only disk",
117+
));
118+
}
119+
111120
// If there isn't a running propolis, Nexus needs to use the Crucible
112121
// Pantry to make this snapshot
113122
let use_the_pantry = if let Some(attach_instance_id) =

nexus/tests/integration_tests/disks.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use nexus_test_utils::resource_helpers::create_default_ip_pools;
2828
use nexus_test_utils::resource_helpers::create_disk;
2929
use nexus_test_utils::resource_helpers::create_instance;
3030
use nexus_test_utils::resource_helpers::create_project;
31+
use nexus_test_utils::resource_helpers::object_create_error;
3132
use nexus_test_utils_macros::nexus_test;
3233
use nexus_types::external_api::params;
3334
use nexus_types::external_api::views;
@@ -2934,6 +2935,106 @@ async fn test_create_read_only_disk_from_image(
29342935
assert_eq!(rw_disk.size, image.size);
29352936
}
29362937

2938+
#[nexus_test]
2939+
async fn test_cannot_snapshot_read_only_disk(
2940+
cptestctx: &ControlPlaneTestContext,
2941+
) {
2942+
let client = &cptestctx.external_client;
2943+
DiskTest::new(&cptestctx).await;
2944+
create_project_and_pool(client).await;
2945+
let disks_url = get_disks_url();
2946+
2947+
// Define a global image
2948+
let image_create_params = params::ImageCreate {
2949+
identity: IdentityMetadataCreateParams {
2950+
name: "alpine".parse().unwrap(),
2951+
description: String::from(
2952+
"you can boot any image, as long as it's alpine",
2953+
),
2954+
},
2955+
source: params::ImageSource::YouCanBootAnythingAsLongAsItsAlpine,
2956+
os: "alpine".to_string(),
2957+
version: "edge".to_string(),
2958+
};
2959+
2960+
let images_url = format!("/v1/images?project={}", PROJECT_NAME);
2961+
let image =
2962+
NexusRequest::objects_post(client, &images_url, &image_create_params)
2963+
.authn_as(AuthnMode::PrivilegedUser)
2964+
.execute_and_parse_unwrap::<views::Image>()
2965+
.await;
2966+
2967+
// Create a base disk from this image, which we will then create a snapshot
2968+
// from in order to create our read-only disk from that snapshot.
2969+
let disk_size = ByteCount::from_gibibytes_u32(2);
2970+
let base_disk_name = "base-disk";
2971+
let base_disk = params::DiskCreate {
2972+
identity: IdentityMetadataCreateParams {
2973+
name: base_disk_name.parse().unwrap(),
2974+
description: String::from("sells rainsticks"),
2975+
},
2976+
disk_backend: params::DiskBackend::Distributed {
2977+
disk_source: params::DiskSource::Image {
2978+
image_id: image.identity.id,
2979+
read_only: false,
2980+
},
2981+
},
2982+
size: disk_size,
2983+
};
2984+
2985+
NexusRequest::new(
2986+
RequestBuilder::new(client, Method::POST, &disks_url)
2987+
.body(Some(&base_disk))
2988+
.expect_status(Some(StatusCode::CREATED)),
2989+
)
2990+
.authn_as(AuthnMode::PrivilegedUser)
2991+
.execute_and_parse_unwrap::<Disk>()
2992+
.await;
2993+
2994+
// Assert the base disk is detached
2995+
let base_disk = disk_get(client, &get_disk_url(base_disk_name)).await;
2996+
assert_eq!(base_disk.state, DiskState::Detached);
2997+
2998+
// Create a snapshot of the base disk.
2999+
let snapshot = resource_helpers::create_snapshot(
3000+
client,
3001+
PROJECT_NAME,
3002+
base_disk_name,
3003+
"ro-snapshot",
3004+
)
3005+
.await;
3006+
assert_eq!(snapshot.disk_id, base_disk.identity.id);
3007+
assert_eq!(snapshot.size, base_disk.size);
3008+
3009+
// Okay, now make a read-only disk out of that snapshot.
3010+
let ro_disk = resource_helpers::create_disk_from_snapshot(
3011+
client,
3012+
PROJECT_NAME,
3013+
"ro-disk-from-snap",
3014+
&snapshot,
3015+
true,
3016+
)
3017+
.await;
3018+
3019+
assert!(ro_disk.read_only);
3020+
3021+
// Now try to take a snapshot of the read-only disk. It should fail with a
3022+
// 400.
3023+
object_create_error(
3024+
client,
3025+
&format!("/v1/snapshots?project={}", PROJECT_NAME),
3026+
&params::SnapshotCreate {
3027+
identity: IdentityMetadataCreateParams {
3028+
name: "will-not-work".parse().unwrap(),
3029+
description: String::from("no thanks"),
3030+
},
3031+
disk: ro_disk.identity.name.clone().try_into().unwrap(),
3032+
},
3033+
http::StatusCode::BAD_REQUEST,
3034+
)
3035+
.await;
3036+
}
3037+
29373038
async fn disk_get(client: &ClientTestContext, disk_url: &str) -> Disk {
29383039
NexusRequest::object_get(client, disk_url)
29393040
.authn_as(AuthnMode::PrivilegedUser)

0 commit comments

Comments
 (0)