diff --git a/control-plane/agents/src/bin/core/volume/service.rs b/control-plane/agents/src/bin/core/volume/service.rs index 5f6887a12..764eb1268 100644 --- a/control-plane/agents/src/bin/core/volume/service.rs +++ b/control-plane/agents/src/bin/core/volume/service.rs @@ -301,10 +301,29 @@ impl Service { }) } + /// Determine the total volume size on the system. + #[tracing::instrument(level = "info", skip(self), err, fields(volume.uuid))] + async fn total_volume_size(&self) -> Result { + let volumes = self.registry.volumes().await; + let mut total: u64 = 0; + for vol in volumes.iter() { + total += vol.spec().size; + } + Ok(total) + } + /// Create a volume using the given parameters. #[tracing::instrument(level = "info", skip(self), err, fields(volume.uuid = %request.uuid))] pub(super) async fn create_volume(&self, request: &CreateVolume) -> Result { let _permit = self.create_volume_permit().await?; + let capacity_limit = request.capacity_limit(); + if let Some(limit) = capacity_limit { + let total_volume_size = self.total_volume_size().await?; + let this_volume_size = request.size(); + if total_volume_size + this_volume_size > limit { + return Err(SvcError::VolWouldExceedCapacity {}); + } + } OperationGuardArc::::create(&self.registry, request).await?; self.registry.volume(&request.uuid).await } 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 +