Skip to content

Commit ccacbc3

Browse files
committed
e2e: add VolumeAttributesClass example and e2e test
Add a VolumeAttributesClass example for NVMe-oF volumes demonstrating the mutable parameters supported by the driver: - Host access control via allowHostNQNs parameter (for external hosts) - QoS parameters The example includes a working configuration with host access control enabled, and all optional parameters documented as comments with explanations. Also add an e2e test that validates VAC functionality by: 1. Creating a VAC with allowHostNQNs parameter 2. Creating a PVC that references the VAC 3. Verifying volume creation 4. Validating cleanup Signed-off-by: gadi-didi <gadi.didi@ibm.com>
1 parent 734c5b1 commit ccacbc3

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

e2e/nvmeof-deploy.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222
"fmt"
23+
"maps"
2324
"strings"
2425
"time"
2526

@@ -29,6 +30,7 @@ import (
2930
apierrs "k8s.io/apimachinery/pkg/api/errors"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3132
"k8s.io/apimachinery/pkg/util/wait"
33+
"k8s.io/client-go/kubernetes"
3234
"k8s.io/kubernetes/test/e2e/framework"
3335
)
3436

@@ -242,3 +244,73 @@ func createNVMeoFCredentials(f *framework.Framework) {
242244
err = createRBDSecret(f, nvmeofNodePluginSecretName, nvmeofKeyringNodePluginUsername, key)
243245
Expect(err).ShouldNot(HaveOccurred())
244246
}
247+
248+
func createNVMeOFVolumeAttributesClass(
249+
c kubernetes.Interface,
250+
name string,
251+
params map[string]string,
252+
) error {
253+
vac, err := getVolumeAttributesClass(nvmeofExamplePath + "volumeattributesclass.yaml")
254+
if err != nil {
255+
return fmt.Errorf("failed to get vac: %w", err)
256+
}
257+
if name != "" {
258+
vac.Name = name
259+
}
260+
261+
// overload any parameters that were passed
262+
if params == nil {
263+
// create an empty params, so that params["clusterID"] below
264+
// does not panic
265+
params = map[string]string{}
266+
}
267+
maps.Copy(vac.Parameters, params)
268+
269+
timeout := time.Duration(deployTimeout) * time.Minute
270+
271+
return wait.PollUntilContextTimeout(context.TODO(), poll, timeout, true, func(ctx context.Context) (bool, error) {
272+
_, err = c.StorageV1().VolumeAttributesClasses().Create(ctx, &vac, metav1.CreateOptions{})
273+
if err != nil {
274+
if apierrs.IsAlreadyExists(err) {
275+
return true, nil
276+
}
277+
if isRetryableAPIError(err) {
278+
return false, nil
279+
}
280+
framework.Logf("error creating VolumeAttributesClass %q: %v", vac.Name, err)
281+
282+
return false, fmt.Errorf("failed to create VolumeAttributesClass %q: %w", vac.Name, err)
283+
}
284+
285+
return true, nil
286+
})
287+
}
288+
289+
func deleteNVMeOFVolumeAttributesClass(c kubernetes.Interface, name string) error {
290+
vac, err := getVolumeAttributesClass(nvmeofExamplePath + "volumeattributesclass.yaml")
291+
if err != nil {
292+
return err
293+
}
294+
if name != "" {
295+
vac.Name = name
296+
}
297+
298+
timeout := time.Duration(deployTimeout) * time.Minute
299+
300+
return wait.PollUntilContextTimeout(context.TODO(), poll, timeout, true, func(ctx context.Context) (bool, error) {
301+
err = c.StorageV1().VolumeAttributesClasses().Delete(ctx, vac.Name, metav1.DeleteOptions{})
302+
if err != nil {
303+
if apierrs.IsNotFound(err) {
304+
return true, nil
305+
}
306+
if isRetryableAPIError(err) {
307+
return false, nil
308+
}
309+
framework.Logf("error deleting VolumeAttributesClass %q: %v", vac.Name, err)
310+
311+
return false, fmt.Errorf("failed to delete VolumeAttributesClass %q: %w", vac.Name, err)
312+
}
313+
314+
return true, nil
315+
})
316+
}

