From c2aed22af785fa7084a80727df6cb15432e35ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20de=20Saint=20L=C3=A9ger?= Date: Wed, 6 Nov 2024 17:56:48 +0100 Subject: [PATCH 1/2] Add orange btrfs snapshotter support Changes: - ensure that kernel and initrd are relative links - add 'active_snap' variable to grub (managed by snapshotter) - add 'root_subpath' variable to grub - snapper can now be used on orange flavor (see notes) Notes: - 'active_snap' and 'root_subpath' allows grub to build a relative path to the kernel and initrd when btrfs_relative_path is not available. - Snapper works on orange flavor, however it can take several minutes before the daemon initialize in active or passive mode. If elemental upgrade is invoked during this time it will fail. --- examples/orange/Dockerfile | 8 ++++- examples/orange/snapshotter.yaml | 5 +++ pkg/action/init.go | 18 +++++++++-- pkg/constants/constants.go | 1 + .../grub-config/etc/elemental/grub.cfg | 31 ++++++++++++++----- .../etc/elemental/bootargs.cfg | 4 +-- pkg/snapshotter/btrfs.go | 6 ++-- pkg/snapshotter/snapper-backend.go | 11 +++++-- 8 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 examples/orange/snapshotter.yaml 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.go b/pkg/snapshotter/btrfs.go index f310893fc39..eec325411f4 100644 --- a/pkg/snapshotter/btrfs.go +++ b/pkg/snapshotter/btrfs.go @@ -265,8 +265,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 +380,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 +405,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 { From 2f7e7c15223a10830355cf7557b9bb9459f4c0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20de=20Saint=20L=C3=A9ger?= Date: Wed, 6 Nov 2024 18:40:37 +0100 Subject: [PATCH 2/2] Change btrfs state volumes detection btrfs volume IDs are not accurate with newer btrfs formated volumes. They are now matched using their names. Changes: - added '-a' to subvolume list to encure that full subvolume path is available - changed btrfs probe from ID match to name match - changed state volume match in findStateMount from target mount point to source subvolume --- pkg/snapshotter/btrfs-backend.go | 61 ++++++++++++++++++++------------ pkg/snapshotter/btrfs.go | 2 -- 2 files changed, 39 insertions(+), 24 deletions(-) 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 eec325411f4..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`