From 8b467154fb74f24265c2ed65155e03279206cd06 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 | 22 +++++++++++++++---- control-plane/agents/src/common/errors.rs | 8 +++++++ control-plane/grpc/proto/v1/misc/common.proto | 1 + .../grpc/proto/v1/volume/volume.proto | 2 ++ control-plane/grpc/src/misc/traits.rs | 2 ++ .../grpc/src/operations/volume/traits.rs | 12 ++++++++++ .../rest/openapi-specs/v0_api_spec.yaml | 3 ++- control-plane/rest/src/versions/v0.rs | 1 + .../stor-port/src/transport_api/mod.rs | 1 + control-plane/stor-port/src/types/mod.rs | 4 ++++ .../src/types/v0/transport/volume.rs | 2 ++ 12 files changed, 55 insertions(+), 7 deletions(-) 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 { + return Err(SvcError::CapacityLimitExceeded {}); + } + } + } + } 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..519553895 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 limit"))] + CapacityLimitExceeded {}, } impl SvcError { @@ -934,6 +936,12 @@ impl From for ReplyError { source, extra, }, + SvcError::CapacityLimitExceeded {} => ReplyError { + kind: ReplyErrorKind::CapacityLimitExceeded, + resource: ResourceKind::Volume, + source, + extra, + }, } } } diff --git a/control-plane/grpc/proto/v1/misc/common.proto b/control-plane/grpc/proto/v1/misc/common.proto index a4dd4bda8..76115c2e9 100644 --- a/control-plane/grpc/proto/v1/misc/common.proto +++ b/control-plane/grpc/proto/v1/misc/common.proto @@ -67,6 +67,7 @@ enum ReplyErrorKind { ReplicaCreateNumber = 27; VolumeNoReplicas = 28; InUse = 29; + CapacityLimitExceeded = 30; } // ResourceKind for the resource which has undergone this error 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/misc/traits.rs b/control-plane/grpc/src/misc/traits.rs index e43d135cb..64acbd462 100644 --- a/control-plane/grpc/src/misc/traits.rs +++ b/control-plane/grpc/src/misc/traits.rs @@ -98,6 +98,7 @@ impl From for common::ReplyErrorKind { ReplyErrorKind::ReplicaCreateNumber => Self::ReplicaCreateNumber, ReplyErrorKind::VolumeNoReplicas => Self::VolumeNoReplicas, ReplyErrorKind::InUse => Self::InUse, + ReplyErrorKind::CapacityLimitExceeded => Self::CapacityLimitExceeded, } } } @@ -135,6 +136,7 @@ impl From for ReplyErrorKind { common::ReplyErrorKind::ReplicaCreateNumber => Self::ReplicaCreateNumber, common::ReplyErrorKind::VolumeNoReplicas => Self::VolumeNoReplicas, common::ReplyErrorKind::InUse => Self::InUse, + common::ReplyErrorKind::CapacityLimitExceeded => Self::CapacityLimitExceeded, } } } 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/openapi-specs/v0_api_spec.yaml b/control-plane/rest/openapi-specs/v0_api_spec.yaml index 36b0abb3d..da2f8858a 100644 --- a/control-plane/rest/openapi-specs/v0_api_spec.yaml +++ b/control-plane/rest/openapi-specs/v0_api_spec.yaml @@ -2911,6 +2911,7 @@ components: - FailedPersist - Deleting - InUse + - CapacityLimitExceeded required: - details - kind @@ -3826,4 +3827,4 @@ components: content: application/json: schema: - $ref: '#/components/schemas/RestJsonError' \ No newline at end of file + $ref: '#/components/schemas/RestJsonError' 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/transport_api/mod.rs b/control-plane/stor-port/src/transport_api/mod.rs index 25f04a95a..c1ed772ac 100644 --- a/control-plane/stor-port/src/transport_api/mod.rs +++ b/control-plane/stor-port/src/transport_api/mod.rs @@ -442,6 +442,7 @@ pub enum ReplyErrorKind { ReplicaCreateNumber, VolumeNoReplicas, InUse, + CapacityLimitExceeded, } impl From for ReplyErrorKind { diff --git a/control-plane/stor-port/src/types/mod.rs b/control-plane/stor-port/src/types/mod.rs index 59c0b4212..29c1ea2d3 100644 --- a/control-plane/stor-port/src/types/mod.rs +++ b/control-plane/stor-port/src/types/mod.rs @@ -134,6 +134,10 @@ impl From for RestError { let error = RestJsonError::new(details, message, Kind::FailedPrecondition); (StatusCode::PRECONDITION_FAILED, error) } + ReplyErrorKind::CapacityLimitExceeded => { + let error = RestJsonError::new(details, message, Kind::ResourceExhausted); + (StatusCode::INSUFFICIENT_STORAGE, error) + } }; RestError::new(status, error) 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.