e2e/nvmeof.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,5 +311,100 @@ var _ = ginkgo.Describe("nvmeof", func() {
311311
framework.Logf("GroupLock Pods-only test passed: %d Pods created and deleted with rapid Group A/B switching",
312312
totalCount)
313313
})
314+
315+
ginkgo.It("create a PVC with VAC containing allowHostNQNs, apply new VAC then delete the PVC", func() {
316+
317+
// Vac test requires Kubernetes 1.34+ for VolumeAttributesClass support
318+
if !k8sVersionGreaterEquals(f.ClientSet, 1, 34) {
319+
framework.Logf("skipping VolumeAttributesClass test, needs Kubernetes >= 1.34")
320+
321+
return
322+
}
323+
324+
// This test validates VolumeAttributesClass (VAC) support for NVMe-oF volumes
325+
// with host access control via the allowHostNQNs parameter.
326+
//
327+
// Test flow:
328+
// 1. Create a VAC with allowHostNQNs parameter containing a list of allowed host NQNs
329+
// 2. Create a PVC that references the VAC via spec.volumeAttributesClassName
330+
// 3. Verify the volume is created and the host access control is applied
331+
// 4. Update the PVC to reference a different VAC and verify the update is applied
332+
// 5. Delete the PVC and verify cleanup
333+
//
334+
// This tests the ControllerModifyVolume functionality in the NVMe-oF driver,
335+
// ensuring that host access lists can be configured via VAC at volume creation time.
336+
337+
// Step 1: Create 2 VACs with allowHostNQNs parameter
338+
vacName1 := "e2e-" + f.UniqueName + "-vac1"
339+
vacName2 := "e2e-" + f.UniqueName + "-vac2"
340+
hostListParam := map[string]string{
341+
"allowHostNQNs": `- nqn.2014-08.org.nvmexpress:test-host-1
342+
- nqn.2014-08.org.nvmexpress:test-host-2
343+
- nqn.2014-08.org.nvmexpress:test-host-3`,
344+
}
345+
hostListParam2 := map[string]string{
346+
"allowHostNQNs": `- nqn.2014-08.org.nvmexpress:test-host-1
347+
- nqn.2014-08.org.nvmexpress:test-host-5`,
348+
}
349+
ginkgo.By("Creating VolumeAttributesClass with allowHostNQNs")
350+
err := createNVMeOFVolumeAttributesClass(f.ClientSet, vacName1, hostListParam)
351+
Expect(err).ShouldNot(HaveOccurred())
352+
defer func() {
353+
err = deleteNVMeOFVolumeAttributesClass(f.ClientSet, vacName1)
354+
if err != nil {
355+
logAndFail("failed to delete volumeattributesclass: %v", err)
356+
}
357+
}()
358+
359+
ginkgo.By("Creating VolumeAttributesClass with allowHostNQNs")
360+
err = createNVMeOFVolumeAttributesClass(f.ClientSet, vacName2, hostListParam2)
361+
Expect(err).ShouldNot(HaveOccurred())
362+
defer func() {
363+
err = deleteNVMeOFVolumeAttributesClass(f.ClientSet, vacName2)
364+
if err != nil {
365+
logAndFail("failed to delete volumeattributesclass: %v", err)
366+
}
367+
}()
368+
369+
// Step 2: Create PVC with reference to the VAC1
370+
pvc, err := loadPVC(pvcPath)
371+
Expect(err).ShouldNot(HaveOccurred())
372+
373+
pvc.Namespace = f.UniqueName
374+
pvc.Spec.StorageClassName = &nvmeofStorageClass
375+
pvc.Spec.VolumeAttributesClassName = &vacName1
376+
377+
ginkgo.By("Creating PVC with VolumeAttributesClass reference")
378+
err = createPVCAndvalidatePV(f.ClientSet, pvc, deployTimeout)
379+
Expect(err).ShouldNot(HaveOccurred())
380+
381+
pv, err := getBoundPV(f.ClientSet, pvc)
382+
Expect(err).ShouldNot(HaveOccurred())
383+
Expect(pv.Spec.VolumeAttributesClassName).NotTo(BeNil())
384+
Expect(*pv.Spec.VolumeAttributesClassName).To(Equal(vacName1))
385+
386+
validateRBDImageCount(f, 1, nvmeofPool)
387+
validateOmapCount(f, 1, rbdType, nvmeofPool, volumesType)
388+
389+
// Step 3: Update PVC to reference VAC2 and verify the update is applied
390+
ginkgo.By("Updating PVC to reference a different VolumeAttributesClass")
391+
392+
err = modifyPVCVolumeAttributesClass(
393+
f.ClientSet,
394+
pvc,
395+
vacName2)
396+
if err != nil {
397+
logAndFail("failed to modify volumeattributesclass: %v", err)
398+
}
399+
400+
// Step 3: Delete PVC
401+
ginkgo.By("Deleting PVC with VAC")
402+
err = deletePVCAndValidatePV(f.ClientSet, pvc, deployTimeout)
403+
Expect(err).ShouldNot(HaveOccurred())
404+
405+
// validate created backend rbd images are deleted
406+
validateRBDImageCount(f, 0, nvmeofPool)
407+
validateOmapCount(f, 0, rbdType, nvmeofPool, volumesType)
408+
})
314409
})
315410
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
# VolumeAttributesClass for NVMe-oF volumes
3+
# This example demonstrates optional mutable parameters for NVMe-oF
4+
apiVersion: storage.k8s.io/v1
5+
kind: VolumeAttributesClass
6+
metadata:
7+
name: nvmeof-vac
8+
driverName: nvmeof.csi.ceph.com
9+
parameters:
10+
# Host access control: YAML list of host NQNs allowed to access this volume
11+
# Format: nqn.<date>.<reverse-domain>:<user-part>
12+
# For Ceph-CSI managed hosts: nqn.2014-08.org.nvmexpress:<nodeID>
13+
allowHostNQNs: |
14+
- nqn.2014-08.org.nvmexpress:host1
15+
- nqn.2014-08.org.nvmexpress:host2
16+
- nqn.2014-08.org.nvmexpress:node-123
17+
18+
# Alternative: Allow any host to access the volume
19+
# allowHostNQNs: |
20+
# - "*"
21+
22+
# Alternative: Empty list denies all hosts
23+
# allowHostNQNs: |
24+
25+
# Optional QoS parameters (uint64 values):
26+
27+
# Limit read/write IOPS (operations per second for both read and write)
28+
# nvmeofRWIOsPerSecond: "10000"
29+
30+
# Limit read/write throughput in MB/s (for both read and write)
31+
# nvmeofRWMegabytesPerSecond: "100"
32+
33+
# Limit read-only IOPS (operations per second for read only)
34+
# nvmeofRIOsPerSecond: "5000"
35+
36+
# Limit write-only IOPS (operations per second for write only)
37+
# nvmeofWIOsPerSecond: "5000"
38+
39+
# Limit read-only throughput in MB/s
40+
# nvmeofRMegabytesPerSecond: "50"
41+
42+
# Limit write-only throughput in MB/s
43+
# nvmeofWMegabytesPerSecond: "50"
44+
45+
# Notes:
46+
# - Cannot mix RBD QoS parameters with NVMe-oF QoS parameters
47+
# - VolumeAttributesClass can be modified at runtime using kubectl
48+
# - Changes are applied dynamically via ControllerModifyVolume

0 commit comments

Comments
 (0)