github.com/anuvu/nomad@v0.8.7-atom1/client/driver/qemu_test.go (about) 1 package driver 2 3 import ( 4 "bytes" 5 "fmt" 6 "net" 7 "path/filepath" 8 "strconv" 9 "strings" 10 "syscall" 11 "testing" 12 "time" 13 14 "github.com/hashicorp/consul/lib/freeport" 15 "github.com/hashicorp/nomad/client/config" 16 cstructs "github.com/hashicorp/nomad/client/structs" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hashicorp/nomad/testutil" 19 20 ctestutils "github.com/hashicorp/nomad/client/testutil" 21 ) 22 23 // The fingerprinter test should always pass, even if QEMU is not installed. 24 func TestQemuDriver_Fingerprint(t *testing.T) { 25 if !testutil.IsTravis() { 26 t.Parallel() 27 } 28 ctestutils.QemuCompatible(t) 29 task := &structs.Task{ 30 Name: "foo", 31 Driver: "qemu", 32 Resources: structs.DefaultResources(), 33 } 34 ctx := testDriverContexts(t, task) 35 defer ctx.AllocDir.Destroy() 36 d := NewQemuDriver(ctx.DriverCtx) 37 38 node := &structs.Node{ 39 Attributes: make(map[string]string), 40 } 41 42 request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} 43 var response cstructs.FingerprintResponse 44 err := d.Fingerprint(request, &response) 45 if err != nil { 46 t.Fatalf("err: %v", err) 47 } 48 49 if !response.Detected { 50 t.Fatalf("expected response to be applicable") 51 } 52 53 attributes := response.Attributes 54 if attributes == nil { 55 t.Fatalf("attributes should not be nil") 56 } 57 58 if attributes[qemuDriverAttr] == "" { 59 t.Fatalf("Missing Qemu driver") 60 } 61 62 if attributes[qemuDriverVersionAttr] == "" { 63 t.Fatalf("Missing Qemu driver version") 64 } 65 } 66 67 func TestQemuDriver_StartOpen_Wait(t *testing.T) { 68 logger := testLogger() 69 if !testutil.IsTravis() { 70 t.Parallel() 71 } 72 ctestutils.QemuCompatible(t) 73 task := &structs.Task{ 74 Name: "linux", 75 Driver: "qemu", 76 Config: map[string]interface{}{ 77 "image_path": "linux-0.2.img", 78 "accelerator": "tcg", 79 "graceful_shutdown": false, 80 "port_map": []map[string]int{{ 81 "main": 22, 82 "web": 8080, 83 }}, 84 "args": []string{"-nodefconfig", "-nodefaults"}, 85 }, 86 LogConfig: &structs.LogConfig{ 87 MaxFiles: 10, 88 MaxFileSizeMB: 10, 89 }, 90 Resources: &structs.Resources{ 91 CPU: 500, 92 MemoryMB: 512, 93 Networks: []*structs.NetworkResource{ 94 { 95 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 96 }, 97 }, 98 }, 99 } 100 101 ctx := testDriverContexts(t, task) 102 defer ctx.AllocDir.Destroy() 103 d := NewQemuDriver(ctx.DriverCtx) 104 105 // Copy the test image into the task's directory 106 dst := ctx.ExecCtx.TaskDir.Dir 107 108 copyFile("./test-resources/qemu/linux-0.2.img", filepath.Join(dst, "linux-0.2.img"), t) 109 110 if _, err := d.Prestart(ctx.ExecCtx, task); err != nil { 111 t.Fatalf("Prestart failed: %v", err) 112 } 113 114 resp, err := d.Start(ctx.ExecCtx, task) 115 if err != nil { 116 t.Fatalf("err: %v", err) 117 } 118 119 // Ensure that sending a Signal returns an error 120 if err := resp.Handle.Signal(syscall.SIGINT); err == nil { 121 t.Fatalf("Expect an error when signalling") 122 } 123 124 // Attempt to open 125 handle2, err := d.Open(ctx.ExecCtx, resp.Handle.ID()) 126 if err != nil { 127 t.Fatalf("err: %v", err) 128 } 129 if handle2 == nil { 130 t.Fatalf("missing handle") 131 } 132 133 // Clean up 134 if err := resp.Handle.Kill(); err != nil { 135 logger.Printf("Error killing Qemu test: %s", err) 136 } 137 } 138 139 func TestQemuDriver_GracefulShutdown(t *testing.T) { 140 testutil.SkipSlow(t) 141 if !testutil.IsTravis() { 142 t.Parallel() 143 } 144 ctestutils.QemuCompatible(t) 145 146 logger := testLogger() 147 148 // Graceful shutdown may be really slow unfortunately 149 killTimeout := 3 * time.Minute 150 151 // Grab a free port so we can tell when the image has started 152 port := freeport.GetT(t, 1)[0] 153 154 task := &structs.Task{ 155 Name: "alpine-shutdown-test", 156 Driver: "qemu", 157 Config: map[string]interface{}{ 158 "image_path": "alpine.qcow2", 159 "graceful_shutdown": true, 160 "args": []string{"-nodefconfig", "-nodefaults"}, 161 "port_map": []map[string]int{{ 162 "ssh": 22, 163 }}, 164 }, 165 LogConfig: &structs.LogConfig{ 166 MaxFiles: 10, 167 MaxFileSizeMB: 10, 168 }, 169 Resources: &structs.Resources{ 170 CPU: 1000, 171 MemoryMB: 256, 172 Networks: []*structs.NetworkResource{ 173 { 174 ReservedPorts: []structs.Port{{Label: "ssh", Value: port}}, 175 }, 176 }, 177 }, 178 KillTimeout: killTimeout, 179 } 180 181 ctx := testDriverContexts(t, task) 182 ctx.DriverCtx.config.MaxKillTimeout = killTimeout 183 defer ctx.AllocDir.Destroy() 184 d := NewQemuDriver(ctx.DriverCtx) 185 186 request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: ctx.DriverCtx.node} 187 var response cstructs.FingerprintResponse 188 err := d.Fingerprint(request, &response) 189 if err != nil { 190 t.Fatalf("err: %v", err) 191 } 192 193 for name, value := range response.Attributes { 194 ctx.DriverCtx.node.Attributes[name] = value 195 } 196 197 dst := ctx.ExecCtx.TaskDir.Dir 198 199 copyFile("./test-resources/qemu/alpine.qcow2", filepath.Join(dst, "alpine.qcow2"), t) 200 201 if _, err := d.Prestart(ctx.ExecCtx, task); err != nil { 202 t.Fatalf("Prestart failed: %v", err) 203 } 204 205 resp, err := d.Start(ctx.ExecCtx, task) 206 if err != nil { 207 t.Fatalf("err: %v", err) 208 } 209 210 // Clean up 211 defer func() { 212 select { 213 case <-resp.Handle.WaitCh(): 214 // Already exited 215 return 216 default: 217 } 218 219 if err := resp.Handle.Kill(); err != nil { 220 logger.Printf("[TEST] Error killing Qemu test: %s", err) 221 } 222 }() 223 224 // Wait until sshd starts before attempting to do a graceful shutdown 225 testutil.WaitForResult(func() (bool, error) { 226 conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) 227 if err != nil { 228 return false, err 229 } 230 231 // Since the connection will be accepted by the QEMU process 232 // before sshd actually starts, we need to block until we can 233 // read the "SSH" magic bytes 234 header := make([]byte, 3) 235 conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 236 _, err = conn.Read(header) 237 if err != nil { 238 return false, err 239 } 240 if !bytes.Equal(header, []byte{'S', 'S', 'H'}) { 241 return false, fmt.Errorf("expected 'SSH' but received: %q %v", string(header), header) 242 } 243 244 logger.Printf("[TEST] connected to sshd in VM") 245 conn.Close() 246 return true, nil 247 }, func(err error) { 248 t.Fatalf("failed to connect to sshd in VM: %v", err) 249 }) 250 251 monitorPath := filepath.Join(ctx.AllocDir.AllocDir, task.Name, qemuMonitorSocketName) 252 253 // userPid supplied in sendQemuShutdown calls is bogus (it's used only 254 // for log output) 255 if err := sendQemuShutdown(ctx.DriverCtx.logger, "", 0); err == nil { 256 t.Fatalf("sendQemuShutdown should return an error if monitorPath parameter is empty") 257 } 258 259 if err := sendQemuShutdown(ctx.DriverCtx.logger, "/path/that/does/not/exist", 0); err == nil { 260 t.Fatalf("sendQemuShutdown should return an error if file does not exist at monitorPath") 261 } 262 263 if err := sendQemuShutdown(ctx.DriverCtx.logger, monitorPath, 0); err != nil { 264 t.Fatalf("unexpected error from sendQemuShutdown: %s", err) 265 } 266 267 select { 268 case <-resp.Handle.WaitCh(): 269 logger.Printf("[TEST] VM exited gracefully as expected") 270 case <-time.After(killTimeout): 271 t.Fatalf("VM did not exit gracefully exit before timeout: %s", killTimeout) 272 } 273 } 274 275 func TestQemuDriverUser(t *testing.T) { 276 if !testutil.IsTravis() { 277 t.Parallel() 278 } 279 ctestutils.QemuCompatible(t) 280 tasks := []*structs.Task{ 281 { 282 Name: "linux", 283 Driver: "qemu", 284 User: "alice", 285 Config: map[string]interface{}{ 286 "image_path": "linux-0.2.img", 287 "accelerator": "tcg", 288 "graceful_shutdown": false, 289 "port_map": []map[string]int{{ 290 "main": 22, 291 "web": 8080, 292 }}, 293 "args": []string{"-nodefconfig", "-nodefaults"}, 294 "msg": "unknown user alice", 295 }, 296 LogConfig: &structs.LogConfig{ 297 MaxFiles: 10, 298 MaxFileSizeMB: 10, 299 }, 300 Resources: &structs.Resources{ 301 CPU: 500, 302 MemoryMB: 512, 303 Networks: []*structs.NetworkResource{ 304 { 305 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 306 }, 307 }, 308 }, 309 }, 310 { 311 Name: "linux", 312 Driver: "qemu", 313 User: "alice", 314 Config: map[string]interface{}{ 315 "image_path": "linux-0.2.img", 316 "accelerator": "tcg", 317 "port_map": []map[string]int{{ 318 "main": 22, 319 "web": 8080, 320 }}, 321 "args": []string{"-nodefconfig", "-nodefaults"}, 322 "msg": "Qemu memory assignment out of bounds", 323 }, 324 LogConfig: &structs.LogConfig{ 325 MaxFiles: 10, 326 MaxFileSizeMB: 10, 327 }, 328 Resources: &structs.Resources{ 329 CPU: 500, 330 MemoryMB: -1, 331 Networks: []*structs.NetworkResource{ 332 { 333 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 334 }, 335 }, 336 }, 337 }, 338 } 339 340 for _, task := range tasks { 341 ctx := testDriverContexts(t, task) 342 defer ctx.AllocDir.Destroy() 343 d := NewQemuDriver(ctx.DriverCtx) 344 345 if _, err := d.Prestart(ctx.ExecCtx, task); err != nil { 346 t.Fatalf("Prestart faild: %v", err) 347 } 348 349 resp, err := d.Start(ctx.ExecCtx, task) 350 if err == nil { 351 resp.Handle.Kill() 352 t.Fatalf("Should've failed") 353 } 354 355 msg := task.Config["msg"].(string) 356 if !strings.Contains(err.Error(), msg) { 357 t.Fatalf("Expecting '%v' in '%v'", msg, err) 358 } 359 } 360 } 361 362 func TestQemuDriverGetMonitorPathOldQemu(t *testing.T) { 363 task := &structs.Task{ 364 Name: "linux", 365 Driver: "qemu", 366 Config: map[string]interface{}{ 367 "image_path": "linux-0.2.img", 368 "accelerator": "tcg", 369 "graceful_shutdown": true, 370 "port_map": []map[string]int{{ 371 "main": 22, 372 "web": 8080, 373 }}, 374 "args": []string{"-nodefconfig", "-nodefaults"}, 375 }, 376 KillTimeout: time.Duration(1 * time.Second), 377 LogConfig: &structs.LogConfig{ 378 MaxFiles: 10, 379 MaxFileSizeMB: 10, 380 }, 381 Resources: &structs.Resources{ 382 CPU: 500, 383 MemoryMB: 512, 384 Networks: []*structs.NetworkResource{ 385 { 386 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 387 }, 388 }, 389 }, 390 } 391 392 ctx := testDriverContexts(t, task) 393 defer ctx.AllocDir.Destroy() 394 395 // Simulate an older version of qemu which does not support long monitor socket paths 396 ctx.DriverCtx.node.Attributes[qemuDriverVersionAttr] = "2.0.0" 397 398 d := &QemuDriver{DriverContext: *ctx.DriverCtx} 399 400 shortPath := strings.Repeat("x", 10) 401 _, err := d.getMonitorPath(shortPath) 402 if err != nil { 403 t.Fatal("Should not have returned an error") 404 } 405 406 longPath := strings.Repeat("x", qemuLegacyMaxMonitorPathLen+100) 407 _, err = d.getMonitorPath(longPath) 408 if err == nil { 409 t.Fatal("Should have returned an error") 410 } 411 412 // Max length includes the '/' separator and socket name 413 maxLengthCount := qemuLegacyMaxMonitorPathLen - len(qemuMonitorSocketName) - 1 414 maxLengthLegacyPath := strings.Repeat("x", maxLengthCount) 415 _, err = d.getMonitorPath(maxLengthLegacyPath) 416 if err != nil { 417 t.Fatalf("Should not have returned an error: %s", err) 418 } 419 } 420 421 func TestQemuDriverGetMonitorPathNewQemu(t *testing.T) { 422 task := &structs.Task{ 423 Name: "linux", 424 Driver: "qemu", 425 Config: map[string]interface{}{ 426 "image_path": "linux-0.2.img", 427 "accelerator": "tcg", 428 "graceful_shutdown": true, 429 "port_map": []map[string]int{{ 430 "main": 22, 431 "web": 8080, 432 }}, 433 "args": []string{"-nodefconfig", "-nodefaults"}, 434 }, 435 KillTimeout: time.Duration(1 * time.Second), 436 LogConfig: &structs.LogConfig{ 437 MaxFiles: 10, 438 MaxFileSizeMB: 10, 439 }, 440 Resources: &structs.Resources{ 441 CPU: 500, 442 MemoryMB: 512, 443 Networks: []*structs.NetworkResource{ 444 { 445 ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}}, 446 }, 447 }, 448 }, 449 } 450 451 ctx := testDriverContexts(t, task) 452 defer ctx.AllocDir.Destroy() 453 454 // Simulate a version of qemu which supports long monitor socket paths 455 ctx.DriverCtx.node.Attributes[qemuDriverVersionAttr] = "2.99.99" 456 457 d := &QemuDriver{DriverContext: *ctx.DriverCtx} 458 459 shortPath := strings.Repeat("x", 10) 460 _, err := d.getMonitorPath(shortPath) 461 if err != nil { 462 t.Fatal("Should not have returned an error") 463 } 464 465 longPath := strings.Repeat("x", qemuLegacyMaxMonitorPathLen+100) 466 _, err = d.getMonitorPath(longPath) 467 if err != nil { 468 t.Fatal("Should not have returned an error") 469 } 470 471 maxLengthCount := qemuLegacyMaxMonitorPathLen - len(qemuMonitorSocketName) - 1 472 maxLengthLegacyPath := strings.Repeat("x", maxLengthCount) 473 _, err = d.getMonitorPath(maxLengthLegacyPath) 474 if err != nil { 475 t.Fatal("Should not have returned an error") 476 } 477 }