diff --git a/mantle/kola/tests/ignition/qemufailure.go b/mantle/kola/tests/ignition/qemufailure.go index 681f2295a2..3876cdffdc 100644 --- a/mantle/kola/tests/ignition/qemufailure.go +++ b/mantle/kola/tests/ignition/qemufailure.go @@ -17,6 +17,9 @@ package ignition import ( "context" "fmt" + "os" + "os/exec" + "regexp" "time" "github.com/pkg/errors" @@ -26,6 +29,8 @@ import ( "github.com/coreos/coreos-assembler/mantle/kola/register" "github.com/coreos/coreos-assembler/mantle/platform" "github.com/coreos/coreos-assembler/mantle/platform/conf" + "github.com/coreos/coreos-assembler/mantle/util" + "github.com/coreos/ignition/v2/config/v3_2/types" ) func init() { @@ -37,6 +42,20 @@ func init() { Platforms: []string{"qemu"}, Tags: []string{"ignition"}, }) + register.RegisterTest(®ister.Test{ + Name: "coreos.unique.boot.failure", + ClusterSize: 0, + Description: "Verify boot fails if there are pre-existing boot filesystems.", + Platforms: []string{"qemu"}, + Run: runDualBootfsFailure, + }) + register.RegisterTest(®ister.Test{ + Name: "coreos.unique.boot.ignition.failure", + ClusterSize: 0, + Description: "Verify boot fails if there are pre-existing boot filesystems created with Ignition.", + Platforms: []string{"qemu"}, + Run: runDualBootfsIgnitionFailure, + }) } func runIgnitionFailure(c cluster.TestCluster) { @@ -45,47 +64,68 @@ func runIgnitionFailure(c cluster.TestCluster) { } } -func ignitionFailure(c cluster.TestCluster) error { - // We can't create files in / due to the immutable bit OSTree creates, so - // this is a convenient way to test Ignition failure. - failConfig, err := conf.EmptyIgnition().Render(conf.FailWarnings) - if err != nil { - return errors.Wrapf(err, "creating empty config") +func runDualBootfsFailure(c cluster.TestCluster) { + if err := dualBootfsFailure(c); err != nil { + c.Fatal(err.Error()) } - failConfig.AddFile("/notwritable.txt", "Hello world", 0644) +} - builder := platform.NewQemuBuilder() - defer builder.Close() - builder.SetConfig(failConfig) - err = builder.AddBootDisk(&platform.Disk{ - BackingFile: kola.QEMUOptions.DiskImage, - }) +func runDualBootfsIgnitionFailure(c cluster.TestCluster) { + if err := dualBootfsIgnitionFailure(c); err != nil { + c.Fatal(err.Error()) + } +} + +// Read file and verify if it contains a pattern +// 1. Read file, make sure it exists +// 2. regex for pattern +func fileContainsPattern(path string, searchPattern string) (bool, error) { + file, err := os.ReadFile(path) if err != nil { - return err + return false, err } - builder.MemoryMiB = 1024 - builder.Firmware = kola.QEMUOptions.Firmware + // File has content, but the pattern is not present + match := regexp.MustCompile(searchPattern).Match(file) + if match { + // Pattern found + return true, nil + } + // Pattern not found + return false, nil +} + +// Start the VM, take string and grep for it in the temporary console logs +func verifyError(builder *platform.QemuBuilder, searchPattern string) error { inst, err := builder.Exec() if err != nil { return err } defer inst.Destroy() - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() errchan := make(chan error) go func() { - err := inst.WaitAll(ctx) - if err == nil { - err = fmt.Errorf("Ignition unexpectedly succeeded") - } else if err == platform.ErrInitramfsEmergency { - // The expected case - err = nil + resultingError := inst.WaitAll(ctx) + if resultingError == nil { + resultingError = fmt.Errorf("ignition unexpectedly succeeded") + } else if resultingError == platform.ErrInitramfsEmergency { + // Expectred initramfs failure, checking the console file to insure + // that coreos.ignition.failure failed + found, err := fileContainsPattern(builder.ConsoleFile, searchPattern) + if err != nil { + resultingError = errors.Wrapf(err, "looking for pattern '%s' in file '%s' failed", searchPattern, builder.ConsoleFile) + } else if !found { + resultingError = errors.Wrapf(err, "pattern '%s' not found in file '%s'", searchPattern, builder.ConsoleFile) + } else { + // The expected case + resultingError = nil + } } else { - err = errors.Wrapf(err, "expected initramfs emergency.target error") + resultingError = errors.Wrapf(resultingError, "expected initramfs emergency.target error") } - errchan <- err + errchan <- resultingError }() select { @@ -101,3 +141,153 @@ func ignitionFailure(c cluster.TestCluster) error { return nil } } + +func ignitionFailure(c cluster.TestCluster) error { + // We can't create files in / due to the immutable bit OSTree creates, so + // this is a convenient way to test Ignition failure. + failConfig, err := conf.EmptyIgnition().Render(conf.FailWarnings) + if err != nil { + return errors.Wrapf(err, "creating empty config") + } + failConfig.AddFile("/notwritable.txt", "Hello world", 0644) + + builder := platform.NewQemuBuilder() + + // Create a temporary log file + consoleFile, err := builder.TempFile("console.log") + if err != nil { + return err + } + // Instruct builder to use it + builder.ConsoleFile = consoleFile.Name() + defer builder.Close() + builder.SetConfig(failConfig) + err = builder.AddBootDisk(&platform.Disk{ + BackingFile: kola.QEMUOptions.DiskImage, + }) + if err != nil { + return err + } + + builder.MemoryMiB = 1024 + builder.Firmware = kola.QEMUOptions.Firmware + + searchPattern := "error creating /sysroot/notwritable.txt" + if err := verifyError(builder, searchPattern); err != nil { + return err + } + return nil +} + +// Verify that there is only one boot filesystem attached to the device +func dualBootfsFailure(c cluster.TestCluster) error { + builder := platform.NewQemuBuilder() + // Create a temporary log file + consoleFile, err := builder.TempFile("console.log") + if err != nil { + return err + } + // Instruct builder to use it + builder.ConsoleFile = consoleFile.Name() + // get current path and create tmp dir + fakeBootFile, err := builder.TempFile("fakeBoot") + if err != nil { + return err + } + + // Truncate the file to 1 gigabyte + const oneGB = 1 << 30 + err = fakeBootFile.Truncate(oneGB) + if err != nil { + return err + } + + cmd := exec.Command("mkfs.ext4", "-L", "boot", fakeBootFile.Name()) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + c.Fatal(err) + } + + err = builder.AddBootDisk(&platform.Disk{ + BackingFile: kola.QEMUOptions.DiskImage, + }) + if err != nil { + return err + } + err = builder.AddDisk(&platform.Disk{ + BackingFile: fakeBootFile.Name(), + BackingFormat: "raw", + }) + if err != nil { + return err + } + builder.MemoryMiB = 1024 + builder.Firmware = kola.QEMUOptions.Firmware + + searchRegexString := "Error: System has 2 devices with a filesystem labeled 'boot'" + if err := verifyError(builder, searchRegexString); err != nil { + return err + } + return nil +} + +// Use ignition config to create a second bootfs +// 1 - produce an ignition file that format a disk with a"boot" label. +// 2 - boot the VM with the ignition file and an extra disk. +// 3 - observe the failure +func dualBootfsIgnitionFailure(c cluster.TestCluster) error { + builder := platform.NewQemuBuilder() + + // Create a temporary log file + consoleFile, err := builder.TempFile("console.log") + if err != nil { + return err + } + // Instruct builder to use it + builder.ConsoleFile = consoleFile.Name() + failConfig, err := conf.EmptyIgnition().Render(conf.FailWarnings) + if err != nil { + return errors.Wrapf(err, "creating empty config") + } + + // Craft an Ignition file that formats a partition + formaterConfig := types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-id/virtio-extra-boot", + Label: util.StrToPtr("boot"), + Format: util.StrToPtr("vfat"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + } + failConfig.MergeV32(formaterConfig) + + builder.SetConfig(failConfig) + err = builder.AddBootDisk(&platform.Disk{ + BackingFile: kola.QEMUOptions.DiskImage, + }) + + if err != nil { + return err + } + + err = builder.AddDisksFromSpecs([]string{"1G:serial=extra-boot"}) + if err != nil { + return err + } + + builder.MemoryMiB = 1024 + builder.Firmware = kola.QEMUOptions.Firmware + + searchRegexString := "Error: System has 2 devices with a filesystem labeled 'boot'" + if err := verifyError(builder, searchRegexString); err != nil { + return err + } + return nil +}