github.com/hernad/nomad@v1.6.112/drivers/qemu/driver_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package qemu 5 6 import ( 7 "context" 8 "io" 9 "os" 10 "path/filepath" 11 "testing" 12 "time" 13 14 "github.com/hernad/nomad/ci" 15 ctestutil "github.com/hernad/nomad/client/testutil" 16 "github.com/hernad/nomad/helper/pluginutils/hclutils" 17 "github.com/hernad/nomad/helper/testlog" 18 "github.com/hernad/nomad/helper/uuid" 19 "github.com/hernad/nomad/nomad/structs" 20 "github.com/hernad/nomad/plugins/drivers" 21 dtestutil "github.com/hernad/nomad/plugins/drivers/testutils" 22 "github.com/hernad/nomad/testutil" 23 "github.com/stretchr/testify/require" 24 ) 25 26 // TODO(preetha) - tests remaining 27 // using monitor socket for graceful shutdown 28 29 // Verifies starting a qemu image and stopping it 30 func TestQemuDriver_Start_Wait_Stop(t *testing.T) { 31 ci.Parallel(t) 32 ctestutil.QemuCompatible(t) 33 34 require := require.New(t) 35 ctx, cancel := context.WithCancel(context.Background()) 36 defer cancel() 37 38 d := NewQemuDriver(ctx, testlog.HCLogger(t)) 39 harness := dtestutil.NewDriverHarness(t, d) 40 41 task := &drivers.TaskConfig{ 42 ID: uuid.Generate(), 43 Name: "linux", 44 Resources: &drivers.Resources{ 45 NomadResources: &structs.AllocatedTaskResources{ 46 Memory: structs.AllocatedMemoryResources{ 47 MemoryMB: 512, 48 }, 49 Cpu: structs.AllocatedCpuResources{ 50 CpuShares: 100, 51 }, 52 Networks: []*structs.NetworkResource{ 53 { 54 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 55 }, 56 }, 57 }, 58 }, 59 } 60 61 tc := &TaskConfig{ 62 ImagePath: "linux-0.2.img", 63 Accelerator: "tcg", 64 GracefulShutdown: false, 65 PortMap: map[string]int{ 66 "main": 22, 67 "web": 8080, 68 }, 69 Args: []string{"-nodefconfig", "-nodefaults"}, 70 } 71 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 72 cleanup := harness.MkAllocDir(task, true) 73 defer cleanup() 74 75 taskDir := filepath.Join(task.AllocDir, task.Name) 76 77 copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t) 78 79 handle, _, err := harness.StartTask(task) 80 require.NoError(err) 81 82 require.NotNil(handle) 83 84 // Ensure that sending a Signal returns an error 85 err = d.SignalTask(task.ID, "SIGINT") 86 require.NotNil(err) 87 88 require.NoError(harness.DestroyTask(task.ID, true)) 89 90 } 91 92 // copyFile moves an existing file to the destination 93 func copyFile(src, dst string, t *testing.T) { 94 in, err := os.Open(src) 95 if err != nil { 96 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 97 } 98 defer in.Close() 99 out, err := os.Create(dst) 100 if err != nil { 101 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 102 } 103 defer func() { 104 if err := out.Close(); err != nil { 105 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 106 } 107 }() 108 if _, err = io.Copy(out, in); err != nil { 109 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 110 } 111 if err := out.Sync(); err != nil { 112 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 113 } 114 } 115 116 // Verifies starting a qemu image and stopping it 117 func TestQemuDriver_User(t *testing.T) { 118 ci.Parallel(t) 119 ctestutil.QemuCompatible(t) 120 121 require := require.New(t) 122 ctx, cancel := context.WithCancel(context.Background()) 123 defer cancel() 124 125 d := NewQemuDriver(ctx, testlog.HCLogger(t)) 126 harness := dtestutil.NewDriverHarness(t, d) 127 128 task := &drivers.TaskConfig{ 129 ID: uuid.Generate(), 130 Name: "linux", 131 User: "alice", 132 Resources: &drivers.Resources{ 133 NomadResources: &structs.AllocatedTaskResources{ 134 Memory: structs.AllocatedMemoryResources{ 135 MemoryMB: 512, 136 }, 137 Cpu: structs.AllocatedCpuResources{ 138 CpuShares: 100, 139 }, 140 Networks: []*structs.NetworkResource{ 141 { 142 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 143 }, 144 }, 145 }, 146 }, 147 } 148 149 tc := &TaskConfig{ 150 ImagePath: "linux-0.2.img", 151 Accelerator: "tcg", 152 GracefulShutdown: false, 153 PortMap: map[string]int{ 154 "main": 22, 155 "web": 8080, 156 }, 157 Args: []string{"-nodefconfig", "-nodefaults"}, 158 } 159 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 160 cleanup := harness.MkAllocDir(task, true) 161 defer cleanup() 162 163 taskDir := filepath.Join(task.AllocDir, task.Name) 164 165 copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t) 166 167 _, _, err := harness.StartTask(task) 168 require.Error(err) 169 require.Contains(err.Error(), "unknown user alice", err.Error()) 170 171 } 172 173 // Verifies getting resource usage stats 174 // 175 // TODO(preetha) this test needs random sleeps to pass 176 func TestQemuDriver_Stats(t *testing.T) { 177 ci.Parallel(t) 178 ctestutil.QemuCompatible(t) 179 180 require := require.New(t) 181 ctx, cancel := context.WithCancel(context.Background()) 182 defer cancel() 183 184 d := NewQemuDriver(ctx, testlog.HCLogger(t)) 185 harness := dtestutil.NewDriverHarness(t, d) 186 187 task := &drivers.TaskConfig{ 188 ID: uuid.Generate(), 189 Name: "linux", 190 Resources: &drivers.Resources{ 191 NomadResources: &structs.AllocatedTaskResources{ 192 Memory: structs.AllocatedMemoryResources{ 193 MemoryMB: 512, 194 }, 195 Cpu: structs.AllocatedCpuResources{ 196 CpuShares: 100, 197 }, 198 Networks: []*structs.NetworkResource{ 199 { 200 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 201 }, 202 }, 203 }, 204 }, 205 } 206 207 tc := &TaskConfig{ 208 ImagePath: "linux-0.2.img", 209 Accelerator: "tcg", 210 GracefulShutdown: false, 211 PortMap: map[string]int{ 212 "main": 22, 213 "web": 8080, 214 }, 215 Args: []string{"-nodefconfig", "-nodefaults"}, 216 } 217 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 218 cleanup := harness.MkAllocDir(task, true) 219 defer cleanup() 220 221 taskDir := filepath.Join(task.AllocDir, task.Name) 222 223 copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t) 224 225 handle, _, err := harness.StartTask(task) 226 require.NoError(err) 227 228 require.NotNil(handle) 229 230 // Wait for task to start 231 _, err = harness.WaitTask(context.Background(), handle.Config.ID) 232 require.NoError(err) 233 234 // Wait until task started 235 require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second)) 236 time.Sleep(30 * time.Second) 237 statsCtx, cancel := context.WithCancel(context.Background()) 238 defer cancel() 239 statsCh, err := harness.TaskStats(statsCtx, task.ID, time.Second*10) 240 require.NoError(err) 241 242 select { 243 case stats := <-statsCh: 244 t.Logf("CPU:%+v Memory:%+v\n", stats.ResourceUsage.CpuStats, stats.ResourceUsage.MemoryStats) 245 require.NotZero(stats.ResourceUsage.MemoryStats.RSS) 246 require.NoError(harness.DestroyTask(task.ID, true)) 247 case <-time.After(time.Second * 1): 248 require.Fail("timeout receiving from stats") 249 } 250 251 } 252 253 func TestQemuDriver_Fingerprint(t *testing.T) { 254 ci.Parallel(t) 255 require := require.New(t) 256 257 ctestutil.QemuCompatible(t) 258 259 ctx, cancel := context.WithCancel(context.Background()) 260 defer cancel() 261 262 d := NewQemuDriver(ctx, testlog.HCLogger(t)) 263 harness := dtestutil.NewDriverHarness(t, d) 264 265 fingerCh, err := harness.Fingerprint(context.Background()) 266 require.NoError(err) 267 select { 268 case finger := <-fingerCh: 269 require.Equal(drivers.HealthStateHealthy, finger.Health) 270 require.True(finger.Attributes["driver.qemu"].GetBool()) 271 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 272 require.Fail("timeout receiving fingerprint") 273 } 274 } 275 276 func TestConfig_ParseAllHCL(t *testing.T) { 277 ci.Parallel(t) 278 279 cfgStr := ` 280 config { 281 image_path = "/tmp/image_path" 282 drive_interface = "virtio" 283 accelerator = "kvm" 284 args = ["arg1", "arg2"] 285 port_map { 286 http = 80 287 https = 443 288 } 289 graceful_shutdown = true 290 }` 291 292 expected := &TaskConfig{ 293 ImagePath: "/tmp/image_path", 294 DriveInterface: "virtio", 295 Accelerator: "kvm", 296 Args: []string{"arg1", "arg2"}, 297 PortMap: map[string]int{ 298 "http": 80, 299 "https": 443, 300 }, 301 GracefulShutdown: true, 302 } 303 304 var tc *TaskConfig 305 hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc) 306 307 require.EqualValues(t, expected, tc) 308 } 309 310 func TestIsAllowedDriveInterface(t *testing.T) { 311 validInterfaces := []string{"ide", "scsi", "sd", "mtd", "floppy", "pflash", "virtio", "none"} 312 invalidInterfaces := []string{"foo", "virtio-foo"} 313 314 for _, i := range validInterfaces { 315 require.Truef(t, isAllowedDriveInterface(i), "drive_interface should be allowed: %v", i) 316 } 317 318 for _, i := range invalidInterfaces { 319 require.Falsef(t, isAllowedDriveInterface(i), "drive_interface should be not allowed: %v", i) 320 } 321 } 322 323 func TestIsAllowedImagePath(t *testing.T) { 324 ci.Parallel(t) 325 326 allowedPaths := []string{"/tmp", "/opt/qemu"} 327 allocDir := "/opt/nomad/some-alloc-dir" 328 329 validPaths := []string{ 330 "local/path", 331 "/tmp/subdir/qemu-image", 332 "/opt/qemu/image", 333 "/opt/qemu/subdir/image", 334 "/opt/nomad/some-alloc-dir/local/image.img", 335 } 336 337 invalidPaths := []string{ 338 "/image.img", 339 "../image.img", 340 "/tmpimage.img", 341 "/opt/other/image.img", 342 "/opt/nomad-submatch.img", 343 } 344 345 for _, p := range validPaths { 346 require.Truef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be allowed: %v", p) 347 } 348 349 for _, p := range invalidPaths { 350 require.Falsef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be not allowed: %v", p) 351 } 352 } 353 354 func TestArgsAllowList(t *testing.T) { 355 ci.Parallel(t) 356 357 pluginConfigAllowList := []string{"-drive", "-net", "-snapshot"} 358 359 validArgs := [][]string{ 360 {"-drive", "/path/to/wherever", "-snapshot"}, 361 {"-net", "tap,vlan=0,ifname=tap0"}, 362 } 363 364 invalidArgs := [][]string{ 365 {"-usbdevice", "mouse"}, 366 {"-singlestep"}, 367 {"--singlestep"}, 368 {" -singlestep"}, 369 {"\t-singlestep"}, 370 } 371 372 for _, args := range validArgs { 373 require.NoError(t, validateArgs(pluginConfigAllowList, args)) 374 require.NoError(t, validateArgs([]string{}, args)) 375 376 } 377 for _, args := range invalidArgs { 378 require.Error(t, validateArgs(pluginConfigAllowList, args)) 379 require.NoError(t, validateArgs([]string{}, args)) 380 } 381 382 }