From ebf90170bc42d9934b526bfb87fddeb5b171cfcb Mon Sep 17 00:00:00 2001 From: Prabhala Tara Aasrita Date: Wed, 22 Jul 2026 16:33:56 +0530 Subject: [PATCH] pool: support CRUSH MSR rules for EC pools Added failureDomains and osdsPerDomain fields to the ErasureCodedSpec, which maps to the crush-num-failure-domains and crush-osds-per-failure-domain Ceph EC profile parameters. This enables creating EC pools that distribute chunks across fewer, larger hosts without needing one host per data chunk. Signed-off-by: Prabhala Tara Aasrita (cherry picked from commit 715f38de91470f841d1e55828c4176822c5ddfde) --- Documentation/CRDs/specification.md | 28 ++++ .../charts/rook-ceph/templates/resources.yaml | 133 ++++++++++++++++++ deploy/examples/crds.yaml | 133 ++++++++++++++++++ pkg/apis/ceph.rook.io/v1/types.go | 15 ++ .../ceph/client/erasure-code-profile.go | 6 + .../ceph/client/erasure-code-profile_test.go | 28 +++- 6 files changed, 336 insertions(+), 7 deletions(-) diff --git a/Documentation/CRDs/specification.md b/Documentation/CRDs/specification.md index 00bbf510e..647839ace 100644 --- a/Documentation/CRDs/specification.md +++ b/Documentation/CRDs/specification.md @@ -7199,6 +7199,34 @@ k8s.io/apimachinery/pkg/api/resource.Quantity Value must be a multiple of 4096 (4Ki).

+ + +crushNumFailureDomains
+ +int32 + + + +(Optional) +

Number of failure domains to use for erasure coded chunk placement. +When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created +that distributes chunks across this many failure domains.

+ + + + +crushOSDsPerFailureDomain
+ +int32 + + + +(Optional) +

Number of OSDs allowed per failure domain for erasure coded chunk placement. +When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created +that allows up to this many chunks on OSDs within each failure domain.

+ +

