From 77afd369dd5ae0ae627c99769cd1cd8156f58b81 Mon Sep 17 00:00:00 2001 From: chriswldenyer Date: Fri, 5 Jan 2024 14:00:48 +0000 Subject: [PATCH] feat: capacity limit for volumes Add mechanism for limiting the total volume size on the system by rejecting volume creation requests from REST. Includes BDDs, although the resize limit is not yet implemented. Signed-off-by: chriswldenyer --- .../agents/src/bin/core/volume/operations.rs | 4 ++-- .../agents/src/bin/core/volume/specs.rs | 23 +++++++++++++++---- control-plane/agents/src/common/errors.rs | 8 +++++++ .../grpc/proto/v1/volume/volume.proto | 2 ++ .../grpc/src/operations/volume/traits.rs | 12 ++++++++++ control-plane/rest/src/versions/v0.rs | 1 + .../src/types/v0/transport/volume.rs | 2 ++ .../volume/capacity_limit/creation.feature | 22 ++++++++++++++++++ .../volume/capacity_limit/resize.feature | 22 ++++++++++++++++++ 9 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tests/bdd/features/volume/capacity_limit/creation.feature create mode 100644 tests/bdd/features/volume/capacity_limit/resize.feature diff --git a/control-plane/agents/src/bin/core/volume/operations.rs b/control-plane/agents/src/bin/core/volume/operations.rs index 76d804045..0b7956c61 100644 --- a/control-plane/agents/src/bin/core/volume/operations.rs +++ b/control-plane/agents/src/bin/core/volume/operations.rs @@ -696,7 +696,7 @@ impl ResourceLifecycleExt for OperationGuardArc { ) -> Result { let specs = registry.specs(); let mut volume = specs - .get_or_create_volume(&CreateVolumeSource::None(request)) + .get_or_create_volume(&CreateVolumeSource::None(request))? .operation_guard_wait() .await?; let volume_clone = volume.start_create(registry, request).await?; @@ -790,7 +790,7 @@ impl ResourceLifecycleExt> for OperationGuardArc ResourceMutex { + ) -> Result, SvcError> { let mut specs = self.write(); if let Some(volume) = specs.volumes.get(&request.source().uuid) { - volume.clone() + Ok(volume.clone()) } else { + // if request has a capacity limit, add up the volumes and reject + // if the capacity limit would be exceeded + match request.source().capacity_limit { + None => {} // no limit, no check needed + Some(limit) => { + let mut total: u64 = request.source().size; + for vol in specs.volumes.to_vec() { + total += vol.lock().size; + if total > limit { + println!("CWD max vol size exceeded, {} vs {}", total, limit); + return Err(SvcError::VolWouldExceedCapacity {}); + } + } + } + } match request { CreateVolumeSource::None(_) => { - specs.volumes.insert(VolumeSpec::from(request.source())) + Ok(specs.volumes.insert(VolumeSpec::from(request.source()))) } CreateVolumeSource::Snapshot(create_from_snap) => { let mut spec = VolumeSpec::from(request.source()); spec.set_content_source(Some(create_from_snap.to_snapshot_source())); - specs.volumes.insert(spec) + Ok(specs.volumes.insert(spec)) } } } diff --git a/control-plane/agents/src/common/errors.rs b/control-plane/agents/src/common/errors.rs index f09f4eb41..d01986f71 100644 --- a/control-plane/agents/src/common/errors.rs +++ b/control-plane/agents/src/common/errors.rs @@ -334,6 +334,8 @@ pub enum SvcError { DrainNotAllowedWhenHAisDisabled {}, #[snafu(display("Target switchover is not allowed without HA"))] SwitchoverNotAllowedWhenHAisDisabled {}, + #[snafu(display("The volume would exceed the capacity"))] + VolWouldExceedCapacity {}, } impl SvcError { @@ -934,6 +936,12 @@ impl From for ReplyError { source, extra, }, + SvcError::VolWouldExceedCapacity {} => ReplyError { + kind: ReplyErrorKind::OutOfRange, + resource: ResourceKind::Volume, + source, + extra, + }, } } } diff --git a/control-plane/grpc/proto/v1/volume/volume.proto b/control-plane/grpc/proto/v1/volume/volume.proto index 633b90c20..12b48ae2e 100644 --- a/control-plane/grpc/proto/v1/volume/volume.proto +++ b/control-plane/grpc/proto/v1/volume/volume.proto @@ -263,6 +263,8 @@ message CreateVolumeRequest { bool thin = 8; // Affinity Group related information. optional AffinityGroup affinity_group = 9; + // maximum total volume size + optional uint64 capacity_limit = 10; } // Publish a volume on a node diff --git a/control-plane/grpc/src/operations/volume/traits.rs b/control-plane/grpc/src/operations/volume/traits.rs index 5f23b8986..c53d6c3ba 100644 --- a/control-plane/grpc/src/operations/volume/traits.rs +++ b/control-plane/grpc/src/operations/volume/traits.rs @@ -890,6 +890,8 @@ pub trait CreateVolumeInfo: Send + Sync + std::fmt::Debug { fn thin(&self) -> bool; /// Affinity Group related information. fn affinity_group(&self) -> Option; + /// Capacity Limit + fn capacity_limit(&self) -> Option; } impl CreateVolumeInfo for CreateVolume { @@ -924,6 +926,10 @@ impl CreateVolumeInfo for CreateVolume { fn affinity_group(&self) -> Option { self.affinity_group.clone() } + + fn capacity_limit(&self) -> Option { + self.capacity_limit + } } /// Intermediate structure that validates the conversion to CreateVolumeRequest type. @@ -972,6 +978,10 @@ impl CreateVolumeInfo for ValidatedCreateVolumeRequest { fn affinity_group(&self) -> Option { self.inner.affinity_group.clone().map(|ag| ag.into()) } + + fn capacity_limit(&self) -> Option { + self.inner.capacity_limit + } } impl ValidateRequestTypes for CreateVolumeRequest { @@ -1008,6 +1018,7 @@ impl From<&dyn CreateVolumeInfo> for CreateVolume { labels: data.labels(), thin: data.thin(), affinity_group: data.affinity_group(), + capacity_limit: data.capacity_limit(), } } } @@ -1025,6 +1036,7 @@ impl From<&dyn CreateVolumeInfo> for CreateVolumeRequest { .map(|labels| crate::common::StringMapValue { value: labels }), thin: data.thin(), affinity_group: data.affinity_group().map(|ag| ag.into()), + capacity_limit: data.capacity_limit(), } } } diff --git a/control-plane/rest/src/versions/v0.rs b/control-plane/rest/src/versions/v0.rs index 34029fa6c..747dbd453 100644 --- a/control-plane/rest/src/versions/v0.rs +++ b/control-plane/rest/src/versions/v0.rs @@ -231,6 +231,7 @@ impl CreateVolumeBody { labels: self.labels.clone(), thin: self.thin, affinity_group: self.affinity_group.clone(), + capacity_limit: None, } } /// Convert into rpc request type. diff --git a/control-plane/stor-port/src/types/v0/transport/volume.rs b/control-plane/stor-port/src/types/v0/transport/volume.rs index 090235e85..90efac7e1 100644 --- a/control-plane/stor-port/src/types/v0/transport/volume.rs +++ b/control-plane/stor-port/src/types/v0/transport/volume.rs @@ -455,6 +455,8 @@ pub struct CreateVolume { pub thin: bool, /// Affinity Group related information. pub affinity_group: Option, + /// Maximum total system volume size. + pub capacity_limit: Option, } /// Create volume request. diff --git a/tests/bdd/features/volume/capacity_limit/creation.feature b/tests/bdd/features/volume/capacity_limit/creation.feature new file mode 100644 index 000000000..d801fcba6 --- /dev/null +++ b/tests/bdd/features/volume/capacity_limit/creation.feature @@ -0,0 +1,22 @@ +Feature: Volume creation capacity limit + + Background: + Given a control plane, Io-Engine instances and a pool + + Scenario: attempted creation exceeding the capacity limit + Given a gRPC request to create a volume + When the request includes a capacity limit + And the volume creation would result in the capacity limit being exceeded + Then volume creation should fail with an out-of-range error + + Scenario: attempted creation within the capacity limit + Given a gRPC request to create a volume + When the request includes a capacity limit + And the volume creation would not result in the capacity limit being exceeded + Then volume creation should succeed + + Scenario: attempted creation with no capacity limit + Given a gRPC request to create a volume + When the request does not include a capacity limit + Then volume creation should succeed + diff --git a/tests/bdd/features/volume/capacity_limit/resize.feature b/tests/bdd/features/volume/capacity_limit/resize.feature new file mode 100644 index 000000000..bc852edf0 --- /dev/null +++ b/tests/bdd/features/volume/capacity_limit/resize.feature @@ -0,0 +1,22 @@ +Feature: Volume resize capacity limit + + Background: + Given a control plane, Io-Engine instances and a pool + + Scenario: attempted volume resize exceeding the capacity limit + Given a gRPC request to resize a volume + When the request includes a capacity limit + And the volume resize would result in the capacity limit being exceeded + Then volume resize should fail with an out-of-range error + + Scenario: attempted resize within the capacity limit + Given a gRPC request to resize a volume + When the request includes a capacity limit + And the volume resize would not result in the capacity limit being exceeded + Then volume resize should succeed + + Scenario: attempted creation with no capacity limit + Given a gRPC request to resize a volume + When the request does not include a capacity limit + Then volume resize should succeed +