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.