ExternalSpec diff --git a/deploy/charts/rook-ceph/templates/resources.yaml b/deploy/charts/rook-ceph/templates/resources.yaml index 936f9f6ab..ac71a150f 100644 --- a/deploy/charts/rook-ceph/templates/resources.yaml +++ b/deploy/charts/rook-ceph/templates/resources.yaml @@ -468,6 +468,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -494,6 +510,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -7790,6 +7809,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -7816,6 +7851,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -8000,6 +8038,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -8026,6 +8080,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -12954,6 +13011,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -12980,6 +13053,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -14611,6 +14687,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -14637,6 +14729,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -15704,6 +15799,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -15730,6 +15841,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -15909,6 +16023,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -15935,6 +16065,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string diff --git a/deploy/examples/crds.yaml b/deploy/examples/crds.yaml index a1efdc324..b5caea65f 100644 --- a/deploy/examples/crds.yaml +++ b/deploy/examples/crds.yaml @@ -470,6 +470,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -496,6 +512,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -7785,6 +7804,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -7811,6 +7846,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -7995,6 +8033,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -8021,6 +8075,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -12943,6 +13000,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -12969,6 +13042,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -14600,6 +14676,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -14626,6 +14718,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -15690,6 +15785,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -15716,6 +15827,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string @@ -15895,6 +16009,22 @@ spec: This is the number of OSDs that can be lost simultaneously before data cannot be recovered. minimum: 0 type: integer + crushNumFailureDomains: + description: |- + Number of failure domains to use for erasure coded chunk placement. + When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + that distributes chunks across this many failure domains. + format: int32 + minimum: 1 + type: integer + crushOSDsPerFailureDomain: + description: |- + Number of OSDs allowed per failure domain for erasure coded chunk placement. + When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + that allows up to this many chunks on OSDs within each failure domain. + format: int32 + minimum: 1 + type: integer dataChunks: description: |- Number of data chunks per object in an erasure coded storage pool (required for erasure-coded pool type). @@ -15921,6 +16051,9 @@ spec: - codingChunks - dataChunks type: object + x-kubernetes-validations: + - message: crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together + rule: has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain) failureDomain: description: 'The failure domain: osd/host/(region or zone if available) - technically also any type in the crush map' type: string diff --git a/pkg/apis/ceph.rook.io/v1/types.go b/pkg/apis/ceph.rook.io/v1/types.go index 067f2c9c6..583e9cd6a 100755 --- a/pkg/apis/ceph.rook.io/v1/types.go +++ b/pkg/apis/ceph.rook.io/v1/types.go @@ -1434,6 +1434,7 @@ type QuotaSpec struct { } // ErasureCodedSpec represents the spec for erasure code in a pool +// +kubebuilder:validation:XValidation:message="crushNumFailureDomains and crushOSDsPerFailureDomain must be specified together",rule="has(self.crushNumFailureDomains) == has(self.crushOSDsPerFailureDomain)" type ErasureCodedSpec struct { // Number of coding chunks per object in an erasure coded storage pool (required for erasure-coded pool type). // This is the number of OSDs that can be lost simultaneously before data cannot be recovered. @@ -1457,6 +1458,20 @@ type ErasureCodedSpec struct { // +kubebuilder:validation:Enum={"4Ki","16Ki","64Ki","256Ki","1Mi"} // +optional StripeUnit *resource.Quantity `json:"stripeUnit,omitempty"` + + // Number of failure domains to use for erasure coded chunk placement. + // When specified along with crushOSDsPerFailureDomain, a CRUSH MSR rule will be created + // that distributes chunks across this many failure domains. + // +kubebuilder:validation:Minimum=1 + // +optional + CrushNumFailureDomains int32 `json:"crushNumFailureDomains,omitempty"` + + // Number of OSDs allowed per failure domain for erasure coded chunk placement. + // When specified along with crushNumFailureDomains, a CRUSH MSR rule will be created + // that allows up to this many chunks on OSDs within each failure domain. + // +kubebuilder:validation:Minimum=1 + // +optional + CrushOSDsPerFailureDomain int32 `json:"crushOSDsPerFailureDomain,omitempty"` } // +genclient diff --git a/pkg/daemon/ceph/client/erasure-code-profile.go b/pkg/daemon/ceph/client/erasure-code-profile.go index b6f2cb130..74495d892 100644 --- a/pkg/daemon/ceph/client/erasure-code-profile.go +++ b/pkg/daemon/ceph/client/erasure-code-profile.go @@ -92,6 +92,12 @@ func CreateErasureCodeProfile(context *clusterd.Context, clusterInfo *ClusterInf if pool.DeviceClass != "" { profilePairs = append(profilePairs, fmt.Sprintf("crush-device-class=%s", pool.DeviceClass)) } + if pool.ErasureCoded.CrushNumFailureDomains > 0 { + profilePairs = append(profilePairs, fmt.Sprintf("crush-num-failure-domains=%d", pool.ErasureCoded.CrushNumFailureDomains)) + } + if pool.ErasureCoded.CrushOSDsPerFailureDomain > 0 { + profilePairs = append(profilePairs, fmt.Sprintf("crush-osds-per-failure-domain=%d", pool.ErasureCoded.CrushOSDsPerFailureDomain)) + } if pool.ErasureCoded.StripeUnit != nil && !pool.ErasureCoded.StripeUnit.IsZero() { stripeBytes, ok := pool.ErasureCoded.StripeUnit.AsInt64() if !ok { diff --git a/pkg/daemon/ceph/client/erasure-code-profile_test.go b/pkg/daemon/ceph/client/erasure-code-profile_test.go index 4b50a64c4..194b85f80 100644 --- a/pkg/daemon/ceph/client/erasure-code-profile_test.go +++ b/pkg/daemon/ceph/client/erasure-code-profile_test.go @@ -29,27 +29,33 @@ import ( ) func TestCreateProfile(t *testing.T) { - testCreateProfile(t, "", "myroot", "") + testCreateProfile(t, "", "myroot", "", 0, 0) } func TestCreateProfileWithFailureDomain(t *testing.T) { - testCreateProfile(t, "osd", "", "") + testCreateProfile(t, "osd", "", "", 0, 0) } func TestCreateProfileWithDeviceClass(t *testing.T) { - testCreateProfile(t, "osd", "", "hdd") + testCreateProfile(t, "osd", "", "hdd", 0, 0) } -func testCreateProfile(t *testing.T, failureDomain, crushRoot, deviceClass string) { +func TestCreateProfileWithMSR(t *testing.T) { + testCreateProfile(t, "host", "", "", 5, 3) +} + +func testCreateProfile(t *testing.T, failureDomain, crushRoot, deviceClass string, failureDomains, osdsPerDomain int32) { stripeUnit := resource.MustParse("4Ki") spec := cephv1.PoolSpec{ FailureDomain: failureDomain, CrushRoot: crushRoot, DeviceClass: deviceClass, ErasureCoded: cephv1.ErasureCodedSpec{ - DataChunks: 2, - CodingChunks: 3, - StripeUnit: &stripeUnit, + DataChunks: 2, + CodingChunks: 3, + StripeUnit: &stripeUnit, + CrushNumFailureDomains: failureDomains, + CrushOSDsPerFailureDomain: osdsPerDomain, }, } @@ -82,6 +88,14 @@ func testCreateProfile(t *testing.T, failureDomain, crushRoot, deviceClass strin assert.Equal(t, fmt.Sprintf("crush-device-class=%s", deviceClass), args[nextArg]) nextArg++ } + if failureDomains > 0 { + assert.Equal(t, fmt.Sprintf("crush-num-failure-domains=%d", failureDomains), args[nextArg]) + nextArg++ + } + if osdsPerDomain > 0 { + assert.Equal(t, fmt.Sprintf("crush-osds-per-failure-domain=%d", osdsPerDomain), args[nextArg]) + nextArg++ + } if spec.ErasureCoded.StripeUnit != nil && !spec.ErasureCoded.StripeUnit.IsZero() { stripeBytes, ok := spec.ErasureCoded.StripeUnit.AsInt64() assert.True(t, ok)