diff --git a/examples/orange/Dockerfile b/examples/orange/Dockerfile index 8ae2e55c259..541dc68e6ba 100644 --- a/examples/orange/Dockerfile +++ b/examples/orange/Dockerfile @@ -51,7 +51,10 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins locales \ kbd \ podman \ - xz-utils + btrfs-progs \ + btrfsmaintenance \ + xz-utils && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Hack to prevent systemd-firstboot failures while setting keymap, this is known # Debian issue (T_T) https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=790955 @@ -75,6 +78,9 @@ RUN cp /usr/share/systemd/tmp.mount /etc/systemd/system # the default cloud-init RUN locale-gen --lang en_US.UTF-8 +# Add default snapshotter setup +ADD snapshotter.yaml /etc/elemental/config.d/snapshotter.yaml + # Generate initrd with required elemental services RUN elemental --debug init -f diff --git a/examples/orange/snapshotter.yaml b/examples/orange/snapshotter.yaml new file mode 100644 index 00000000000..151542b3ae2 --- /dev/null +++ b/examples/orange/snapshotter.yaml @@ -0,0 +1,5 @@ +snapshotter: + type: btrfs + max-snaps: 4 + config: + snapper: false diff --git a/pkg/action/init.go b/pkg/action/init.go index 20c58125121..a200d322bb7 100644 --- a/pkg/action/init.go +++ b/pkg/action/init.go @@ -18,6 +18,7 @@ package action import ( "fmt" + "path/filepath" "strings" "github.com/rancher/elemental-toolkit/v2/pkg/constants" @@ -69,7 +70,12 @@ func RunInit(cfg *types.RunConfig, spec *types.InitSpec) error { if kernel != constants.KernelPath { cfg.Config.Logger.Debugf("Creating kernel symlink from %s to %s", kernel, constants.KernelPath) _ = cfg.Fs.Remove(constants.KernelPath) - err = cfg.Fs.Symlink(kernel, constants.KernelPath) + relKernel, err := filepath.Rel(filepath.Dir(constants.KernelPath), kernel) + if err != nil { + cfg.Config.Logger.Errorf("could set a relative path from '%s' to '%s': %v", constants.KernelPath, kernel, err) + return err + } + err = cfg.Fs.Symlink(relKernel, constants.KernelPath) if err != nil { cfg.Config.Logger.Errorf("failed creating kernel symlink") return err @@ -89,7 +95,7 @@ func RunInit(cfg *types.RunConfig, spec *types.InitSpec) error { cfg.Config.Logger.Errorf("dracut failed with output: %s", output) } - cfg.Config.Logger.Debugf("darcut output: %s", output) + cfg.Config.Logger.Debugf("dracut output: %s", output) initrd, err := utils.FindInitrd(cfg.Fs, "/") if err != nil || !strings.HasPrefix(initrd, constants.ElementalInitrd) { @@ -99,9 +105,15 @@ func RunInit(cfg *types.RunConfig, spec *types.InitSpec) error { cfg.Config.Logger.Debugf("Creating initrd symlink from %s to %s", initrd, constants.InitrdPath) _ = cfg.Fs.Remove(constants.InitrdPath) - err = cfg.Fs.Symlink(initrd, constants.InitrdPath) + relInitrd, err := filepath.Rel(filepath.Dir(constants.InitrdPath), initrd) + if err != nil { + cfg.Config.Logger.Errorf("could set a relative path from '%s' to '%s': %v", constants.InitrdPath, initrd, err) + return err + } + err = cfg.Fs.Symlink(relInitrd, constants.InitrdPath) if err != nil { cfg.Config.Logger.Errorf("failed creating initrd symlink") + return err } return err diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 8d601d1b54b..d615ffd186f 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -89,6 +89,7 @@ const ( GrubDefEntry = "Elemental" GrubFallback = "default_fallback" GrubPassiveSnapshots = "passive_snaps" + GrubActiveSnapshot = "active_snap" ElementalBootloaderBin = "/usr/lib/elemental/bootloader" // Mountpoints or links to images and partitions diff --git a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg index 060a5def4ca..679a448cd57 100644 --- a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg +++ b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg @@ -65,22 +65,37 @@ function set_loopdevice { ## Sources bootargs from the current volume function source_bootargs { - source (${volume})/etc/cos/bootargs.cfg - source (${volume})/etc/elemental/bootargs.cfg + source (${volume})/${root_subpath}etc/cos/bootargs.cfg + source (${volume})/${root_subpath}etc/elemental/bootargs.cfg } ## Defines the volume and image to boot from for active or passive boots function set_volume { if [ "${snapshotter}" == "btrfs" ]; then + # apply btrfs default subvolume if applicable set btrfs_relative_path="y" set volume="${root}" - if [ -n "${1}" ]; then - set img="@/.snapshots/${1}/snapshot" - btrfs-mount-subvol ($root) / ${img} + # check if active snap is defined with default top level volume + if [ -d "@/.snapshots/${active_snap}/snapshot" ]; then + if [ -n "${1}" ]; then + set img="@/.snapshots/${1}/snapshot" + else + set img="@/.snapshots/${active_snap}/snapshot" + fi + set root_subpath="${img}/" + else + # if not in top level use subvolume based mounts + set root_subpath="" + if [ -n "${1}" ]; then + set img="@/.snapshots/${1}/snapshot" + btrfs-mount-subvol ($root) / ${img} + fi fi elif [ -z "${1}" ]; then + set root_subpath="" set_loopdevice /.snapshots/active else + set root_subpath="" set img="/.snapshots/${1}/snapshot.img" set_loopdevice ${img} fi @@ -88,7 +103,7 @@ function set_volume { menuentry "${display_name}" --id active { set mode=active - search --no-floppy --label --set=root ${state_label} + search --no-floppy --set root --label ${state_label} set_volume source_bootargs linux (${volume})${kernel} ${kernelcmd} ${extra_cmdline} ${extra_active_cmdline} @@ -98,7 +113,7 @@ menuentry "${display_name}" --id active { for passive_snap in ${passive_snaps}; do menuentry "${display_name} (snapshot ${passive_snap})" --id passive${passive_snap} ${passive_snap} { set mode=passive - search --no-floppy --label --set=root ${state_label} + search --no-floppy --set root --label ${state_label} set_volume ${2} source_bootargs linux (${volume})${kernel} ${kernelcmd} ${extra_cmdline} ${extra_passive_cmdline} @@ -108,7 +123,7 @@ done menuentry "${display_name} recovery" --id recovery { set mode=recovery - search --no-floppy --label --set=root ${recovery_label} + search --no-floppy --set root --label ${recovery_label} # Check the presence of the image and fallback to legacy path if not present set img=/boot/recovery.img diff --git a/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg b/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg index 82030070752..33cf54c1212 100644 --- a/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg +++ b/pkg/features/embedded/grub-default-bootargs/etc/elemental/bootargs.cfg @@ -23,5 +23,5 @@ else set kernelcmd="console=tty1 console=ttyS0 root=LABEL=${state_label} ${img_arg} ${snap_arg} elemental.mode=${mode} elemental.oemlabel=${oem_label} panic=5 security=selinux fsck.mode=force fsck.repair=yes" fi -set kernel=/boot/vmlinuz -set initramfs=/boot/initrd +set kernel=/${root_subpath}boot/vmlinuz +set initramfs=/${root_subpath}boot/initrd diff --git a/pkg/snapshotter/btrfs-backend.go b/pkg/snapshotter/btrfs-backend.go index cc77fc15308..8c8ec85dd3e 100644 --- a/pkg/snapshotter/btrfs-backend.go +++ b/pkg/snapshotter/btrfs-backend.go @@ -119,27 +119,14 @@ func newBtrfsBackend(cfg *types.Config, maxSnapshots int) *btrfsBackend { // Probe tests the given device and returns the found state as a backendStat struct func (b *btrfsBackend) Probe(device string, mountpoint string) (backendStat, error) { - var rootVolume, snapshotsVolume bool var stat backendStat - volumes, err := b.getSubvolumes(mountpoint) + rootVolume, snapshotsVolume, err := b.getStateSubvolumes(mountpoint) if err != nil { return stat, err } - b.cfg.Logger.Debugf( - "Looking for subvolume ids %d and %d in subvolume list: %v", - rootSubvolID, snapshotsSubvolID, volumes, - ) - for _, vol := range volumes { - if vol.id == rootSubvolID { - rootVolume = true - } else if vol.id == snapshotsSubvolID { - snapshotsVolume = true - } - } - - if rootVolume && snapshotsVolume { + if (rootVolume != nil) && (snapshotsVolume != nil) { id, err := b.getActiveSnapshot(mountpoint) if err != nil { return stat, err @@ -411,7 +398,7 @@ func (b btrfsBackend) findSubvolumeByPath(rootDir, path string) (int, error) { // getSubvolumes lists all btrfs subvolumes for the given root func (b btrfsBackend) getSubvolumes(rootDir string) (btrfsSubvolList, error) { - out, err := b.cfg.Runner.Run("btrfs", "subvolume", "list", "--sort=path", rootDir) + out, err := b.cfg.Runner.Run("btrfs", "subvolume", "list", "-a", "--sort=path", rootDir) if err != nil { b.cfg.Logger.Errorf("failed listing btrfs subvolumes: %s", err.Error()) return nil, err @@ -419,6 +406,28 @@ func (b btrfsBackend) getSubvolumes(rootDir string) (btrfsSubvolList, error) { return parseVolumes(strings.TrimSpace(string(out))), nil } +func (b btrfsBackend) getStateSubvolumes(rootDir string) (rootVolume *btrfsSubvol, snapshotsVolume *btrfsSubvol, err error) { + volumes, err := b.getSubvolumes(rootDir) + if err != nil { + return nil, nil, err + } + + snapshots := filepath.Join(rootSubvol, snapshotsPath) + b.cfg.Logger.Debugf( + "Looking for subvolumes %s and %s in subvolume list: %v", + rootSubvol, snapshots, volumes, + ) + for _, vol := range volumes { + if vol.path == rootSubvol { + rootVolume = &vol + } else if vol.path == snapshots { + snapshotsVolume = &vol + } + } + + return rootVolume, snapshotsVolume, err +} + // getActiveSnapshot returns the active snapshot. Zero value means there is no active or default snapshot func (b btrfsBackend) getActiveSnapshot(rootDir string) (int, error) { out, err := b.cfg.Runner.Run("btrfs", "subvolume", "get-default", rootDir) @@ -448,7 +457,7 @@ func parseVolumes(rawBtrfsList string) btrfsSubvolList { match := re.FindStringSubmatch(strings.TrimSpace(scanner.Text())) if match != nil { id, _ := strconv.Atoi(match[1]) - path := match[2] + path := strings.TrimPrefix(match[2], "/") list = append(list, btrfsSubvol{id: id, path: path}) } } @@ -530,11 +539,19 @@ func (b btrfsBackend) findStateMount(device string) (rootDir string, stateMount if len(lineFields) != 2 { continue } - if strings.Contains(lineFields[1], constants.RunningStateDir) { - stateMount = lineFields[1] - } else if match := r.FindStringSubmatch(lineFields[0]); match != nil { - rootDir = lineFields[1] - snapshotID, _ = strconv.Atoi(match[1]) + + subStart := strings.Index(lineFields[0], "[/") + subEnd := strings.LastIndex(lineFields[0], "]") + + if subStart != -1 && subEnd != -1 { + subVolume := lineFields[0][subStart+2 : subEnd] + + if subVolume == rootSubvol { + stateMount = lineFields[1] + } else if match := r.FindStringSubmatch(subVolume); match != nil { + rootDir = lineFields[1] + snapshotID, _ = strconv.Atoi(match[1]) + } } } diff --git a/pkg/snapshotter/btrfs.go b/pkg/snapshotter/btrfs.go index f310893fc39..27435511f7f 100644 --- a/pkg/snapshotter/btrfs.go +++ b/pkg/snapshotter/btrfs.go @@ -31,8 +31,6 @@ import ( const ( rootSubvol = "@" - rootSubvolID = 257 - snapshotsSubvolID = 258 snapshotsPath = ".snapshots" snapshotPathTmpl = ".snapshots/%d/snapshot" snapshotPathRegex = `.snapshots/(\d+)/snapshot` @@ -265,8 +263,9 @@ func (b *Btrfs) CloseTransaction(snapshot *types.Snapshot) (err error) { return err } - _ = b.setBootloader() + // cleanup snapshots before setting bootloader otherwise deleted snapshots may show up in bootloader _ = b.backend.SnapshotsCleanup(b.rootDir) + _ = b.setBootloader(snapshot.ID) return nil } @@ -379,7 +378,7 @@ func (b *Btrfs) getPassiveSnapshots() ([]int, error) { } // setBootloader sets the bootloader variables to update new passives -func (b *Btrfs) setBootloader() error { +func (b *Btrfs) setBootloader(activeSnapshotID int) error { var passives, fallbacks []string b.cfg.Logger.Infof("Setting bootloader with current passive snapshots") @@ -404,6 +403,7 @@ func (b *Btrfs) setBootloader() error { envs := map[string]string{ constants.GrubFallback: fallbackList, constants.GrubPassiveSnapshots: snapsList, + constants.GrubActiveSnapshot: strconv.Itoa(activeSnapshotID), "snapshotter": constants.BtrfsSnapshotterType, } diff --git a/pkg/snapshotter/snapper-backend.go b/pkg/snapshotter/snapper-backend.go index e4173465d74..2e8ee28321a 100644 --- a/pkg/snapshotter/snapper-backend.go +++ b/pkg/snapshotter/snapper-backend.go @@ -30,8 +30,9 @@ import ( ) const ( - snapperRootConfig = "/etc/snapper/configs/root" - snapperSysconfig = "/etc/sysconfig/snapper" + snapperRootConfig = "/etc/snapper/configs/root" + snapperSysconfig = "/etc/sysconfig/snapper" + snapperDefaultconfig = "/etc/default/snapper" ) var _ subvolumeBackend = (*snapperBackend)(nil) @@ -220,7 +221,11 @@ func (s snapperBackend) configureSnapper(snapshotPath string) error { } sysconfigData := map[string]string{} - sysconfig := filepath.Join(snapshotPath, snapperSysconfig) + sysconfig := filepath.Join(snapshotPath, snapperDefaultconfig) + if ok, _ := utils.Exists(s.cfg.Fs, sysconfig); !ok { + sysconfig = filepath.Join(snapshotPath, snapperSysconfig) + } + if ok, _ := utils.Exists(s.cfg.Fs, sysconfig); ok { sysconfigData, err = utils.LoadEnvFile(s.cfg.Fs, sysconfig) if err != nil {