diff --git a/boot/piboot/piboot.go b/boot/piboot/piboot.go index eb0590ab4cf9229842cff21eb0c87f3dd0f42163..5f714fe7a2e0681e1b54eba38ae716229a292094 100644 --- a/boot/piboot/piboot.go +++ b/boot/piboot/piboot.go @@ -1,15 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Huawei Inc. -// Package piboot implements the bootloader protocol for the Raspberry Pi boot loader. +// Package piboot implements the boot protocol for the Raspberry Pi. // // This implementation assumes the following: The raspberry pi boot firmware is -// at least the release from 2020-10-28. The kernel image and command line files -// are stored in sub-directories of the firmware boot partition under slot-a/ -// and slot-b/. Each kernel command line file points to the correct slot. The -// kernel image supports the RPI_FIRMWARE_SET_REBOOT_FLAGS feature. +// at least the release from 2020-10-28. All boot assets selected by the +// os_prefix= mechanism are stored in a sub-directory named either slot-a/ or +// slot-b/, one for each slot. // -// XXX: simplify slot-a/ and slot-b/ detection with the "prefix" variable. +// Each kernel command line file points root= argument to block device matching +// each slot. The kernel image supports the RPI_FIRMWARE_SET_REBOOT_FLAGS +// feature, which is used to perform "tryboot" reboots before commiting the new +// configuration. // // References: // - https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa @@ -23,78 +25,71 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "git.ostc-eu.org/OSTC/OHOS/components/sysota/boot" - "git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg" - "git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/condfilter" "git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/pimodel" "git.ostc-eu.org/OSTC/OHOS/components/sysota/rauc/installhandler" + "git.ostc-eu.org/OSTC/OHOS/components/sysota/squashfstools/unsquashfs" ) const ( - slotAPrefix = "slot-a/" - slotBPrefix = "slot-b/" - - // configTxtName is the name of the firmware configuration file read during the boot process. + // configTxtName is the name of the firmware configuration file read during + // the boot process. configTxtName = "config.txt" - - // trybootTxtName is the name of the firmware configuration file read during the try-once boot process. - // It is further described at https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa + // trybootTxtName is the name of the firmware configuration file read during + // the try-once boot process. It is further described at + // https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa trybootTxtName = "tryboot.txt" - - kernelParameter = "kernel" - initramfsParameter = "initramfs" - cmdlineParameter = "cmdline" + // argSysotaSlot is a kernel command line argument encoding slot name. + argSysotaSlot = "sysota.slot=" + // argRoot is a kenrel command line argument designating the device with the + // root file system. + argRoot = "root=" ) +func slotPrefix(slot boot.Slot) string { + return fmt.Sprintf("slot-%s/", strings.ToLower(slot.String())) +} + // PiBoot implements boot.Protocol for the Raspberry Pi boot loader. type PiBoot struct { - // BootMountPoint determines the mount point of the firmware boot partition. - // It is expected that the "config.txt" file is present there. - BootMountPoint string - - revCode pimodel.RevisionCode - serialNum pimodel.SerialNumber - - // FilterPredicate decides if a config.txt section should be inspected. - // - // A non-zero filter predicate can be used to support a single system image - // that is compatible with different device types, each with a distinct - // kernel image by only considering those configuration sections that apply - // to the current device. - // - // When the filter predicate is not used, section names are not parsed and - // unrecognized sections are preserved perfectly. When the predicate is set - // all sections must be recognized. - // - // NOTE(zyga): FilterPredicate is deprecated in favour of (upcoming) usage - // of revision code and serial number coupled with condfilter.Simulator to - // perform read and write requests. - FilterPredicate func([]condfilter.ConditionalFilter) bool + bootMountPoint string + revCode pimodel.RevisionCode + serialNum pimodel.SerialNumber } -// New returns a new PiBoot with the given firmware boot partition mount point. -// The specified directory must contain the "config.txt" file. -// -// Revision code and serial number are stored but are not yet used. +// New returns a new PiBoot with the given boot mount point and CPU properties. func New(bootMountPoint string, revCode pimodel.RevisionCode, serialNum pimodel.SerialNumber) (*PiBoot, error) { - file, err := os.Open(filepath.Join(bootMountPoint, configTxtName)) - if err != nil { - return nil, err - } - - if err := file.Close(); err != nil { - return nil, err - } - + // TODO(zyga): drop the error return path if it is not useful later on. return &PiBoot{ - BootMountPoint: bootMountPoint, + bootMountPoint: bootMountPoint, revCode: revCode, serialNum: serialNum, }, nil } +// BootMountPoint returns the corresponding value passed to the New. +func (pi *PiBoot) BootMountPoint() string { + return pi.bootMountPoint +} + +// RevisionCode returns the corresponding value passed to the New. +func (pi *PiBoot) RevisionCode() pimodel.RevisionCode { + return pi.revCode +} + +// SerialNumber returns the corresponding value passed to the New. +func (pi *PiBoot) SerialNumber() pimodel.SerialNumber { + return pi.serialNum +} + +// activeBootConfig loads the boot config corresponding to the active slot. +func (pi *PiBoot) activeBootConfig() (*BootConfig, error) { + return NewBootConfig(pi.bootMountPoint, pi.revCode, pi.serialNum) +} + // UnknownActiveSlotError records problems with determining active slot. type UnknownActiveSlotError struct { Err error @@ -126,67 +121,34 @@ func (e *UnknownInactiveSlotError) Error() string { } var ( + // ErrIncompatible records incompatible boot configuration + ErrIncompatible = errors.New("incompatible boot configuration") // ErrCorruptedConfig records boot configuration deviating from assumptions. ErrCorruptedConfig = errors.New("inconsistent or corrupted configuration") - // ErrExpectedSlotAOrSlotB records existence of unexpected slot. - ErrExpectedSlotAOrSlotB = errors.New("expected slot A or slot B") ) // QueryActive returns the slot that is used for booting. // -// Active slot is determined by inspecting the kernel and cmdline parameters in -// the last section that is applicable, as decided by the filter predicate. By -// arbitrary convention, both parameters must have the prefix "slot-a/" or -// "slot-b/" to be recognized. -// +// Active slot is determined by the value of os_prefix= parameter. // If there is an error, it will be of type *UnknownActiveSlotError. func (pi *PiBoot) QueryActive() (slot boot.Slot, err error) { - file, err := os.Open(filepath.Join(pi.BootMountPoint, configTxtName)) + bootCfg, err := pi.activeBootConfig() if err != nil { return boot.InvalidSlot, &UnknownActiveSlotError{Err: err} } - defer func() { - e := file.Close() - if err == nil { - err = e - } - }() - - var configTxt picfg.ConfigTxt - - if err := picfg.NewDecoder(file).Decode(&configTxt); err != nil { - return boot.InvalidSlot, &UnknownActiveSlotError{Err: err} - } - - // Find the configured kernel and kernel cmdline - var kernel, initramfs, cmdline string - - err = visitSections(&configTxt, pi.FilterPredicate, func(sect *picfg.Section) error { - for _, param := range sect.Parameters { - switch param.Name { - case kernelParameter: - kernel = param.Value - case initramfsParameter: - initramfs = param.Value - case cmdlineParameter: - cmdline = param.Value - } - } - return nil - }) + osPrefix, _, err := bootCfg.ComputedOSPrefix() if err != nil { return boot.InvalidSlot, &UnknownActiveSlotError{Err: err} } - // Assuming kernel and cmdline are stored in slot-specific sub-directories, - // find the configured slot. - switch { - case strings.HasPrefix(kernel, slotAPrefix) && strings.HasPrefix(cmdline, slotAPrefix) && strings.HasPrefix(initramfs, slotAPrefix): + + switch osPrefix { + case slotPrefix(boot.SlotA): return boot.SlotA, nil - case strings.HasPrefix(kernel, slotBPrefix) && strings.HasPrefix(cmdline, slotBPrefix) && strings.HasPrefix(initramfs, slotBPrefix): + case slotPrefix(boot.SlotB): return boot.SlotB, nil default: - return boot.InvalidSlot, &UnknownActiveSlotError{Err: ErrCorruptedConfig} + return boot.InvalidSlot, &UnknownActiveSlotError{Err: ErrIncompatible} } } @@ -213,77 +175,34 @@ func (pi *PiBoot) QueryInactive() (boot.Slot, error) { } // TrySwitch configures the boot loader for a one-off boot using the given slot. -func (pi *PiBoot) TrySwitch(to boot.Slot) (err error) { - // Load and decode config.txt - configTxtFile, err := os.Open(filepath.Join(pi.BootMountPoint, configTxtName)) - if err != nil { - return err - } - - defer func() { - e := configTxtFile.Close() - if err == nil { - err = e - } - }() - - var configTxt picfg.ConfigTxt - if err := picfg.NewDecoder(configTxtFile).Decode(&configTxt); err != nil { - return err - } - - // Find the values to modify, swapping A and B around - err = visitSections(&configTxt, pi.FilterPredicate, func(sect *picfg.Section) error { - for paramIdx, param := range sect.Parameters { - switch param.Name { - case kernelParameter, cmdlineParameter, initramfsParameter: - var oldPrefix, newPrefix string - switch to { - case boot.SlotA: - oldPrefix = slotBPrefix - newPrefix = slotAPrefix - case boot.SlotB: - oldPrefix = slotAPrefix - newPrefix = slotBPrefix - default: - continue - } - sect.Parameters[paramIdx].Value = strings.Replace(param.Value, oldPrefix, newPrefix, -1) - } - } - return nil - }) +func (pi *PiBoot) TrySwitch(targetSlot boot.Slot) (err error) { + slottedBootDir := slotPrefix(targetSlot) + + // Load boot configuration pretending that slot sub-directory of the boot + // partition is the is the real boot partition. This is the state we get + // from the pre-install handler. + slotBootCfg, err := NewBootConfig( + filepath.Join(pi.bootMountPoint, slottedBootDir), pi.revCode, pi.serialNum) if err != nil { return err } - // Encode and write modified config.txt as tryboot.txt - trybootTxtFile, err := os.OpenFile( - filepath.Join(pi.BootMountPoint, trybootTxtName), - os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) - if err != nil { + // Set the os_prefix= to slot-$inactive/ and add a sysota.slot={A,B} flag to + // kernel command line (informative only). + if err := slotBootCfg.SetOSPrefix(slottedBootDir); err != nil { return err } - defer func() { - e := trybootTxtFile.Close() - if err == nil { - err = e - } - }() - - if err := picfg.NewEncoder(trybootTxtFile).Encode(configTxt); err != nil { - return err - } + // Save the configuration as "tryboot.txt" in the real boot mount point. + slotBootCfg.BootMountPoint = pi.bootMountPoint + slotBootCfg.ConfigFileName = trybootTxtName - return nil + return slotBootCfg.Save() } // CommitSwitch re-configures a try-mode slot for continuous use. func (pi *PiBoot) CommitSwitch() error { - return os.Rename( - filepath.Join(pi.BootMountPoint, trybootTxtName), - filepath.Join(pi.BootMountPoint, configTxtName)) + return os.Rename(filepath.Join(pi.bootMountPoint, trybootTxtName), filepath.Join(pi.bootMountPoint, configTxtName)) } const rebootCommand = "reboot" @@ -298,45 +217,115 @@ func (pi *PiBoot) Reboot(flags boot.RebootFlags) error { return cmd.Run() } -// CancelSwitch removes the try-mode boot configuration file. +// CancelSwitch removes the try-mode boot configuration and cmdline file. func (pi *PiBoot) CancelSwitch() error { - if err := os.Remove(filepath.Join(pi.BootMountPoint, trybootTxtName)); err != nil && !os.IsNotExist(err) { + if err := os.Remove(filepath.Join(pi.bootMountPoint, trybootTxtName)); err != nil && !os.IsNotExist(err) { return err } + // TODO(zyga): perhaps remove the slot subdirectory as well? return nil } -// visitSection calls visitor function on each section in the config file. -// -// An optional predicate function may be used to visit only matching sections. -// The predicate is called with the effective fiters that are in effect at each -// phase of the traversal process. Sections can be mutated by the visitor. -func visitSections(configTxt *picfg.ConfigTxt, pred func([]condfilter.ConditionalFilter) bool, visitor func(sect *picfg.Section) error) error { - var tracker condfilter.StateTracker - - for idx := range configTxt.Sections { - sect := &configTxt.Sections[idx] - - if pred != nil && sect.Filter != "" { - filter, err := condfilter.Unmarshal([]byte(sect.Filter)) - if err != nil { - return err - } - - tracker.Filter(filter) - - if !pred(tracker.EffectiveFilters()) { - continue - } - } +// systemClass is the class of the RAUC image with a SystemOTA root file system + boot partition + kernel image. +const systemClass = "system" - if err := visitor(sect); err != nil { - return err - } +// ErrWouldClobber reports cases where the pre-install hook would clobber active slot. +var ErrWouldClobber = errors.New("pre-install hook would clobber active slot") + +// PreInstallHandler copies boot assets out of system.img and into the boot partition. +func (pi *PiBoot) PreInstallHandler(ctx *installhandler.HandlerContext) (err error) { + // If the handler is invoked but doesn't find a bundle and a target slot + // with "system" class then do nothing without reporting an error. This will + // allow systems to use other slots and classes without SystemOTA + // interfering with that. + systemImgInfo := ctx.ImageWithClass(systemClass) + targetSlotInfo := ctx.TargetSlotWithClass(systemClass) + + if systemImgInfo == nil || targetSlotInfo == nil { + return nil } - return nil + // Which slot are we targetting? + var targetSlot boot.Slot + if err := targetSlot.UnmarshalText([]byte(targetSlotInfo.BootName)); err != nil { + return err + } + + // Sanity check, ensure that we never clobber the active slot. This should + // never happen but it's just easy to check so let's check and have less + // things to worry about. + activeSlot, err := pi.QueryActive() + if err != nil { + return err + } + + if activeSlot == targetSlot { + return ErrWouldClobber + } + + slottedBootDir := slotPrefix(targetSlot) + + // Remove $boot/{squashfs-root/ and $boot/slot-$inactive to make space for + // the unpack/move we are about to do below. + if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, slottedBootDir)); err != nil { + return err + } + + const squashfsRootDir = "squashfs-root" + if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, squashfsRootDir)); err != nil { + return err + } + + // Extract the ($system.img)/boot directory from the system.img squashfs + // into $boot/squashfs-root/boot. + const bootDir = "boot" + + unsquashfsFrom := filepath.Join(ctx.BundleMountPoint, systemImgInfo.Name) + unsquashfsTo := filepath.Join(pi.bootMountPoint, squashfsRootDir) + unsquashfsWhat := bootDir + + if ctx.InitialHandlerPID != 0 { + // Note that the rauc-pre-pre-handler is invoked by RAUC inside a separate + // mount namespace where the bundle file is mounted. Look at the mounted + // file through the magic symlink in /proc/[pid]/root. + unsquashfsFrom = filepath.Join("/proc", strconv.Itoa(ctx.InitialHandlerPID), "root", unsquashfsFrom) + } + + if err := unsquashfs.Run(unsquashfsFrom, unsquashfsTo, &unsquashfs.Options{Force: true}, unsquashfsWhat); err != nil { + return err + } + + // Rename the extracted boot directory to $boot/slot-$inactive. + renameFrom := filepath.Join(pi.bootMountPoint, squashfsRootDir, bootDir) + renameTo := filepath.Join(pi.bootMountPoint, slottedBootDir) + + if err := os.Rename(renameFrom, renameTo); err != nil { + return err + } + + // Remove the now-empty squashfs directory. + if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, squashfsRootDir)); err != nil { + return err + } + + // Load the boot configuration from the slot directory and modify root=. + bootCfgDir := filepath.Join(pi.bootMountPoint, slotPrefix(targetSlot)) + + slotBootCfg, err := NewBootConfig(bootCfgDir, pi.revCode, pi.serialNum) + if err != nil { + return err + } + + // Set the root= argument to the block device matching the slot. + slotBootCfg.CmdLine.SetArg(argRoot, targetSlotInfo.DevicePath) + // Set the sysota.slot={A,B} kernel command line (informative only). + slotBootCfg.CmdLine.SetArg(argSysotaSlot, targetSlot.String()) + + // TODO(zyga): validate the final configuration. + + // Save the configuration back to the per-slot directory. + return slotBootCfg.Save() } // PostInstallHandler reboots the Raspberry Pi if necessary. @@ -354,7 +343,7 @@ func (pi *PiBoot) PostInstallHandler(ctx *installhandler.HandlerContext) error { // shouldReboot returns true if a slot with "system" class is being modified. func shouldReboot(ctx *installhandler.HandlerContext) bool { for _, slotID := range ctx.TargetSlotIDs { - if ctx.Slots[slotID].Class == "system" { + if ctx.Slots[slotID].Class == systemClass { return true } } diff --git a/boot/piboot/piboot_test.go b/boot/piboot/piboot_test.go index 49ba2cbc7879f5aec0df390d03b7dea919aa5434..d301c5cf638f8eeccce82a685445fe5e2f778487 100644 --- a/boot/piboot/piboot_test.go +++ b/boot/piboot/piboot_test.go @@ -4,6 +4,7 @@ package piboot_test import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -13,9 +14,9 @@ import ( "git.ostc-eu.org/OSTC/OHOS/components/sysota/boot" "git.ostc-eu.org/OSTC/OHOS/components/sysota/boot/piboot" - "git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/condfilter" "git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/pimodel" "git.ostc-eu.org/OSTC/OHOS/components/sysota/rauc/installhandler" + "git.ostc-eu.org/OSTC/OHOS/components/sysota/squashfstools/mksquashfs" "git.ostc-eu.org/OSTC/OHOS/components/sysota/testutil" ) @@ -26,6 +27,7 @@ type bootTest struct { bootDir string revCode pimodel.RevisionCode serialNum pimodel.SerialNumber + pi *piboot.PiBoot } var _ = Suite(&bootTest{ @@ -36,52 +38,53 @@ var _ = Suite(&bootTest{ func (s *bootTest) SetUpTest(c *C) { s.bootDir = c.MkDir() - s.MockConfigTxt(c, "") + pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) + c.Assert(err, IsNil) + + s.pi = pi } -func (s *bootTest) MockConfigTxt(c *C, text string) { - configName := filepath.Join(s.bootDir, "config.txt") - err := ioutil.WriteFile(configName, []byte(text), 0o644) +func (s *bootTest) MockBootFile(c *C, path, content string) { + name := filepath.Join(s.bootDir, path) + + err := os.MkdirAll(filepath.Dir(name), 0o755) c.Assert(err, IsNil) -} -func (s *bootTest) MockTrybootTxt(c *C, text string) { - configName := filepath.Join(s.bootDir, "tryboot.txt") - err := ioutil.WriteFile(configName, []byte(text), 0o644) + err = ioutil.WriteFile(name, []byte(content), 0o644) c.Assert(err, IsNil) } -func (s *bootTest) TestOpen(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - c.Check(pi.BootMountPoint, Equals, s.bootDir) +func (s *bootTest) MockConfigTxt(c *C, content string) { + s.MockBootFile(c, "config.txt", content) } -func (s *bootTest) TestQueryActive(c *C) { +func (s *bootTest) MockTrybootTxt(c *C, content string) { + s.MockBootFile(c, "tryboot.txt", content) +} + +func (s *bootTest) TestNew(c *C) { pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) c.Assert(err, IsNil) - s.MockConfigTxt(c, ``) + c.Check(pi.BootMountPoint(), Equals, s.bootDir) + c.Check(pi.RevisionCode(), Equals, s.revCode) + c.Check(pi.SerialNumber(), Equals, s.serialNum) +} - slot, err := pi.QueryActive() - c.Assert(err, ErrorMatches, "cannot determine active slot: inconsistent or corrupted configuration") +func (s *bootTest) TestQueryActive(c *C) { + slot, err := s.pi.QueryActive() + c.Assert(err, ErrorMatches, "cannot determine active slot: incompatible boot configuration") c.Check(slot, Equals, boot.InvalidSlot) - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=slot-a/\n") - slot, err = pi.QueryActive() + slot, err = s.pi.QueryActive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotA) - s.MockConfigTxt(c, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=slot-b/\n") - slot, err = pi.QueryActive() + slot, err = s.pi.QueryActive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotB) @@ -90,228 +93,145 @@ func (s *bootTest) TestQueryActive(c *C) { "cmdline=slot-b/cmdline.txt\n"+ "initramfs slot-c/initrd.img followkernel\n") - slot, err = pi.QueryActive() - c.Assert(err, ErrorMatches, "cannot determine active slot: inconsistent or corrupted configuration") + slot, err = s.pi.QueryActive() + c.Assert(err, ErrorMatches, "cannot determine active slot: incompatible boot configuration") c.Check(slot, Equals, boot.InvalidSlot) } -func (s *bootTest) TestQueryActiveWithFilterPredicate(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - - s.MockConfigTxt(c, ""+ - "[pi3]\n"+ - "kernel=slot-b/pi3-vmlinuz\n"+ - "cmdline=slot-b/pi3-cmdline.txt\n"+ - "initramfs slot-b/pi3-initrd.img followkernel\n"+ - "[pi4]\n"+ - "kernel=slot-a/pi4-vmlinuz\n"+ - "cmdline=slot-a/pi4-cmdline.txt\n"+ - "initramfs slot-a/pi4-initrd.img followkernel\n") - - pi.FilterPredicate = func(effective []condfilter.ConditionalFilter) bool { - for _, filter := range effective { - if model, ok := filter.(condfilter.PiModelFilter); ok { - return model == condfilter.Pi4 - } - } - - return true - } - slot, err := pi.QueryActive() - c.Assert(err, IsNil) - c.Check(slot, Equals, boot.SlotA) - - pi.FilterPredicate = func(effective []condfilter.ConditionalFilter) bool { - for _, filter := range effective { - if model, ok := filter.(condfilter.PiModelFilter); ok { - return model == condfilter.Pi3 - } - } - - return true - } - slot, err = pi.QueryActive() - c.Assert(err, IsNil) - c.Check(slot, Equals, boot.SlotB) -} - func (s *bootTest) TestQueryInactive(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - - s.MockConfigTxt(c, ``) - - slot, err := pi.QueryInactive() - c.Assert(err, ErrorMatches, "cannot determine inactive slot: inconsistent or corrupted configuration") + slot, err := s.pi.QueryInactive() + c.Assert(err, ErrorMatches, "cannot determine inactive slot: incompatible boot configuration") c.Check(slot, Equals, boot.InvalidSlot) - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=slot-a/") - slot, err = pi.QueryInactive() + slot, err = s.pi.QueryInactive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotB) - s.MockConfigTxt(c, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=slot-b/") - slot, err = pi.QueryInactive() + slot, err = s.pi.QueryInactive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotA) - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-c/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=potato/") - slot, err = pi.QueryInactive() - c.Assert(err, ErrorMatches, "cannot determine inactive slot: inconsistent or corrupted configuration") + slot, err = s.pi.QueryInactive() + c.Assert(err, ErrorMatches, "cannot determine inactive slot: incompatible boot configuration") c.Check(slot, Equals, boot.InvalidSlot) } func (s *bootTest) TestTrySwitchAtoB(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") + s.MockConfigTxt(c, "# pristine") + s.MockBootFile(c, "slot-b/config.txt", ""+ + "kernel=vmlinuz\n"+ + "cmdline=cmdline.txt\n"+ + "initramfs initrd.img followkernel\n") + s.MockBootFile(c, "slot-b/cmdline.txt", "... root=/dev/mmcblk0p4 sysota.slot=B") - err = pi.TrySwitch(boot.SlotB) + err := s.pi.TrySwitch(boot.SlotB) c.Assert(err, IsNil) + // The config.txt file is not touched. data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "config.txt")) c.Assert(err, IsNil) - c.Check(string(data), Equals, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") + c.Check(string(data), Equals, "# pristine") + // The tryboot.txt is slot-b/config.txt with os_prefix= set. data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "tryboot.txt")) c.Assert(err, IsNil) c.Check(string(data), Equals, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") -} + "kernel=vmlinuz\n"+ + "cmdline=cmdline.txt\n"+ + "initramfs initrd.img followkernel\n"+ + "os_prefix=slot-b/\n") -func (s *bootTest) TestTrySwitchBtoA(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) + // The cmdline.txt file is not modified. + data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "cmdline.txt")) c.Assert(err, IsNil) + c.Check(string(data), Equals, "... root=/dev/mmcblk0p4 sysota.slot=B") +} - s.MockConfigTxt(c, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") +func (s *bootTest) TestTrySwitchBtoA(c *C) { + s.MockConfigTxt(c, "# pristine") + s.MockBootFile(c, "slot-a/config.txt", ""+ + "kernel=vmlinuz\n"+ + "cmdline=cmdline.txt\n"+ + "initramfs initrd.img followkernel\n") + s.MockBootFile(c, "slot-a/cmdline.txt", "... root=/dev/mmcblk0p3 sysota.slot=A") - err = pi.TrySwitch(boot.SlotA) + err := s.pi.TrySwitch(boot.SlotA) c.Assert(err, IsNil) + // The config.txt file is not touched. data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "config.txt")) c.Assert(err, IsNil) - c.Check(string(data), Equals, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") + c.Check(string(data), Equals, "# pristine") + // The tryboot.txt is slot-a/config.txt with os_prefix= set. data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "tryboot.txt")) c.Assert(err, IsNil) c.Check(string(data), Equals, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") -} + "kernel=vmlinuz\n"+ + "cmdline=cmdline.txt\n"+ + "initramfs initrd.img followkernel\n"+ + "os_prefix=slot-a/\n") -func (s *bootTest) TestReboot(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) + // The cmdline.txt file is not modified. + data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-a", "cmdline.txt")) c.Assert(err, IsNil) + c.Check(string(data), Equals, "... root=/dev/mmcblk0p3 sysota.slot=A") +} +func (s *bootTest) TestReboot(c *C) { restore, rebootLog := s.MockCommand(c, "reboot") defer restore() - err = pi.Reboot(0) + err := s.pi.Reboot(0) c.Assert(err, IsNil) c.Check(rebootLog(c), DeepEquals, []string{`reboot`}) - err = pi.Reboot(boot.RebootTryBoot) + err = s.pi.Reboot(boot.RebootTryBoot) c.Assert(err, IsNil) c.Check(rebootLog(c), DeepEquals, []string{`reboot 0\ tryboot`}) } func (s *bootTest) TestCommitSwitch(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) + s.MockConfigTxt(c, "os_prefix=slot-a/") + s.MockTrybootTxt(c, "os_prefix=slot-b/") - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") - s.MockTrybootTxt(c, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") - - err = pi.CommitSwitch() + err := s.pi.CommitSwitch() c.Assert(err, IsNil) - slot, err := pi.QueryActive() + slot, err := s.pi.QueryActive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotB) - _, err = os.Stat(filepath.Join(pi.BootMountPoint, "tryboot.txt")) + _, err = os.Stat(filepath.Join(s.bootDir, "tryboot.txt")) c.Assert(os.IsNotExist(err), Equals, true) } func (s *bootTest) TestCancelSwitch(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - - s.MockConfigTxt(c, ""+ - "kernel=slot-a/vmlinuz\n"+ - "cmdline=slot-a/cmdline.txt\n"+ - "initramfs slot-a/initrd.img followkernel\n") - s.MockTrybootTxt(c, ""+ - "kernel=slot-b/vmlinuz\n"+ - "cmdline=slot-b/cmdline.txt\n"+ - "initramfs slot-b/initrd.img followkernel\n") + s.MockConfigTxt(c, "os_prefix=slot-a/") + s.MockTrybootTxt(c, "os_prefix=slot-b/") - err = pi.CancelSwitch() + err := s.pi.CancelSwitch() c.Assert(err, IsNil) - slot, err := pi.QueryActive() + slot, err := s.pi.QueryActive() c.Assert(err, IsNil) c.Check(slot, Equals, boot.SlotA) - _, err = os.Stat(filepath.Join(pi.BootMountPoint, "tryboot.txt")) + _, err = os.Stat(filepath.Join(s.pi.BootMountPoint(), "tryboot.txt")) c.Assert(os.IsNotExist(err), Equals, true) // Idempotent - err = pi.CancelSwitch() + err = s.pi.CancelSwitch() c.Assert(err, IsNil) } -func (s *bootTest) TestVisitSectionsCondfilterErrors(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - - pi.FilterPredicate = func([]condfilter.ConditionalFilter) bool { return true } - - s.MockConfigTxt(c, "[frozbonicator]") - - _, err = pi.QueryActive() - c.Assert(err, ErrorMatches, `cannot determine active slot: conditional filter error "\[frozbonicator\]": unknown filter`) -} - func (s *bootTest) TestPostInstallHandler(c *C) { - pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum) - c.Assert(err, IsNil) - restore, rebootLog := s.MockCommand(c, "reboot") defer restore() @@ -321,7 +241,7 @@ func (s *bootTest) TestPostInstallHandler(c *C) { "RAUC_SLOT_CLASS_1=potato", }) c.Check(err, IsNil) - err = pi.PostInstallHandler(ctx) + err = s.pi.PostInstallHandler(ctx) c.Check(err, IsNil) c.Check(rebootLog(c), DeepEquals, []string(nil)) @@ -331,7 +251,97 @@ func (s *bootTest) TestPostInstallHandler(c *C) { "RAUC_SLOT_CLASS_1=system", }) c.Assert(err, IsNil) - err = pi.PostInstallHandler(ctx) + err = s.pi.PostInstallHandler(ctx) c.Check(err, IsNil) c.Check(rebootLog(c), DeepEquals, []string{`reboot 0\ tryboot`}) } + +type preInstallHandlerSuite struct { + bootDir string + bundleDir string + systemDir string + + revCode pimodel.RevisionCode + serialNum pimodel.SerialNumber + + pi *piboot.PiBoot + ctx *installhandler.HandlerContext +} + +var _ = Suite(&preInstallHandlerSuite{ + // Arbitrary choice for as long a valid revision is used. + revCode: Pi4BRevCode, + serialNum: sampleSerial, +}) + +func (s *preInstallHandlerSuite) SetUpTest(c *C) { + var err error + + s.bootDir = c.MkDir() + s.bundleDir = c.MkDir() + s.systemDir = c.MkDir() + + s.pi, err = piboot.New(s.bootDir, s.revCode, s.serialNum) + c.Assert(err, IsNil) + + // Prepare minimum configuration of the current boot system. + // This is needed to detect updates which would clobber the current system. + c.Assert(ioutil.WriteFile(filepath.Join(s.bootDir, "config.txt"), []byte("os_prefix=slot-a/"), 0o600), IsNil) + + // Prepare an fake system image. + c.Assert(os.Mkdir(filepath.Join(s.systemDir, "boot"), 0o700), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/vmlinuz-5.10"), []byte("kernel"), 0o600), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/initrd-5.10.img"), []byte("initrd"), 0o600), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/config.txt"), []byte(""+ + "kernel=vmlinuz-5.10\n"+ + "initramfs initrd-5.10.img followkernel\n"), 0o600), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/cmdline.txt"), []byte("root=hardcoded"), 0o600), IsNil) + c.Assert(mksquashfs.Run(filepath.Join(s.bundleDir, "system.img"), nil, s.systemDir), IsNil) + + // Prepare the handler context + s.ctx, err = installhandler.NewHandlerContext([]string{ + fmt.Sprintf("RAUC_BUNDLE_MOUNT_POINT=%s", s.bundleDir), + "RAUC_IMAGE_CLASS_1=system", + "RAUC_IMAGE_NAME_1=system.img", + "RAUC_SLOT_BOOTNAME_1=B", + "RAUC_SLOT_BOOTNAME_2=A", + "RAUC_SLOT_CLASS_1=system", + "RAUC_SLOT_CLASS_2=system", + "RAUC_SLOT_DEVICE_1=/dev/mmcblk0p4", + "RAUC_SLOT_DEVICE_2=/dev/mmcblk0p3", + "RAUC_SLOTS=1 2 ", + "RAUC_TARGET_SLOTS=1 ", + }) + c.Assert(err, IsNil) +} + +func (s *preInstallHandlerSuite) TestPreInstallHandler(c *C) { + // Run the pre-install handler. + err := s.pi.PreInstallHandler(s.ctx) + c.Assert(err, IsNil) + + data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "config.txt")) + c.Assert(err, IsNil) + c.Check(string(data), Equals, ""+ + "kernel=vmlinuz-5.10\n"+ + "initramfs initrd-5.10.img followkernel\n") + + data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "cmdline.txt")) + c.Assert(err, IsNil) + c.Check(string(data), Equals, "root=/dev/mmcblk0p4 sysota.slot=B") + + data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "vmlinuz-5.10")) + c.Assert(err, IsNil) + c.Check(string(data), Equals, "kernel") + + data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "initrd-5.10.img")) + c.Assert(err, IsNil) + c.Check(string(data), Equals, "initrd") +} + +func (s *preInstallHandlerSuite) TestPreInstallHandlerSanityCheck(c *C) { + c.Assert(ioutil.WriteFile(filepath.Join(s.bootDir, "config.txt"), []byte("os_prefix=slot-b/"), 0o600), IsNil) + + err := s.pi.PreInstallHandler(s.ctx) + c.Assert(err, ErrorMatches, `pre-install hook would clobber active slot`) +} diff --git a/boot/piboot/tests/bin/check-slot-a-b-pristine.sh b/boot/piboot/tests/bin/check-slot-a-b-pristine.sh new file mode 100755 index 0000000000000000000000000000000000000000..c8b4cf1815a2aab19136d2b0972f25e05c62b4ed --- /dev/null +++ b/boot/piboot/tests/bin/check-slot-a-b-pristine.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Huawei Inc. + +set -e +set -x + +# Check that the slot-specific boot directories originally copied by the +# pre-install handler are pristine. This data is consistent with what is +# prepared by spread.yaml for the test suite boot/piboot/tests. + +test -e "$SYSOTA_BOOT_DIR"/slot-a/config.txt +test -e "$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt + +test -e "$SYSOTA_BOOT_DIR"/slot-b/config.txt +test -e "$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt + +# OS prefix is not set in either config file. +NOMATCH 'os_prefix=' "$SYSOTA_BOOT_DIR"/slot-a/config.txt +NOMATCH 'os_prefix=' "$SYSOTA_BOOT_DIR"/slot-b/config.txt + +# cmdline= is set to the data provided by RAUC. +MATCH 'root=/tmp/rauc-fake-system/slot-a' "$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt +MATCH 'root=/tmp/rauc-fake-system/slot-b' "$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt diff --git a/boot/piboot/tests/bundle-install/task.yaml b/boot/piboot/tests/bundle-install/task.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0d0b2d0baf13a864910fe21cdbd7616a49138804 --- /dev/null +++ b/boot/piboot/tests/bundle-install/task.yaml @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Huawei Inc. + +summary: play-through of "rauc install" +# This test doesn't run in LXD as RAUC cannot mount a squashfs due to the +# sandbox blocking that. It would only work if RAUC would be able to detect +# and handle that situation and switch to FUSE. This test works fine in qemu. +backends: [-lxd] +prepare: | + mkdir bundle-dir + mkdir system-dir + + # Create a fake system image, with an extremely barren /etc and /boot + # directories. As the validation logic in SysOTA improves, this may have to + # look more realistic, at least with regards to boot assets. + mkdir system-dir/etc + mkdir system-dir/boot + + cat <<OS_RELEASE > system-dir/etc/os-release + ID=sysota-test + ID_VERSION=1 + OS_RELEASE + + cat <<CONFIG_TXT > system-dir/boot/config.txt + kernel=new-kernel.img + CONFIG_TXT + + cat <<CMDLINE_TXT > system-dir/boot/cmdline.txt + new-cmdline-text root=hardcoded-root + CMDLINE_TXT + + mksquashfs system-dir bundle-dir/system.img -comp zstd + + # APPEND to the RAUC configuration file to set SystemOTA as custom + # bootloader handler, pre-install handler and post-install handler. + cat <<RAUC_SYSTEM_CONF >>/etc/rauc/system.conf + [handlers] + pre-install=/usr/libexec/sysota/rauc-pre-install-handler + post-install=/usr/libexec/sysota/rauc-post-install-handler + bootloader-custom-backend=/usr/libexec/sysota/rauc-custom-boot-handler + RAUC_SYSTEM_CONF + + # Create a fake rauc bundle with that system image + cat <<RAUC_MANIFEST >bundle-dir/manifest.raucm + [update] + compatible=OSTC SystemOTA Test Environment + version=1 + + [image.system] + filename=system.img + RAUC_MANIFEST + + rauc bundle --cert="$TEST_RAUC_KEYS_DIR/cert.pem" --key="$TEST_RAUC_KEYS_DIR/key.pem" bundle-dir bundle.img + + # Store /proc/cmdline and allow us to fake /proc/cmdline easily in the execute phase below. + cp /proc/cmdline cmdline.orig + cp cmdline.orig cmdline + printf "%s rauc.slot=A\n" "$(cat cmdline.orig)" > cmdline + mount --bind cmdline /proc/cmdline + + # Stop rauc and grab a cursor for the journal log + systemctl stop rauc.service || true + journalctl -u sysotad.service --cursor-file=cursor >/dev/null || true + + # Truncate the log files log and reset backend state + truncate --size=0 /var/{backend,pre-install,post-install}.log + echo 0 >/tmp/backend.state + +execute: | + rauc install bundle.img + + MATCH "os_prefix=slot-b/" "$SYSOTA_BOOT_DIR/tryboot.txt" + MATCH "kernel=new-kernel.img" "$SYSOTA_BOOT_DIR/tryboot.txt" + MATCH "new-cmdline-text root=/tmp/rauc-fake-system/slot-b sysota.slot=B" "$SYSOTA_BOOT_DIR/slot-b/cmdline.txt" + + test -f "$MOCK_REBOOT_LOG" + MATCH '^/usr/sbin/reboot 0\\ tryboot$' "$MOCK_REBOOT_LOG" + +restore: | + umount /proc/cmdline || true + + rm -rf system-dir bundle-dir + rm -f *.img + rm -f cursor + rm -f cmdline{,.orig} + +debug: | + cat /proc/cmdline + journalctl --cursor-file=cursor -u rauc.service diff --git a/boot/piboot/tests/cancel-switch/task.yaml b/boot/piboot/tests/cancel-switch/task.yaml index 74423f208d0fa5f99739a1dee7c71e64b7dc1d9c..173bcd79182942e2194f82bd5d51503612d3f7c6 100644 --- a/boot/piboot/tests/cancel-switch/task.yaml +++ b/boot/piboot/tests/cancel-switch/task.yaml @@ -5,16 +5,21 @@ summary: Integration tests for the PiBoot.CancelSwitch function details: | The CancelSwitch function removes the tryboot.txt file. prepare: | - cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/tryboot.txt - kernel=slot-b/kernel - cmdline=slot-b/cmdline.txt - initramfs slot-b/initrd.cpio - CONFIG_TXT + # Note that this test depends on the extensive boot configuration + # done at the suite prepare stage. + + # Prepare for the switch to slot B. This is tested separately in another test. + test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = "" execute: | test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CancelSwitch)" = "" + + # The inactive slot configuration files were removed. test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt + + # The active slot configuration files are pristine. test -e "$SYSOTA_BOOT_DIR"/config.txt - MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt -restore: | - test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txe - rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt + MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt + + # The slot-specific boot directories originally prepared by the pre-install + # handler are pristine. + check-slot-a-b-pristine.sh diff --git a/boot/piboot/tests/commit-switch/task.yaml b/boot/piboot/tests/commit-switch/task.yaml index c3accdfc117110bf8242decf4e2c4592da4b6dc0..ab5834439ac41e76beebf15ef9d9314fa55beee6 100644 --- a/boot/piboot/tests/commit-switch/task.yaml +++ b/boot/piboot/tests/commit-switch/task.yaml @@ -6,13 +6,21 @@ details: | The CommitSwitch function renames the tryboot.txt file to config.txt, thus cementing the single-boot configuration for subsequent reboots. prepare: | - cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/tryboot.txt - kernel=slot-b/kernel - cmdline=slot-b/cmdline.txt - initramfs slot-b/initrd.cpio - CONFIG_TXT + # Note that this test depends on the extensive boot configuration + # done at the suite prepare stage. + + # Prepare for the switch to slot B. This is tested separately in another test. + test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = "" execute: | test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CommitSwitch)" = "" + + # The tryboot.txt file is now gone. test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt + + # The config.txt now points to slot-b and a dedicated cmdline file. test -e "$SYSOTA_BOOT_DIR"/config.txt - MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/config.txt + MATCH 'os_prefix=slot-b/' "$SYSOTA_BOOT_DIR"/config.txt + + # The slot-specific boot directories originally prepared by the pre-install + # handler are pristine. + check-slot-a-b-pristine.sh diff --git a/boot/piboot/tests/smoke/task.yaml b/boot/piboot/tests/smoke/task.yaml deleted file mode 100644 index f37c9a1251a7b855ee2172735885cee43f808761..0000000000000000000000000000000000000000 --- a/boot/piboot/tests/smoke/task.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Huawei Inc. - -summary: Smoke tests for the boot protocol for the Raspberry Pi -execute: | - # Given the initial state, we see the A and B slots as active and inactive respectively - test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader QueryActive)" = 's "A"' - test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader QueryInactive)" = 's "B"' - - # The TrySwitch function creates a tryboot.txt file, replacing slot A with slot B without affecting config.txt - test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt - test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = "" - MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt - MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt - - # The Reboot function invoked with the try-boot flag reboots the system, - # passing special argument picked up by the Raspberry Pi kernel. - test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader Reboot u 1)" = "" - test -f "$MOCK_REBOOT_LOG" - MATCH 'reboot 0\\ tryboot' "$MOCK_REBOOT_LOG" - - # The CommitSwitch function replaces config.txt with tryboot.txt - test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CommitSwitch)" = "" - test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt - test -e "$SYSOTA_BOOT_DIR"/config.txt - MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/config.txt -restore: | - rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt diff --git a/boot/piboot/tests/try-switch/task.yaml b/boot/piboot/tests/try-switch/task.yaml index f627551259176fb8c4e4866b9cdeb6a0c2e5d28b..897e15a73e3010cb3b13f3050f7b831df89427ad 100644 --- a/boot/piboot/tests/try-switch/task.yaml +++ b/boot/piboot/tests/try-switch/task.yaml @@ -6,10 +6,24 @@ details: | The TrySwitch function creates a tryboot.txt file, replacing slot A with slot B without affecting config.txt. The contents of tryboot.txt is based on the contents of config.txt. -execute: | +prepare: | + # Note that this test depends on the extensive boot configuration + # done at the suite prepare stage. + + # There is no configuration for the inactive slot prior to our call. test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt +execute: | test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = "" - MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt - MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt -restore: | - rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt + MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt + + # The active slot configuration files are pristine. + test -e "$SYSOTA_BOOT_DIR"/config.txt + MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt + + # The inactive slot files are configured properly. + test -f "$SYSOTA_BOOT_DIR/tryboot.txt" + MATCH 'os_prefix=slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt + + # The slot-specific boot directories originally prepared by the pre-install + # handler are pristine. + check-slot-a-b-pristine.sh diff --git a/cspell.json b/cspell.json index 9a67566b0de9901e1c69d80e4aec186d29bf8749..5d5f0629a42a37671698fbd6192741f5d3351fb6 100644 --- a/cspell.json +++ b/cspell.json @@ -53,6 +53,7 @@ "fdatasync", // file data synchronization system call "FDCWD", // file descriptor of current working directory "findstring", // find string, a make function + "followkernel", // option specific to Raspberry Pi config.txt initramfs parameter "freedesktop", // free desktop organization "froz", // one of meta variable names "frozbonicator", // one of meta variable names diff --git a/spread.yaml b/spread.yaml index eff522f4649dd0c426234798f2184770e2a20f75..044a07589fd3ebebcd7b2045f772f29f557e6089 100644 --- a/spread.yaml +++ b/spread.yaml @@ -103,6 +103,7 @@ suites: # Note: the variable is not special to sysotad, it is only used in tests. SYSOTA_BOOT_DIR: /var/tmp/sysota-fake/boot MOCK_REBOOT_LOG: /var/tmp/reboot.log + PATH: $PATH:$SPREAD_PATH/tests/bin:$SPREAD_PATH/boot/piboot/tests/bin prepare: | # Prepare sysotad.conf file that forces the use of pi-boot and sets # the location of the boot directory to SYSOTA_BOOT_DIR. Note that @@ -125,28 +126,40 @@ suites: # In prepare-each we restore this state from a helper tarball. mkdir -p "$SYSOTA_BOOT_DIR" - mkdir -p "$SYSOTA_BOOT_DIR/slot-a/" + mkdir -p "$SYSOTA_BOOT_DIR"/slot-a + mkdir -p "$SYSOTA_BOOT_DIR"/slot-b - cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-a/config.txt - # The fake config.txt as plausibly copied by pre-install handler and as - # present in the system image. + # Data for the slot A (presumed active). + + cat <<SLOT_A_CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-a/config.txt kernel=kernel.img - cmdline=cmdline.txt - initramfs initrd.cpio - CONFIG_TXT + SLOT_A_CONFIG_TXT + + cat <<SLOT_A_CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt + cmdline-text root=$TEST_RAUC_FAKE_SYSTEM_DIR/slot-a sysota.slot=A + SLOT_A_CMDLINE_TXT - cat <<CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt - fake-unused-cmdline - CMDLINE_TXT + # Data for the slot B. + + cat <<SLOT_B_CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-b/config.txt + kernel=kernel.img + SLOT_B_CONFIG_TXT + + cat <<SLOT_B_CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt + cmdline-text root=$TEST_RAUC_FAKE_SYSTEM_DIR/slot-b sysota.slot=B + SLOT_B_CMDLINE_TXT + + # Boot configuration for the active slot. This configuration differs + # from that of slot-a/config.txt by the presence of os_prefix=. - # The config.txt file as derived from slot-a/config.txt with slot-a/ - # prefix added to kernel=, cmdline= and initramfs parameters. cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/config.txt - kernel=slot-a/kernel.img - cmdline=slot-a/cmdline.txt - initramfs slot-a/initrd.cpio + kernel=kernel.img + os_prefix=slot-a/ CONFIG_TXT + # Note that the vanilla data does not define tryboot.txt. This is + # done in individual tests which require it. + tar -zcf /var/tmp/vanilla-boot.tar.gz -C "$SYSOTA_BOOT_DIR" . # Restart sysotad.service at least once to make sure it load the new @@ -204,7 +217,6 @@ suites: restore-each: | rm -f "$MOCK_REBOOT_LOG" - test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt # Restore bootloader configuration to vanilla state. rm -rf "$SYSOTA_BOOT_DIR"