Skip to content

Commit 21a862d

Browse files
committed
Add e2e test that checks PreferredLinkTarget is updated
Update LocalVolume + LocalVolumeSets with a check for PreferredLinkTarget update. The check picks one PV / LVDL, on the corresponding node adds a new symlink to the corresponding device that's more preferred than the current one, and expects that the LVDL gets updated. Then it removes the symlink + checks the old PreferredLinkTarget is restored. The test expects the test disks are NVME. It creates a symlink `scsi-1`, which is more preferable by LSO. I also added some common functionality to add / remove symlinks easily and updated newNodeJob to get hostname instead of the whole Node object - it saves one API call to get a Node from hostname label from PV nodeAffinity.
1 parent c85e33a commit 21a862d

7 files changed

Lines changed: 201 additions & 10 deletions

File tree

test/e2e/cleanup_hostdirs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ set +x
5656
`
5757
nodeName, _ := node.Labels[corev1.LabelHostname]
5858
return newNodeJob(
59-
node,
59+
nodeName,
6060
namespace,
6161
fmt.Sprintf("cleanup-%s", nodeName),
6262
"cleans up the hostdir artificats on the node following functional tests",

test/e2e/gomega_util.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/onsi/gomega"
11+
localv1 "github.com/openshift/local-storage-operator/api/v1"
1112
framework "github.com/openshift/local-storage-operator/test/framework"
1213
batchv1 "k8s.io/api/batch/v1"
1314
corev1 "k8s.io/api/core/v1"
@@ -278,3 +279,16 @@ func consumePV(t *testing.T, ctx *framework.Context, pv corev1.PersistentVolume)
278279
matchingPod.TypeMeta.Kind = "Pod"
279280
return pvc, job, &matchingPod
280281
}
282+
283+
func waitForLVDLContent(t *testing.T, f *framework.Framework, namespace, lvdlName string, message string, check func(lvdl *localv1.LocalVolumeDeviceLink) error) {
284+
matcher := gomega.NewWithT(t)
285+
matcher.Eventually(func() error {
286+
lvdl := &localv1.LocalVolumeDeviceLink{}
287+
if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Name: lvdlName, Namespace: namespace}, lvdl); err != nil {
288+
t.Logf("error getting LVDL %q: %v", lvdlName, err)
289+
return err
290+
}
291+
return check(lvdl)
292+
}, 30*time.Minute, time.Second*2).ShouldNot(gomega.HaveOccurred(), message)
293+
// TODO: lower the timeout above, using 30 minutes to have time for debugging
294+
}

test/e2e/hostudev.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package e2e
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/onsi/gomega"
8+
framework "github.com/openshift/local-storage-operator/test/framework"
9+
batchv1 "k8s.io/api/batch/v1"
10+
corev1 "k8s.io/api/core/v1"
11+
)
12+
13+
// addNewUdevSymlink adds a new udev symlink on the node. The link will point to the same device as the currentLink.
14+
func addNewUdevSymlink(t *testing.T, ctx *framework.TestCtx, nodeHostname string, currentLink, newLink string) {
15+
t.Logf("adding new udev symlink %s -> %s on node %s", newLink, currentLink, nodeHostname)
16+
matcher := gomega.NewWithT(t)
17+
18+
namespace, err := ctx.GetOperatorNamespace()
19+
matcher.Expect(err).NotTo(gomega.HaveOccurred(), "could not determine namespace")
20+
21+
symlinkJob, err := newAddUdevSymlinkJob(nodeHostname, namespace, currentLink, newLink)
22+
matcher.Expect(err).NotTo(gomega.HaveOccurred(), "could not create symlink job")
23+
createOrReplaceJob(t, ctx, symlinkJob, fmt.Sprintf("creating symlink job on node: %q", nodeHostname))
24+
25+
waitForJobCompletion(t, symlinkJob, fmt.Sprintf("waiting for check-symlink job to complete: %q", symlinkJob.GetName()))
26+
symlinkJob.TypeMeta.Kind = "Job"
27+
eventuallyDelete(t, false, symlinkJob)
28+
t.Logf("added udev symlink %s -> %s on node %s", newLink, currentLink, nodeHostname)
29+
}
30+
31+
// removeUdevSymlink removes a udev symlink on the node. It literally calls `rm -f $linkPattern` (in the root directory),
32+
// so it can be used to remove multiple symlinks at once.
33+
func removeUdevSymlink(t *testing.T, ctx *framework.TestCtx, nodeHostname string, linkPattern string) {
34+
t.Logf("removing udev symlinks matching %s on node %s", linkPattern, nodeHostname)
35+
matcher := gomega.NewWithT(t)
36+
37+
namespace, err := ctx.GetOperatorNamespace()
38+
matcher.Expect(err).NotTo(gomega.HaveOccurred(), "could not determine namespace")
39+
40+
symlinkJob, err := newRemoveUdevSymlinkJobs(nodeHostname, namespace, linkPattern)
41+
matcher.Expect(err).NotTo(gomega.HaveOccurred(), "could not create symlink job")
42+
createOrReplaceJob(t, ctx, symlinkJob, fmt.Sprintf("creating symlink job on node: %q", nodeHostname))
43+
44+
waitForJobCompletion(t, symlinkJob, fmt.Sprintf("waiting for check-symlink job to complete: %q", symlinkJob.GetName()))
45+
symlinkJob.TypeMeta.Kind = "Job"
46+
eventuallyDelete(t, false, symlinkJob)
47+
t.Logf("removed udev symlinks matching %s on node %s", linkPattern, nodeHostname)
48+
}
49+
50+
func newAddUdevSymlinkJob(nodeHostname, namespace, currentUdevLink, newLink string) (*batchv1.Job, error) {
51+
script := `
52+
set -eu
53+
set -x
54+
55+
echo "adding new udev symlink $CURRENT_LINK -> $NEW_LINK"
56+
if [[ ! -L "$CURRENT_LINK" ]]; then
57+
echo "not a symlink: $CURRENT_LINK"
58+
exit 1
59+
fi
60+
61+
DEVICE=$(readlink -f "$CURRENT_LINK")
62+
ln -sfv "$DEVICE" "$NEW_LINK"
63+
`
64+
return newNodeJob(
65+
nodeHostname,
66+
namespace,
67+
fmt.Sprintf("add-udev-symlink-job-%s", nodeHostname),
68+
"adds a new udev symlink",
69+
[]string{"/bin/bash", "-c", script},
70+
&NodeJobOptions{
71+
ContainerRestartPolicy: corev1.RestartPolicyNever,
72+
Env: []corev1.EnvVar{
73+
{Name: "CURRENT_LINK", Value: currentUdevLink},
74+
{Name: "NEW_LINK", Value: newLink},
75+
},
76+
})
77+
78+
}
79+
80+
func newRemoveUdevSymlinkJobs(nodeHostname, namespace, pattern string) (*batchv1.Job, error) {
81+
script := `
82+
set -eu
83+
set -x
84+
85+
ls -la "$PATTERN"
86+
rm -fv "$PATTERN"
87+
`
88+
return newNodeJob(
89+
nodeHostname,
90+
namespace,
91+
fmt.Sprintf("remove-udev-symlink-job-%s", nodeHostname),
92+
"removes udev symlinks matching the pattern",
93+
[]string{"/bin/bash", "-c", script},
94+
&NodeJobOptions{
95+
ContainerRestartPolicy: corev1.RestartPolicyNever,
96+
Env: []corev1.EnvVar{
97+
{Name: "PATTERN", Value: pattern},
98+
},
99+
})
100+
}

test/e2e/jobs.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,7 @@ type NodeJobOptions struct {
2828

2929
// newNodeJob returns a Job that runs the given command on the specified node.
3030
// The job uses the diskmaker image, mounts local-disks and /dev, and is pinned to the node via affinity.
31-
func newNodeJob(node corev1.Node, namespace, jobName, description string, command []string, opts *NodeJobOptions) (*batchv1.Job, error) {
32-
nodeName, found := node.Labels[corev1.LabelHostname]
33-
if !found {
34-
return nil, fmt.Errorf("could not get %q label for node: %q", corev1.LabelHostname, node.GetName())
35-
}
36-
31+
func newNodeJob(nodeHostname, namespace, jobName, description string, command []string, opts *NodeJobOptions) (*batchv1.Job, error) {
3732
hostContainerPropagation := corev1.MountPropagationHostToContainer
3833
directoryHostPath := corev1.HostPathDirectory
3934
volumes := []corev1.Volume{
@@ -95,7 +90,7 @@ func newNodeJob(node corev1.Node, namespace, jobName, description string, comman
9590
{
9691
Key: corev1.LabelHostname,
9792
Operator: corev1.NodeSelectorOpIn,
98-
Values: []string{nodeName},
93+
Values: []string{nodeHostname},
9994
},
10095
},
10196
},
@@ -124,7 +119,7 @@ func newNodeJob(node corev1.Node, namespace, jobName, description string, comman
124119
Name: jobName,
125120
Namespace: namespace,
126121
Labels: map[string]string{
127-
corev1.LabelHostname: nodeName,
122+
corev1.LabelHostname: nodeHostname,
128123
"app": jobName,
129124
},
130125
Annotations: map[string]string{

test/e2e/localvolume_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func LocalVolumeTest(ctx *framework.Context, cleanupFuncs *[]cleanupFn) func(*te
163163
for _, pv := range pvs {
164164
pvNames = append(pvNames, pv.Name)
165165
}
166+
166167
t.Log("verifying LocalVolumeDeviceLink objects were created for PVs")
167168
lvdls := eventuallyFindLVDLsForPVs(t, f, namespace, pvNames)
168169
lvdlNames := make([]string, 0, len(lvdls))
@@ -173,6 +174,33 @@ func LocalVolumeTest(ctx *framework.Context, cleanupFuncs *[]cleanupFn) func(*te
173174
matcher.Expect(lvdl.Status.ValidLinkTargets).ToNot(gomega.BeEmpty(), "expected ValidLinkTargets for LVDL %q", lvdl.Name)
174175
}
175176

177+
// Use just the first LVDL for the test. Figure its node by inspecting the corresponding PV spec.nodeAffinity.
178+
lvdl := lvdls[0]
179+
nodeHostname := findNodeHostnameForLVDL(t, f, lvdl)
180+
t.Logf("using node %q for the test of LVDL %s", nodeHostname, lvdl.Name)
181+
// On AWS, the preferred link is usually "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*"
182+
// scsi-1 has a higher priority in dikutil.go and should become the new preferred target
183+
newPreferredTarget := "/dev/disk/by-id/scsi-1-local-storage-e2e-test"
184+
oldPreferredTarget := lvdl.Status.PreferredLinkTarget
185+
addNewUdevSymlink(t, ctx, nodeHostname, lvdl.Status.PreferredLinkTarget, newPreferredTarget)
186+
waitForLVDLContent(t, f, namespace, lvdl.Name, "waiting for LVDL to get new preferredLinkTarget", func(lvdl *localv1.LocalVolumeDeviceLink) error {
187+
if lvdl.Status.PreferredLinkTarget != newPreferredTarget {
188+
return fmt.Errorf("expected PreferredLinkTarget for LVDL %q to be updated, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
189+
}
190+
t.Logf("PreferredLinkTarget for LVDL %q is updated, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
191+
return nil
192+
})
193+
194+
removeUdevSymlink(t, ctx, nodeHostname, "/dev/disk/by-id/scsi-1-local-storage-e2e-test")
195+
196+
waitForLVDLContent(t, f, namespace, lvdl.Name, "waiting for LVDL to restore old preferredLinkTarget", func(lvdl *localv1.LocalVolumeDeviceLink) error {
197+
if lvdl.Status.PreferredLinkTarget != oldPreferredTarget {
198+
return fmt.Errorf("expected PreferredLinkTarget for LVDL %q to be restored, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
199+
}
200+
t.Logf("PreferredLinkTarget for LVDL %q is restored, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
201+
return nil
202+
})
203+
176204
// verify deletion
177205
for _, pv := range pvs {
178206
eventuallyDelete(t, false, &pv)
@@ -258,6 +286,33 @@ func LocalVolumeTest(ctx *framework.Context, cleanupFuncs *[]cleanupFn) func(*te
258286

259287
}
260288

289+
// findNodeHostnameForLVDL returns the node hostname where the LVDL's PV is scheduled,
290+
// by inspecting the PV's spec.nodeAffinity (Required, LabelHostname).
291+
func findNodeHostnameForLVDL(t *testing.T, f *framework.Framework, lvdl localv1.LocalVolumeDeviceLink) string {
292+
t.Helper()
293+
pvName := lvdl.Spec.PersistentVolumeName
294+
if pvName == "" {
295+
t.Fatalf("LVDL %q has no PersistentVolumeName", lvdl.Name)
296+
}
297+
pv := &corev1.PersistentVolume{}
298+
err := f.Client.Get(goctx.TODO(), types.NamespacedName{Name: pvName}, pv)
299+
if err != nil {
300+
t.Fatalf("expected to find PV %s for LVDL %q: %v", pvName, lvdl.Name, err)
301+
}
302+
if pv.Spec.NodeAffinity == nil || pv.Spec.NodeAffinity.Required == nil {
303+
t.Fatalf("PV %s has no NodeAffinity.Required", pvName)
304+
}
305+
for _, term := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms {
306+
for _, expr := range term.MatchExpressions {
307+
if expr.Key == corev1.LabelHostname && len(expr.Values) > 0 {
308+
return expr.Values[0]
309+
}
310+
}
311+
}
312+
t.Fatalf("PV %s NodeAffinity has no %q expression", pvName, corev1.LabelHostname)
313+
return ""
314+
}
315+
261316
func eventuallyFindLVDLsForPVs(t *testing.T, f *framework.Framework, namespace string, pvNames []string) []localv1.LocalVolumeDeviceLink {
262317
matcher := gomega.NewWithT(t)
263318
pvNameSet := sets.New(pvNames...)

test/e2e/localvolumeset_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,33 @@ func LocalVolumeSetTest(ctx *framework.TestCtx, cleanupFuncs *[]cleanupFn) func(
371371
matcher.Expect(lvdl.Status.ValidLinkTargets).ToNot(gomega.BeEmpty(), "expected ValidLinkTargets for LVDL %q", lvdl.Name)
372372
}
373373

374+
// Use just the first LVDL for the test. Figure its node by inspecting the corresponding PV spec.nodeAffinity.
375+
lvdl := fsLVDLs[0]
376+
nodeHostname := findNodeHostnameForLVDL(t, f, lvdl)
377+
t.Logf("using node %q for the test of LVDL %s", nodeHostname, lvdl.Name)
378+
// On AWS, the preferred link is usually "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*"
379+
// scsi-1 has a higher priority in dikutil.go and should become the new preferred target
380+
newPreferredTarget := "/dev/disk/by-id/scsi-1-local-storage-e2e-test"
381+
oldPreferredTarget := lvdl.Status.PreferredLinkTarget
382+
addNewUdevSymlink(t, ctx, nodeHostname, lvdl.Status.PreferredLinkTarget, newPreferredTarget)
383+
waitForLVDLContent(t, f, namespace, lvdl.Name, "waiting for LVDL to get new preferredLinkTarget", func(lvdl *localv1.LocalVolumeDeviceLink) error {
384+
if lvdl.Status.PreferredLinkTarget != newPreferredTarget {
385+
return fmt.Errorf("expected PreferredLinkTarget for LVDL %q to be updated, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
386+
}
387+
t.Logf("PreferredLinkTarget for LVDL %q is updated, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
388+
return nil
389+
})
390+
391+
removeUdevSymlink(t, ctx, nodeHostname, "/dev/disk/by-id/scsi-1-local-storage-e2e-test")
392+
393+
waitForLVDLContent(t, f, namespace, lvdl.Name, "waiting for LVDL to restore old preferredLinkTarget", func(lvdl *localv1.LocalVolumeDeviceLink) error {
394+
if lvdl.Status.PreferredLinkTarget != oldPreferredTarget {
395+
return fmt.Errorf("expected PreferredLinkTarget for LVDL %q to be restored, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
396+
}
397+
t.Logf("PreferredLinkTarget for LVDL %q is restored, got %q", lvdl.Name, lvdl.Status.PreferredLinkTarget)
398+
return nil
399+
})
400+
374401
// verify deletion
375402
t.Log("verify that filesystem PVs come back after deletion")
376403
for _, pv := range fsPVs {

test/e2e/symlink_check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ set +x
5656
nodeName, _ := node.Labels[corev1.LabelHostname]
5757
backoffLimit := int32(1)
5858
return newNodeJob(
59-
node,
59+
nodeName,
6060
namespace,
6161
fmt.Sprintf("symlink-check-%s", nodeName),
6262
"checks for leftover symlinks on the node following functional tests",

0 commit comments

Comments
 (0)