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