Skip to content

Commit 374d0f3

Browse files
authored
chore(vm): hotplug memory e2e test (#2281)
- Add udev rule to bring online hotplugged memory "sticks". Signed-off-by: Ivan Mikheykin <ivan.mikheykin@flant.com>
1 parent fcffcc6 commit 374d0f3

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

test/e2e/internal/object/const.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ var HotplugCPUUdevRule = WriteFile{
8888
Owner: "root:root",
8989
}
9090

91+
var HotplugMemoryUdevRule = WriteFile{
92+
Path: "/etc/udev/rules.d/99-hotplug-memory.rules",
93+
Content: `SUBSYSTEM=="memory",ACTION=="add",DEVPATH=="/devices/system/memory/memory[0-9]*", TEST=="state", ATTR{state}!="online", ATTR{state}="online"`,
94+
Owner: "root:root",
95+
}
96+
9197
var AlpineCloudInit = CloudConfig{
9298
PackageUpdate: true,
9399
Packages: append(basePackages, "eudev", "iputils"),
@@ -98,6 +104,7 @@ var AlpineCloudInit = CloudConfig{
98104
},
99105
WriteFiles: []WriteFile{
100106
HotplugCPUUdevRule,
107+
HotplugMemoryUdevRule,
101108
},
102109
}.Render()
103110

test/e2e/vm/hotplug_memory.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
Copyright 2026 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package vm
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"regexp"
24+
"strconv"
25+
"strings"
26+
27+
. "github.com/onsi/ginkgo/v2"
28+
. "github.com/onsi/gomega"
29+
"k8s.io/apimachinery/pkg/api/resource"
30+
"k8s.io/apimachinery/pkg/types"
31+
"k8s.io/utils/ptr"
32+
crclient "sigs.k8s.io/controller-runtime/pkg/client"
33+
34+
vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd"
35+
vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm"
36+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
37+
"github.com/deckhouse/virtualization/test/e2e/internal/framework"
38+
"github.com/deckhouse/virtualization/test/e2e/internal/object"
39+
"github.com/deckhouse/virtualization/test/e2e/internal/util"
40+
)
41+
42+
var _ = Describe("HotplugMemory", func() {
43+
var (
44+
f *framework.Framework
45+
t *memoryHotplugTest
46+
)
47+
48+
BeforeEach(func() {
49+
Skip("Hotplug memory requires enabling feature gate 'HotplugMemoryWithLiveMigration' in ModuleConfig. Skip until prechecks are implemented.")
50+
f = framework.NewFramework("memory-hotplug")
51+
DeferCleanup(f.After)
52+
f.Before()
53+
t = newMemoryHotplugTest(f)
54+
})
55+
56+
DescribeTable("should apply memory changes with live migration", func(initialMemory, changedMemory string) {
57+
initialQuantity := resource.MustParse(initialMemory)
58+
changedQuantity := resource.MustParse(changedMemory)
59+
60+
By("Environment preparation")
61+
vmName := strings.ToLower(fmt.Sprintf("vm-%s-%s", initialMemory, changedMemory))
62+
t.generateResources(vmName, initialMemory)
63+
err := f.CreateWithDeferredDeletion(context.Background(), t.VM, t.VD)
64+
Expect(err).NotTo(HaveOccurred())
65+
66+
By("Waiting for VM agent to be ready")
67+
util.UntilSSHReady(f, t.VM, framework.MiddleTimeout)
68+
69+
By("Checking initial memory size")
70+
err = f.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VM), t.VM)
71+
Expect(err).NotTo(HaveOccurred())
72+
Expect(t.VM.Status.Resources.Memory.Size).To(Equal(initialQuantity))
73+
74+
guestMemorySize, err := t.getGuestMemorySize()
75+
Expect(err).NotTo(HaveOccurred())
76+
Expect(guestMemorySize).To(Equal(int(initialQuantity.Value())))
77+
78+
By("Applying memory size changes")
79+
patch, err := json.Marshal([]map[string]interface{}{{
80+
"op": "replace",
81+
"path": "/spec/memory/size",
82+
"value": changedMemory,
83+
}})
84+
Expect(err).NotTo(HaveOccurred())
85+
err = f.GenericClient().Patch(context.Background(), t.VM, crclient.RawPatch(types.JSONPatchType, patch))
86+
Expect(err).NotTo(HaveOccurred())
87+
88+
By("Waiting until memory size is applied without restart")
89+
util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(t.VM), framework.MaxTimeout)
90+
util.UntilSSHReady(f, t.VM, framework.MiddleTimeout)
91+
92+
By("Checking changed memory size")
93+
err = f.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VM), t.VM)
94+
Expect(err).NotTo(HaveOccurred())
95+
Expect(t.VM.Status.Resources.Memory.Size).To(Equal(changedQuantity))
96+
97+
guestMemorySize, err = t.getGuestMemorySize()
98+
Expect(err).NotTo(HaveOccurred())
99+
Expect(guestMemorySize).To(Equal(int(changedQuantity.Value())))
100+
},
101+
Entry("change memory from 1Gi to 2Gi", "1Gi", "2Gi"),
102+
)
103+
})
104+
105+
type memoryHotplugTest struct {
106+
Framework *framework.Framework
107+
108+
VM *v1alpha2.VirtualMachine
109+
VD *v1alpha2.VirtualDisk
110+
}
111+
112+
func newMemoryHotplugTest(f *framework.Framework) *memoryHotplugTest {
113+
return &memoryHotplugTest{Framework: f}
114+
}
115+
116+
func (t *memoryHotplugTest) generateResources(vmName, memSize string) {
117+
memSizeQuantity := resource.MustParse(memSize)
118+
119+
vdName := fmt.Sprintf("vd-%s-root", vmName)
120+
t.VD = object.NewVDFromCVI(vdName, t.Framework.Namespace().Name, object.PrecreatedCVIAlpineBIOS,
121+
vdbuilder.WithSize(ptr.To(resource.MustParse("350Mi"))),
122+
)
123+
124+
t.VM = vmbuilder.New(
125+
vmbuilder.WithName(vmName),
126+
vmbuilder.WithNamespace(t.Framework.Namespace().Name),
127+
vmbuilder.WithCPU(2, ptr.To("10%")),
128+
vmbuilder.WithMemory(memSizeQuantity),
129+
vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy),
130+
vmbuilder.WithProvisioningUserData(object.AlpineCloudInit),
131+
vmbuilder.WithBlockDeviceRefs(
132+
v1alpha2.BlockDeviceSpecRef{
133+
Kind: v1alpha2.DiskDevice,
134+
Name: t.VD.Name,
135+
},
136+
),
137+
vmbuilder.WithRestartApprovalMode(v1alpha2.Automatic),
138+
)
139+
}
140+
141+
var totalOnlineMemRe = regexp.MustCompile(`^Total online memory:\s+(\d+)$`)
142+
143+
func (t *memoryHotplugTest) getGuestMemorySize() (int, error) {
144+
cmdOut, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, "lsmem -b --summary=only")
145+
if err != nil {
146+
return 0, err
147+
}
148+
149+
lines := strings.Split(cmdOut, "\n")
150+
151+
for _, line := range lines {
152+
matches := totalOnlineMemRe.FindStringSubmatch(line)
153+
if len(matches) >= 2 {
154+
return strconv.Atoi(matches[1])
155+
}
156+
}
157+
158+
return 0, fmt.Errorf("failed to find total online memory in lsmem output: %v", cmdOut)
159+
}

0 commit comments

Comments
 (0)