|
20 | 20 | package cgroup |
21 | 21 |
|
22 | 22 | import ( |
| 23 | + "io/fs" |
23 | 24 | "os" |
24 | 25 | "path" |
25 | | - "path/filepath" |
26 | 26 | "strings" |
| 27 | + "syscall" |
| 28 | + "time" |
27 | 29 |
|
28 | 30 | . "github.com/onsi/ginkgo/v2" |
29 | 31 | . "github.com/onsi/gomega" |
| 32 | + . "github.com/onsi/gomega/gstruct" |
30 | 33 | cgroups "github.com/opencontainers/cgroups" |
31 | 34 | devices "github.com/opencontainers/cgroups/devices/config" |
32 | 35 | "go.uber.org/mock/gomock" |
| 36 | + "golang.org/x/sys/unix" |
33 | 37 |
|
34 | 38 | v1 "kubevirt.io/api/core/v1" |
35 | 39 |
|
36 | 40 | "kubevirt.io/kubevirt/pkg/safepath" |
37 | | - "kubevirt.io/kubevirt/pkg/virt-handler/isolation" |
38 | 41 | ) |
39 | 42 |
|
40 | 43 | var _ = Describe("cgroup manager", func() { |
@@ -346,53 +349,285 @@ var _ = Describe("parseDevicesList", func() { |
346 | 349 |
|
347 | 350 | var _ = Describe("generateDeviceRulesForVMI", func() { |
348 | 351 | var ( |
349 | | - ctrl *gomock.Controller |
350 | | - tempDir string |
| 352 | + origStatDevice func(*safepath.Path, string) (os.FileInfo, error) |
| 353 | + origReadDevDir func(*safepath.Path, string) ([]os.DirEntry, error) |
351 | 354 | ) |
352 | 355 |
|
| 356 | + noDevices := func(_ *safepath.Path, _ string) (os.FileInfo, error) { |
| 357 | + return nil, os.ErrNotExist |
| 358 | + } |
| 359 | + noDirs := func(_ *safepath.Path, _ string) ([]os.DirEntry, error) { |
| 360 | + return nil, os.ErrNotExist |
| 361 | + } |
| 362 | + |
353 | 363 | BeforeEach(func() { |
354 | | - ctrl = gomock.NewController(GinkgoT()) |
355 | | - tempDir = GinkgoT().TempDir() |
356 | | - Expect(os.MkdirAll(filepath.Join(tempDir, "dev"), 0755)).To(Succeed()) |
| 364 | + origStatDevice = statDevice |
| 365 | + origReadDevDir = readDeviceDir |
357 | 366 | }) |
358 | 367 |
|
359 | | - newMockIsolationWithMountRoot := func() isolation.IsolationResult { |
360 | | - mountRoot, err := safepath.NewPathNoFollow(tempDir) |
361 | | - Expect(err).ToNot(HaveOccurred()) |
362 | | - |
363 | | - mockIso := isolation.NewMockIsolationResult(ctrl) |
364 | | - mockIso.EXPECT().MountRoot().Return(mountRoot, nil) |
365 | | - return mockIso |
366 | | - } |
| 368 | + AfterEach(func() { |
| 369 | + statDevice = origStatDevice |
| 370 | + readDeviceDir = origReadDevDir |
| 371 | + }) |
367 | 372 |
|
368 | 373 | It("should skip hypervisor device rule when emulation is allowed and device is missing", func() { |
369 | | - rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, newMockIsolationWithMountRoot(), "", "kvm", true) |
| 374 | + statDevice = noDevices |
| 375 | + readDeviceDir = noDirs |
| 376 | + |
| 377 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
370 | 378 | Expect(err).ToNot(HaveOccurred()) |
371 | 379 | Expect(rules).To(BeEmpty()) |
372 | 380 | }) |
373 | 381 |
|
374 | 382 | It("should fail when hypervisor device is missing and emulation is not allowed", func() { |
375 | | - _, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, newMockIsolationWithMountRoot(), "", "kvm", false) |
| 383 | + statDevice = noDevices |
| 384 | + readDeviceDir = noDirs |
| 385 | + |
| 386 | + _, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", false) |
376 | 387 | Expect(err).To(HaveOccurred()) |
377 | 388 | }) |
378 | 389 |
|
| 390 | + It("should create a rule for the hypervisor device", func() { |
| 391 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 392 | + if relPath == "/dev/kvm" { |
| 393 | + return charDeviceInfo(10, 232), nil |
| 394 | + } |
| 395 | + return nil, os.ErrNotExist |
| 396 | + } |
| 397 | + readDeviceDir = noDirs |
| 398 | + |
| 399 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", false) |
| 400 | + Expect(err).ToNot(HaveOccurred()) |
| 401 | + Expect(rules).To(ConsistOf( |
| 402 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 403 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(10)), "Minor": Equal(int64(232)), |
| 404 | + })), |
| 405 | + )) |
| 406 | + }) |
| 407 | + |
| 408 | + It("should discover VFIO device nodes", func() { |
| 409 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 410 | + switch relPath { |
| 411 | + case "/dev/vfio/vfio": |
| 412 | + return charDeviceInfo(10, 196), nil |
| 413 | + case "/dev/vfio/42": |
| 414 | + return charDeviceInfo(243, 0), nil |
| 415 | + default: |
| 416 | + return nil, os.ErrNotExist |
| 417 | + } |
| 418 | + } |
| 419 | + readDeviceDir = func(_ *safepath.Path, relPath string) ([]os.DirEntry, error) { |
| 420 | + if relPath == "/dev/vfio" { |
| 421 | + return []os.DirEntry{ |
| 422 | + &fakeDirEntry{name: "vfio"}, |
| 423 | + &fakeDirEntry{name: "42"}, |
| 424 | + }, nil |
| 425 | + } |
| 426 | + return nil, os.ErrNotExist |
| 427 | + } |
| 428 | + |
| 429 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
| 430 | + Expect(err).ToNot(HaveOccurred()) |
| 431 | + Expect(rules).To(ConsistOf( |
| 432 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 433 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(10)), "Minor": Equal(int64(196)), |
| 434 | + })), |
| 435 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 436 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(243)), "Minor": Equal(int64(0)), |
| 437 | + })), |
| 438 | + )) |
| 439 | + }) |
| 440 | + |
| 441 | + It("should discover USB device nodes in nested directories", func() { |
| 442 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 443 | + switch relPath { |
| 444 | + case "/dev/bus/usb/001/001": |
| 445 | + return charDeviceInfo(189, 0), nil |
| 446 | + case "/dev/bus/usb/001/002": |
| 447 | + return charDeviceInfo(189, 1), nil |
| 448 | + default: |
| 449 | + return nil, os.ErrNotExist |
| 450 | + } |
| 451 | + } |
| 452 | + readDeviceDir = func(_ *safepath.Path, relPath string) ([]os.DirEntry, error) { |
| 453 | + switch relPath { |
| 454 | + case "/dev/bus/usb": |
| 455 | + return []os.DirEntry{&fakeDirEntry{name: "001", isDir: true}}, nil |
| 456 | + case "/dev/bus/usb/001": |
| 457 | + return []os.DirEntry{ |
| 458 | + &fakeDirEntry{name: "001"}, |
| 459 | + &fakeDirEntry{name: "002"}, |
| 460 | + }, nil |
| 461 | + default: |
| 462 | + return nil, os.ErrNotExist |
| 463 | + } |
| 464 | + } |
| 465 | + |
| 466 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
| 467 | + Expect(err).ToNot(HaveOccurred()) |
| 468 | + Expect(rules).To(ConsistOf( |
| 469 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 470 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(189)), "Minor": Equal(int64(0)), |
| 471 | + })), |
| 472 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 473 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(189)), "Minor": Equal(int64(1)), |
| 474 | + })), |
| 475 | + )) |
| 476 | + }) |
| 477 | + |
| 478 | + It("should discover devices from both VFIO and USB", func() { |
| 479 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 480 | + switch relPath { |
| 481 | + case "/dev/vfio/vfio": |
| 482 | + return charDeviceInfo(10, 196), nil |
| 483 | + case "/dev/vfio/0": |
| 484 | + return charDeviceInfo(243, 0), nil |
| 485 | + case "/dev/bus/usb/001/001": |
| 486 | + return charDeviceInfo(189, 0), nil |
| 487 | + case "/dev/bus/usb/002/001": |
| 488 | + return charDeviceInfo(189, 128), nil |
| 489 | + default: |
| 490 | + return nil, os.ErrNotExist |
| 491 | + } |
| 492 | + } |
| 493 | + readDeviceDir = func(_ *safepath.Path, relPath string) ([]os.DirEntry, error) { |
| 494 | + switch relPath { |
| 495 | + case "/dev/vfio": |
| 496 | + return []os.DirEntry{ |
| 497 | + &fakeDirEntry{name: "vfio"}, |
| 498 | + &fakeDirEntry{name: "0"}, |
| 499 | + }, nil |
| 500 | + case "/dev/bus/usb": |
| 501 | + return []os.DirEntry{ |
| 502 | + &fakeDirEntry{name: "001", isDir: true}, |
| 503 | + &fakeDirEntry{name: "002", isDir: true}, |
| 504 | + }, nil |
| 505 | + case "/dev/bus/usb/001": |
| 506 | + return []os.DirEntry{&fakeDirEntry{name: "001"}}, nil |
| 507 | + case "/dev/bus/usb/002": |
| 508 | + return []os.DirEntry{&fakeDirEntry{name: "001"}}, nil |
| 509 | + default: |
| 510 | + return nil, os.ErrNotExist |
| 511 | + } |
| 512 | + } |
| 513 | + |
| 514 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
| 515 | + Expect(err).ToNot(HaveOccurred()) |
| 516 | + Expect(rules).To(HaveLen(4)) |
| 517 | + }) |
| 518 | + |
| 519 | + It("should create a rule for urandom when RNG is enabled", func() { |
| 520 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 521 | + if relPath == "/dev/urandom" { |
| 522 | + return charDeviceInfo(1, 9), nil |
| 523 | + } |
| 524 | + return nil, os.ErrNotExist |
| 525 | + } |
| 526 | + readDeviceDir = noDirs |
| 527 | + |
| 528 | + vmi := &v1.VirtualMachineInstance{} |
| 529 | + vmi.Spec.Domain.Devices.Rng = &v1.Rng{} |
| 530 | + |
| 531 | + rules, err := generateDeviceRulesForVMI(vmi, nil, "", "kvm", true) |
| 532 | + Expect(err).ToNot(HaveOccurred()) |
| 533 | + Expect(rules).To(ConsistOf( |
| 534 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 535 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(1)), "Minor": Equal(int64(9)), |
| 536 | + })), |
| 537 | + )) |
| 538 | + }) |
| 539 | + |
| 540 | + It("should create a rule for vhost-vsock when AutoattachVSOCK is enabled", func() { |
| 541 | + statDevice = func(_ *safepath.Path, relPath string) (os.FileInfo, error) { |
| 542 | + if relPath == "/dev/vhost-vsock" { |
| 543 | + return charDeviceInfo(10, 241), nil |
| 544 | + } |
| 545 | + return nil, os.ErrNotExist |
| 546 | + } |
| 547 | + readDeviceDir = noDirs |
| 548 | + |
| 549 | + autoAttach := true |
| 550 | + vmi := &v1.VirtualMachineInstance{} |
| 551 | + vmi.Spec.Domain.Devices.AutoattachVSOCK = &autoAttach |
| 552 | + |
| 553 | + rules, err := generateDeviceRulesForVMI(vmi, nil, "", "kvm", true) |
| 554 | + Expect(err).ToNot(HaveOccurred()) |
| 555 | + Expect(rules).To(ConsistOf( |
| 556 | + PointTo(MatchFields(IgnoreExtras, Fields{ |
| 557 | + "Type": Equal(devices.CharDevice), "Major": Equal(int64(10)), "Minor": Equal(int64(241)), |
| 558 | + })), |
| 559 | + )) |
| 560 | + }) |
| 561 | + |
379 | 562 | It("should not fail when /dev/vfio does not exist", func() { |
380 | | - rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, newMockIsolationWithMountRoot(), "", "kvm", true) |
| 563 | + statDevice = noDevices |
| 564 | + readDeviceDir = noDirs |
| 565 | + |
| 566 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
381 | 567 | Expect(err).ToNot(HaveOccurred()) |
382 | 568 | Expect(rules).To(BeEmpty()) |
383 | 569 | }) |
384 | 570 |
|
385 | 571 | It("should not fail when /dev/vfio exists but is empty", func() { |
386 | | - Expect(os.MkdirAll(filepath.Join(tempDir, "dev", "vfio"), 0755)).To(Succeed()) |
387 | | - rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, newMockIsolationWithMountRoot(), "", "kvm", true) |
| 572 | + statDevice = noDevices |
| 573 | + readDeviceDir = func(_ *safepath.Path, relPath string) ([]os.DirEntry, error) { |
| 574 | + if relPath == "/dev/vfio" { |
| 575 | + return nil, nil |
| 576 | + } |
| 577 | + return nil, os.ErrNotExist |
| 578 | + } |
| 579 | + |
| 580 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
388 | 581 | Expect(err).ToNot(HaveOccurred()) |
389 | 582 | Expect(rules).To(BeEmpty()) |
390 | 583 | }) |
391 | 584 |
|
392 | 585 | It("should not fail when /dev/bus/usb exists but is empty", func() { |
393 | | - Expect(os.MkdirAll(filepath.Join(tempDir, "dev", "bus", "usb"), 0755)).To(Succeed()) |
394 | | - rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, newMockIsolationWithMountRoot(), "", "kvm", true) |
| 586 | + statDevice = noDevices |
| 587 | + readDeviceDir = func(_ *safepath.Path, relPath string) ([]os.DirEntry, error) { |
| 588 | + if relPath == "/dev/bus/usb" { |
| 589 | + return nil, nil |
| 590 | + } |
| 591 | + return nil, os.ErrNotExist |
| 592 | + } |
| 593 | + |
| 594 | + rules, err := generateDeviceRulesForVMI(&v1.VirtualMachineInstance{}, nil, "", "kvm", true) |
395 | 595 | Expect(err).ToNot(HaveOccurred()) |
396 | 596 | Expect(rules).To(BeEmpty()) |
397 | 597 | }) |
398 | 598 | }) |
| 599 | + |
| 600 | +func charDeviceInfo(major, minor uint32) os.FileInfo { |
| 601 | + return &fakeFileInfo{ |
| 602 | + mode: os.ModeDevice | os.ModeCharDevice, |
| 603 | + rdev: unix.Mkdev(major, minor), |
| 604 | + } |
| 605 | +} |
| 606 | + |
| 607 | +type fakeFileInfo struct { |
| 608 | + name string |
| 609 | + mode os.FileMode |
| 610 | + rdev uint64 |
| 611 | +} |
| 612 | + |
| 613 | +func (f *fakeFileInfo) Name() string { return f.name } |
| 614 | +func (f *fakeFileInfo) Size() int64 { return 0 } |
| 615 | +func (f *fakeFileInfo) Mode() os.FileMode { return f.mode } |
| 616 | +func (f *fakeFileInfo) ModTime() time.Time { return time.Time{} } |
| 617 | +func (f *fakeFileInfo) IsDir() bool { return f.mode.IsDir() } |
| 618 | +func (f *fakeFileInfo) Sys() interface{} { return &syscall.Stat_t{Rdev: f.rdev} } |
| 619 | + |
| 620 | +type fakeDirEntry struct { |
| 621 | + name string |
| 622 | + isDir bool |
| 623 | +} |
| 624 | + |
| 625 | +func (e *fakeDirEntry) Name() string { return e.name } |
| 626 | +func (e *fakeDirEntry) IsDir() bool { return e.isDir } |
| 627 | +func (e *fakeDirEntry) Type() fs.FileMode { |
| 628 | + if e.isDir { |
| 629 | + return fs.ModeDir |
| 630 | + } |
| 631 | + return 0 |
| 632 | +} |
| 633 | +func (e *fakeDirEntry) Info() (fs.FileInfo, error) { return nil, nil } |
0 commit comments