diff --git a/examples/orange/Dockerfile b/examples/orange/Dockerfile index dbb19f54f4..637e69b27d 100644 --- a/examples/orange/Dockerfile +++ b/examples/orange/Dockerfile @@ -62,10 +62,8 @@ ARG KBD=2.6.4 RUN curl -L https://mirrors.edge.kernel.org/pub/linux/utils/kbd/kbd-${KBD}.tar.xz --output kbd-${KBD}.tar.xz && \ tar xaf kbd-${KBD}.tar.xz && mkdir -p /usr/share/keymaps && cp -Rp kbd-${KBD}/data/keymaps/* /usr/share/keymaps/ -# Symlink grub2-editenv and snapper global config -RUN mkdir -p /etc/sysconfig && \ - ln -sf //etc/default/snapper /etc/sysconfig/snapper && \ - ln -sf /usr/bin/grub-editenv /usr/bin/grub2-editenv +# Symlink grub2-editenv +RUN ln -sf /usr/bin/grub-editenv /usr/bin/grub2-editenv # Just add the elemental cli COPY --from=toolkit /usr/bin/elemental /usr/bin/elemental diff --git a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg index b318b33130..679a448cd5 100644 --- a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg +++ b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg @@ -72,8 +72,11 @@ function source_bootargs { ## 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}" + # check if active snap is defined with default top level volume if [ -d "@/.snapshots/${active_snap}/snapshot" ]; then - set volume="${root}" if [ -n "${1}" ]; then set img="@/.snapshots/${1}/snapshot" else @@ -81,8 +84,7 @@ function set_volume { fi set root_subpath="${img}/" else - set btrfs_relative_path="y" - set volume="${root}" + # if not in top level use subvolume based mounts set root_subpath="" if [ -n "${1}" ]; then set img="@/.snapshots/${1}/snapshot" diff --git a/pkg/snapshotter/btrfs.go b/pkg/snapshotter/btrfs.go index 3717ecbfcd..5b6beec9ef 100644 --- a/pkg/snapshotter/btrfs.go +++ b/pkg/snapshotter/btrfs.go @@ -18,15 +18,16 @@ package snapshotter import ( "bufio" - "encoding/xml" "fmt" + "os" "path/filepath" "regexp" "slices" + "sort" "strconv" "strings" - "time" + "github.com/hashicorp/go-multierror" "github.com/rancher/elemental-toolkit/v2/pkg/constants" "github.com/rancher/elemental-toolkit/v2/pkg/elemental" "github.com/rancher/elemental-toolkit/v2/pkg/types" @@ -34,24 +35,18 @@ import ( ) const ( - rootSubvol = "@" - snapshotsPath = ".snapshots" - snapshotPathTmpl = ".snapshots/%d/snapshot" - snapshotPathRegex = `.snapshots/(\d+)/snapshot` - snapshotInfoPath = ".snapshots/%d/info.xml" - snapshotWorkDir = "snapshot.workDir" - dateFormat = "2006-01-02 15:04:05" - snapperRootConfig = "/etc/snapper/configs/root" - snapperSysconfig = "/etc/sysconfig/snapper" + topSubvolID = 5 + rootSubvol = "@" + snapshotsPath = ".snapshots" + snapshotPathTmpl = ".snapshots/%d/snapshot" + snapshotPathRegex = `.snapshots/(\d+)/snapshot` + snapshotWorkDir = "snapshot.workDir" + snapperBootstrapPath = ".bootstrap" + snapperRootConfig = "/etc/snapper/configs/root" + snapperSysconfig = "/etc/sysconfig/snapper" + snapperDefaultconfig = "/etc/default/snapper" ) -func configTemplatesPaths() []string { - return []string{ - "/etc/snapper/config-templates/default", - "/usr/share/snapper/config-templates/default", - } -} - var _ types.Snapshotter = (*Btrfs)(nil) type Btrfs struct { @@ -59,13 +54,11 @@ type Btrfs struct { snapshotterCfg types.SnapshotterConfig btrfsCfg types.BtrfsConfig rootDir string + snapshotsDir string + stateDir string efiDir string activeSnapshotID int bootloader types.Bootloader - installing bool - snapperArgs []string - snapshotsUmount func() error - snapshotsMount func() error } type btrfsSubvol struct { @@ -75,37 +68,6 @@ type btrfsSubvol struct { type btrfsSubvolList []btrfsSubvol -type Date time.Time - -type SnapperSnapshotXML struct { - XMLName xml.Name `xml:"snapshot"` - Type string `xml:"type"` - Num int `xml:"num"` - Date Date `xml:"date"` - Cleanup string `xml:"cleanup"` - Description string `xml:"description"` -} - -func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - t := time.Time(d) - v := t.Format(dateFormat) - return e.EncodeElement(v, start) -} - -func (d *Date) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { - var s string - err := dec.DecodeElement(&s, &start) - if err != nil { - return err - } - t, err := time.Parse(dateFormat, s) - if err != nil { - return err - } - *d = Date(t) - return nil -} - // NewLoopDeviceSnapshotter creates a new loop device snapshotter vased on the given configuration and the given bootloader func newBtrfsSnapshotter(cfg types.Config, snapCfg types.SnapshotterConfig, bootloader types.Bootloader) (types.Snapshotter, error) { if snapCfg.Type != constants.BtrfsSnapshotterType { @@ -132,8 +94,7 @@ func newBtrfsSnapshotter(cfg types.Config, snapCfg types.SnapshotterConfig, boot } return &Btrfs{ cfg: cfg, snapshotterCfg: snapCfg, btrfsCfg: *btrfsCfg, - bootloader: bootloader, snapshotsUmount: func() error { return nil }, - snapshotsMount: func() error { return nil }, + bootloader: bootloader, }, nil } @@ -142,21 +103,19 @@ func (b *Btrfs) InitSnapshotter(state *types.Partition, efiDir string) error { var ok bool b.cfg.Logger.Infof("Initiate btrfs snapshotter at %s", state.MountPoint) - b.rootDir = state.MountPoint + b.stateDir = state.MountPoint b.efiDir = efiDir b.cfg.Logger.Debug("Checking if essential subvolumes are already created") - ok, err = b.isInitiated(state.MountPoint) - if ok && elemental.IsActiveMode(b.cfg) || elemental.IsPassiveMode(b.cfg) { - return b.configureSnapperAndRootDir(state) - } - if err != nil { + if ok, err = b.isInitiated(state.MountPoint); ok { + if elemental.IsActiveMode(b.cfg) || elemental.IsPassiveMode(b.cfg) { + // if in active or passive mode, assume that root, state and snapshots are already mounted + return b.configureMountPointAndRootDir(state) + } + } else if err != nil { b.cfg.Logger.Errorf("failed loading initial snapshotter state: %v") return err - } - - if !ok { - b.installing = true + } else { b.cfg.Logger.Debug("Running initial btrfs configuration") err = b.setBtrfsForFirstTime(state) if err != nil { @@ -176,22 +135,23 @@ func (b *Btrfs) StartTransaction() (*types.Snapshot, error) { b.cfg.Logger.Info("Starting a btrfs snapshotter transaction") - if !b.installing && b.activeSnapshotID == 0 { + if b.stateDir == "" || b.snapshotsDir == "" { b.cfg.Logger.Errorf("Snapshotter should have been initalized before starting a transaction") return nil, fmt.Errorf("uninitialized snapshotter") } - if !b.btrfsCfg.DisableSnapper { - if !b.installing { - b.cfg.Logger.Infof("Creating a new snapshot from %d", b.activeSnapshotID) + if b.activeSnapshotID > 0 { + b.cfg.Logger.Infof("Creating a new snapshot from %d", b.activeSnapshotID) + if !b.btrfsCfg.DisableSnapper { args := []string{ "create", "--from", strconv.Itoa(b.activeSnapshotID), - "--read-write", "--print-number", "--description", - fmt.Sprintf("Update for snapshot %d", b.activeSnapshotID), - "-c", "number", "--userdata", "update-in-progress=yes", + "--read-write", + "--print-number", + "--description", fmt.Sprintf("Update for snapshot %d", b.activeSnapshotID), + "--cleanup-algorithm", "number", + "--userdata", "update-in-progress=yes", } - args = append(b.snapperArgs, args...) - cmdOut, err := b.cfg.Runner.Run("snapper", args...) + cmdOut, err := b.runCurrentSnapper(args...) if err != nil { b.cfg.Logger.Errorf("snapper failed to create a new snapshot: %v", err) return nil, err @@ -201,76 +161,78 @@ func (b *Btrfs) StartTransaction() (*types.Snapshot, error) { b.cfg.Logger.Errorf("failed parsing new snapshot ID") return nil, err } - - workingDir = filepath.Join(b.rootDir, snapshotsPath, strconv.Itoa(newID), snapshotWorkDir) - err = utils.MkdirAll(b.cfg.Fs, workingDir, constants.DirPerm) - if err != nil { - b.cfg.Logger.Errorf("failed creating the snapshot working directory: %v", err) - _ = b.DeleteSnapshot(newID) - return nil, err - } - path = filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, newID)) } else { - b.cfg.Logger.Info("Creating first root filesystem as a snapshot") - newID = 1 - err = utils.MkdirAll(b.cfg.Fs, filepath.Join(b.rootDir, snapshotsPath, strconv.Itoa(newID)), constants.DirPerm) + ids, err := b.GetSnapshots() if err != nil { + b.cfg.Logger.Errorf("unable to get btrfs snapshots") return nil, err } - cmdOut, err := b.cfg.Runner.Run( - "btrfs", "subvolume", "create", - filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, newID)), - ) - if err != nil { - b.cfg.Logger.Errorf("failed creating first snapshot volume: %s", string(cmdOut)) - return nil, err - } - snapperXML := filepath.Join(b.rootDir, fmt.Sprintf(snapshotInfoPath, newID)) - err = b.writeSnapperSnapshotXML(snapperXML, firstSnapperSnapshotXML()) - if err != nil { - b.cfg.Logger.Errorf("failed creating snapper info XML") - return nil, err + + // minimum ID + newID = 1 + + for _, id := range ids { + // search for next ID to be used + newID = max(id+1, newID) } - workingDir = filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, newID)) - path = workingDir } - } else { - ids, err := b.GetSnapshots() + + path = filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, newID)) + workingDir = filepath.Join(filepath.Dir(path), snapshotWorkDir) + err = utils.MkdirAll(b.cfg.Fs, workingDir, constants.DirPerm) if err != nil { - b.cfg.Logger.Errorf("unable to get btrfs snapshots") + b.cfg.Logger.Errorf("failed creating the snapshot working directory: %v", err) + _ = b.DeleteSnapshot(newID) return nil, err } - // minimum ID (in case of initial installation) - newID = 1 - - for _, id := range ids { - // search for next ID to be used - newID = max(id+1, newID) + if b.btrfsCfg.DisableSnapper { + source := filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, b.activeSnapshotID)) + cmdOut, err := b.cfg.Runner.Run("btrfs", "subvolume", "snapshot", "-i", "1/0", source, path) + if err != nil { + b.cfg.Logger.Errorf("failed creating snapshot volume: %s", string(cmdOut)) + _ = b.DeleteSnapshot(newID) + return nil, err + } } + } else { + b.cfg.Logger.Info("Bootstraping first root filesystem") - // compute snapshot workdir and path - path = filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, newID)) - workingDir = path + ".inprogress" + // assume ID of snapshot will be '1' + newID = 1 + path = filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, newID)) - // create tree up to snapshot subvolume - err = utils.MkdirAll(b.cfg.Fs, filepath.Dir(path), constants.DirPerm) - if err != nil { - return nil, err - } + if !b.btrfsCfg.DisableSnapper { + workingDir = filepath.Join(b.stateDir, snapperBootstrapPath) - if b.activeSnapshotID == 0 { - cmdOut, err := b.cfg.Runner.Run("btrfs", "subvolume", "create", workingDir) + // make all parent directories but not target as it will be a subvolume + err = utils.MkdirAll(b.cfg.Fs, filepath.Dir(workingDir), constants.DirPerm) + if err != nil { + return nil, err + } + cmdOut, err := b.cfg.Runner.Run( + "btrfs", "subvolume", "create", "-i", "1/0", + workingDir, + ) if err != nil { b.cfg.Logger.Errorf("failed creating first snapshot volume: %s", string(cmdOut)) - _ = b.DeleteSnapshot(newID) return nil, err } } else { - source := filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, b.activeSnapshotID)) - cmdOut, err := b.cfg.Runner.Run("btrfs", "subvolume", "snapshot", source, workingDir) + workingDir = filepath.Join(filepath.Dir(path), snapshotWorkDir) + + // create snapshot working dir + err = utils.MkdirAll(b.cfg.Fs, workingDir, constants.DirPerm) if err != nil { - b.cfg.Logger.Errorf("failed creating snapshot volume: %s", string(cmdOut)) + b.cfg.Logger.Errorf("failed creating the snapshot working directory: %v", err) + _ = b.DeleteSnapshot(newID) + return nil, err + } + + // create target subvolume + cmdOut, err := b.cfg.Runner.Run("btrfs", "subvolume", "create", "-i", "1/0", path) + if err != nil { + b.cfg.Logger.Errorf("failed creating first snapshot volume: %s", string(cmdOut)) _ = b.DeleteSnapshot(newID) return nil, err } @@ -301,128 +263,133 @@ func (b *Btrfs) StartTransaction() (*types.Snapshot, error) { func (b *Btrfs) CloseTransactionOnError(snapshot *types.Snapshot) (err error) { if snapshot.InProgress { err = b.cfg.Mounter.Unmount(snapshot.MountPoint) - } - defer func() { - newErr := b.snapshotsUmount() - if err == nil { - err = newErr + + if err != nil { + b.cfg.Logger.Errorf("unable to unmount snapshot mountpoint %s: %v", snapshot.MountPoint, err) } - }() - err = b.DeleteSnapshot(snapshot.ID) - return err + } + + return b.DeleteSnapshot(snapshot.ID) +} + +func (b *Btrfs) syncSnapshotWorkdir(snapshot *types.Snapshot) (err error) { + err = utils.MirrorData(b.cfg.Logger, b.cfg.Runner, b.cfg.Fs, snapshot.WorkDir, snapshot.Path) + if err != nil { + b.cfg.Logger.Errorf("failed syncing working directory with snapshot directory") + return err + } + + err = b.cfg.Fs.RemoveAll(snapshot.WorkDir) + if err != nil { + b.cfg.Logger.Errorf("failed deleting snapshot's workdir '%s': %s", snapshot.WorkDir, err) + return err + } + + snapshot.WorkDir = snapshot.Path + return nil } func (b *Btrfs) CloseTransaction(snapshot *types.Snapshot) (err error) { var cmdOut []byte - var subvolID int if !snapshot.InProgress { b.cfg.Logger.Debugf("No transaction to close for snapshot %d workdir", snapshot.ID) return fmt.Errorf("given snapshot is not in progress") } - defer func() { - if err != nil { - _ = b.DeleteSnapshot(snapshot.ID) - } - newErr := b.snapshotsUmount() - if err == nil { - err = newErr - } - }() b.cfg.Logger.Infof("Closing transaction for snapshot %d workdir", snapshot.ID) - // Make sure snapshots mountpoint folder is part of the resulting snapshot image - err = utils.MkdirAll(b.cfg.Fs, filepath.Join(snapshot.WorkDir, snapshotsPath), constants.DirPerm) + snapshotsBind := map[string]string{b.snapshotsDir: filepath.Join("/", snapshotsPath)} + + b.cfg.Logger.Debugf("Unmount %s", snapshot.MountPoint) + err = b.cfg.Mounter.Unmount(snapshot.MountPoint) if err != nil { - b.cfg.Logger.Errorf("failed creating snapshots folder: %v", err) + b.cfg.Logger.Errorf("failed umounting snapshot %d workdir bind mount", snapshot.ID) return err } if !b.btrfsCfg.DisableSnapper { - // Configure snapper - err = b.configureSnapper(snapshot) - if err != nil { - b.cfg.Logger.Errorf("failed configuring snapper: %v", err) - return err - } - - b.cfg.Logger.Debugf("Unmount %s", snapshot.MountPoint) - err = b.cfg.Mounter.Unmount(snapshot.MountPoint) - if err != nil { - b.cfg.Logger.Errorf("failed umounting snapshot %d workdir bind mount", snapshot.ID) - return err - } + // when bootstraping snapper, snapshot is not created yet + if ok, err := utils.Exists(b.cfg.Fs, snapshot.Path); !ok { + // Configure snapper + err = b.configureSnapshotSnapper(snapshot) + if err != nil { + b.cfg.Logger.Errorf("failed configuring snapper: %v", err) + return err + } + cmdOut, err := b.runSnapshotSnapper(snapshot.WorkDir, snapshotsBind, + "--config", "root", "create", + "--read-write", + "--cleanup-algorithm", "number", + "--description", "first root filesystem", + "--userdata", "update-in-progress=no") + if err != nil { + b.cfg.Logger.Errorf("unable to create initial snapshot: %v", err) + b.cfg.Logger.Debugf("snapper output: %s", string(cmdOut)) + return err + } - if !b.installing { - err = utils.MirrorData(b.cfg.Logger, b.cfg.Runner, b.cfg.Fs, snapshot.WorkDir, snapshot.Path) + // we now have the first snapshot. delete bootstrap volume + _, err = b.deleteBootstrapSubvolume() if err != nil { - b.cfg.Logger.Errorf("failed syncing working directory with snapshot directory") + b.cfg.Logger.Errorf("can't delete bootstrap subvolume: %v", err) return err } + snapshot.WorkDir = snapshot.Path + } else if err != nil { + b.cfg.Logger.Errorf("unable to stat snapshot %d workdir", snapshot.ID) + return err + } else { + b.syncSnapshotWorkdir(snapshot) - err = b.cfg.Fs.RemoveAll(snapshot.WorkDir) + // Configure snapper after sync (otherwise, snapper reports that workdir is not a volume) + err = b.configureSnapshotSnapper(snapshot) if err != nil { - b.cfg.Logger.Errorf("failed deleting snapshot's workdir '%s': %s", snapshot.WorkDir, err) + b.cfg.Logger.Errorf("failed configuring snapper: %v", err) return err } args := []string{"modify", "--userdata", "update-in-progress=no", strconv.Itoa(snapshot.ID)} - args = append(b.snapperArgs, args...) - cmdOut, err = b.cfg.Runner.Run("snapper", args...) + cmdOut, err := b.runCurrentSnapper(args...) if err != nil { - b.cfg.Logger.Errorf("failed setting read only property to snapshot %d: %s", snapshot.ID, string(cmdOut)) + b.cfg.Logger.Errorf("failed updating snapshot user data %d: %s", snapshot.ID, string(cmdOut)) return err } } } else { - b.cfg.Logger.Debugf("Unmount %s", snapshot.MountPoint) - err = b.cfg.Mounter.Unmount(snapshot.MountPoint) - if err != nil { - b.cfg.Logger.Errorf("failed umounting snapshot %d workdir bind mount", snapshot.ID) - return err - } + b.syncSnapshotWorkdir(snapshot) + } - // rename subvolume to its definitive name - err := b.cfg.Fs.Rename(snapshot.WorkDir, snapshot.Path) - if err != nil { - b.cfg.Logger.Errorf("unable to set snapshot to its definitive path %d: %v", snapshot.ID, err) - return err - } + // Make sure snapshots mountpoint folder is part of the resulting snapshot image + err = utils.MkdirAll(b.cfg.Fs, filepath.Join(snapshot.Path, snapshotsPath), constants.DirPerm) + if err != nil { + b.cfg.Logger.Errorf("failed creating snapshots folder: %v", err) + return err } - extraBind := map[string]string{filepath.Join(b.rootDir, snapshotsPath): filepath.Join("/", snapshotsPath)} - err = elemental.ApplySELinuxLabels(b.cfg, snapshot.Path, extraBind) + err = elemental.ApplySELinuxLabels(b.cfg, snapshot.Path, snapshotsBind) if err != nil { b.cfg.Logger.Errorf("failed relabelling snapshot path: %s", snapshot.Path) return err } + // sets the snapshot readonly cmdOut, err = b.cfg.Runner.Run("btrfs", "property", "set", snapshot.Path, "ro", "true") if err != nil { b.cfg.Logger.Errorf("failed setting read only property to snapshot %d: %s", snapshot.ID, string(cmdOut)) return err } + defaultSubvolID := topSubvolID if !b.btrfsCfg.DisableDefaultSubVolume { - subvolID, err = b.findSubvolumeByPath(fmt.Sprintf(snapshotPathTmpl, snapshot.ID)) + defaultSubvolID, err = b.findSubvolumeByPath(snapshot.Path) if err != nil { b.cfg.Logger.Error("failed finding subvolume") return err } - - cmdOut, err = b.cfg.Runner.Run("btrfs", "subvolume", "set-default", strconv.Itoa(subvolID), snapshot.Path) - if err != nil { - b.cfg.Logger.Errorf("failed setting default subvolume property to snapshot %d: %s", snapshot.ID, string(cmdOut)) - return err - } } else { - _, _, _, stateDir, err := findStateMount(b.cfg.Runner, b.rootDir) - if err != nil { - b.cfg.Logger.Errorf("unable to find btrfs state directory: %s", err.Error()) - return err - } - - activeSnap := filepath.Join(stateDir, constants.ActiveSnapshot) + // when not using default subvolume, a symlink is used in the state directory + // to identify active snapshot (loopdevice snapshotter use same feature) + activeSnap := filepath.Join(b.stateDir, constants.ActiveSnapshot) linkDst := fmt.Sprintf(snapshotPathTmpl, snapshot.ID) b.cfg.Logger.Debugf("creating symlink %s to %s", activeSnap, linkDst) _ = b.cfg.Fs.Remove(activeSnap) @@ -437,52 +404,79 @@ func (b *Btrfs) CloseTransaction(snapshot *types.Snapshot) (err error) { } } - _ = b.setBootloader() + // ensure default subvolume is always up to date + cmdOut, err = b.cfg.Runner.Run("btrfs", "subvolume", "set-default", strconv.Itoa(defaultSubvolID), snapshot.Path) + if err != nil { + b.cfg.Logger.Errorf("failed setting default subvolume property to snapshot %d: %s", snapshot.ID, string(cmdOut)) + return err + } + + err = b.updateBtrfsContext(b.stateDir) + if err != nil { + b.cfg.Logger.Errorf("got error updating snapshotter context %v", err) + return err + } - if (!b.btrfsCfg.DisableSnapper) && (!b.installing) { - args := []string{"cleanup", "--path", filepath.Join(b.rootDir, snapshotsPath), "number"} - args = append(b.snapperArgs, args...) - _, _ = b.cfg.Runner.Run("snapper", args...) + // snapper has a special algorithm to clean snapshots. + // do not rely on it, use an implementation similar to loop device + // to handle pure btrfs snapshotter using same code + err = b.cleanOldSnapshots() + if err != nil { + b.cfg.Logger.Warnf("got error cleaning old snapshots %v", err) } + _ = b.setBootloader() + return nil } func (b *Btrfs) DeleteSnapshot(id int) error { + b.cfg.Logger.Infof("Deleting snapshot %d", id) var cmdOut []byte - b.cfg.Logger.Infof("Deleting snapshot %d", id) + if id <= 0 { + // ignore invalid ids + return nil + } - snapshots, err := b.GetSnapshots() - if err != nil { - b.cfg.Logger.Errorf("failed listing available snapshots: %v", err) + if b.activeSnapshotID == id { + err := fmt.Errorf("active snapshot can't be deleted") return err } - if !slices.Contains(snapshots, id) { - b.cfg.Logger.Debugf("snapshot %d not found, nothing has been deleted", id) - return nil - } if !b.btrfsCfg.DisableSnapper { + if (b.activeSnapshotID == 0) && (id == 1) { + // handle special bootstrap volume + ok, err := b.deleteBootstrapSubvolume() + + if ok { + return err + } else { + b.cfg.Logger.Debugf("snapshot %d not found, nothing has been deleted", id) + return nil + } + } + + snapshots, err := b.GetSnapshots() + if err != nil { + b.cfg.Logger.Errorf("failed listing available snapshots: %v", err) + return err + } + if !slices.Contains(snapshots, id) { + b.cfg.Logger.Debugf("snapshot %d not found, nothing has been deleted", id) + return nil + } + args := []string{"delete", "--sync", strconv.Itoa(id)} - args = append(b.snapperArgs, args...) - cmdOut, err = b.cfg.Runner.Run("snapper", args...) + cmdOut, err := b.runCurrentSnapper(args...) if err != nil { b.cfg.Logger.Errorf("snapper failed deleting snapshot %d: %s", id, string(cmdOut)) return err } } else { // Remove btrfs subvolume first - basePath := filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, id)) - snapshotDir := basePath - if ok, err := utils.Exists(b.cfg.Fs, snapshotDir, false); !ok { - snapshotDir = basePath + ".inprogress" - } else if err != nil { - b.cfg.Logger.Errorf("unable to stat snapshot subvolume %d: %s", id, snapshotDir) - return err - } - - if ok, err := utils.Exists(b.cfg.Fs, snapshotDir, false); ok { + snapshotDir := filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, id)) + if ok, err := utils.Exists(b.cfg.Fs, snapshotDir); ok { args := []string{"subvolume", "delete", "-c", snapshotDir} cmdOut, err = b.cfg.Runner.Run("btrfs", args...) if err != nil { @@ -498,9 +492,14 @@ func (b *Btrfs) DeleteSnapshot(id int) error { // then remove associated directory parent := filepath.Dir(snapshotDir) - err = b.cfg.Fs.RemoveAll(parent) - if err != nil { - b.cfg.Logger.Errorf("failed deleting snapshot parent directory '%s': %s", parent, err) + if ok, err := utils.Exists(b.cfg.Fs, parent); ok { + err := b.cfg.Fs.RemoveAll(parent) + if err != nil { + b.cfg.Logger.Errorf("failed deleting snapshot directory '%s': %s", parent, err) + return err + } + } else if err != nil { + b.cfg.Logger.Errorf("unable to stat snapshot directory %d: %s", id, snapshotDir) return err } } @@ -509,81 +508,53 @@ func (b *Btrfs) DeleteSnapshot(id int) error { } func (b *Btrfs) GetSnapshots() (snapshots []int, err error) { - if !b.btrfsCfg.DisableSnapper { - var ok bool + ids := []int{} - if ok, err = b.isInitiated(b.rootDir); ok { - // Check if snapshots subvolume is mounted - snapshotsSubolume := filepath.Join(b.rootDir, fmt.Sprintf(snapshotPathTmpl, b.activeSnapshotID), snapshotsPath) - if notMnt, _ := b.cfg.Mounter.IsLikelyNotMountPoint(snapshotsSubolume); notMnt { - err = b.snapshotsMount() - if err != nil { - return nil, err - } - defer func() { - nErr := b.snapshotsUmount() - if err == nil && nErr != nil { - err = nErr - snapshots = nil - } - }() - } - snapshots, err = b.loadSnapshots() - if err != nil { - return nil, err - } - return snapshots, err - } else if err != nil { - return nil, err - } - } else { - // btrfs subvolume list is not safe here. Use snapshot directory - _, _, snapshotDir, _, err := findStateMount(b.cfg.Runner, b.rootDir) + if b.stateDir == "" { + return ids, fmt.Errorf("unable to find btrfs state directory") + } + if !b.btrfsCfg.DisableSnapper { + err = b.loadSnapperSnapshots(&ids) if err != nil { - b.cfg.Logger.Errorf("unable to find btrfs snapshots directory: %v", err) - return nil, err + return ids, err } - - list, err := b.cfg.Fs.ReadDir(snapshotDir) + } else { + list, err := b.cfg.Fs.ReadDir(b.snapshotsDir) if err != nil { b.cfg.Logger.Errorf("failed listing btrfs snapshots directory: %v", err) - return nil, err + return ids, err } re := regexp.MustCompile(`^\d+$`) - ids := []int{} for _, entry := range list { if entry.IsDir() { entryName := entry.Name() if re.MatchString(entryName) { - exists, _ := utils.Exists(b.cfg.Fs, filepath.Join(snapshotDir, entryName, "snapshot"), false) - inprogress, _ := utils.Exists(b.cfg.Fs, filepath.Join(snapshotDir, entryName, "snapshot.inprogress"), false) - if exists || inprogress { - id, _ := strconv.Atoi(entryName) + id, _ := strconv.Atoi(entryName) + snapshotDir := filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, id)) + + if validateDirectory(b.cfg.Fs, snapshotDir) != "" { ids = append(ids, id) } } } } - return ids, nil } - return []int{}, err + return ids, err } -func (b *Btrfs) loadSnapshots() ([]int, error) { - ids := []int{} +func (b *Btrfs) loadSnapperSnapshots(ids *[]int) error { re := regexp.MustCompile(`^(\d+),(yes|no),(yes|no)$`) args := []string{"--csvout", "list", "--columns", "number,default,active"} - args = append(b.snapperArgs, args...) - cmdOut, err := b.cfg.Runner.Run("snapper", args...) + cmdOut, err := b.runCurrentSnapper(args...) if err != nil { // snapper tries to relabel even when listing subvolumes, skip this error. if !strings.HasPrefix(string(cmdOut), "fsetfilecon on") { b.cfg.Logger.Errorf("failed collecting current snapshots: %s", string(cmdOut)) - return nil, err + return err } } @@ -596,14 +567,11 @@ func (b *Btrfs) loadSnapshots() ([]int, error) { if id == 0 { continue } - ids = append(ids, id) - if match[2] == "yes" { - b.activeSnapshotID = id - } + *ids = append(*ids, id) } } - return ids, nil + return nil } // SnapshotImageToSource converts the given snapshot into an ImageSource. This is useful to deploy a system @@ -622,22 +590,44 @@ func (b *Btrfs) SnapshotToImageSource(snap *types.Snapshot) (*types.ImageSource, } func (b *Btrfs) 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()) + b.cfg.Logger.Errorf("failed listing btrfs subvolumes: %v", err) return nil, err } return b.parseVolumes(strings.TrimSpace(string(out))), nil } +func (b *Btrfs) 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 +} + func (b *Btrfs) getActiveSnapshot() (int, error) { + re := regexp.MustCompile(snapshotPathRegex) if !b.btrfsCfg.DisableDefaultSubVolume { - out, err := b.cfg.Runner.Run("btrfs", "subvolume", "get-default", b.rootDir) + out, err := b.cfg.Runner.Run("btrfs", "subvolume", "get-default", b.stateDir) if err != nil { - b.cfg.Logger.Errorf("failed listing btrfs subvolumes: %s", err.Error()) + b.cfg.Logger.Errorf("failed getting default btrfs subvolume: %v", err) return 0, err } - re := regexp.MustCompile(snapshotPathRegex) list := b.parseVolumes(strings.TrimSpace(string(out))) for _, v := range list { match := re.FindStringSubmatch(v.path) @@ -647,21 +637,17 @@ func (b *Btrfs) getActiveSnapshot() (int, error) { } } } else { - _, _, _, stateDir, err := findStateMount(b.cfg.Runner, b.rootDir) - if err != nil { - b.cfg.Logger.Errorf("unable to find btrfs sate directory: %s", err.Error()) - return 0, err - } - - activeSnap := filepath.Join(stateDir, constants.ActiveSnapshot) + activeSnap := filepath.Join(b.stateDir, constants.ActiveSnapshot) activePath, err := b.cfg.Fs.Readlink(activeSnap) if err != nil { - b.cfg.Logger.Errorf("failed reading active symlink %s: %s", activeSnap, err.Error()) + if os.IsNotExist(err) { + return 0, nil + } + b.cfg.Logger.Errorf("failed reading active symlink %s: %v", activeSnap, err) return 0, err } b.cfg.Logger.Debugf("active snapshot path is %s", activePath) - re := regexp.MustCompile(snapshotPathRegex) match := re.FindStringSubmatch(activePath) if match != nil { id, _ := strconv.Atoi(match[1]) @@ -680,119 +666,44 @@ func (b *Btrfs) 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}) } } return list } -func (b *Btrfs) 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 -} - func (b *Btrfs) isInitiated(rootDir string) (bool, error) { - if b.activeSnapshotID > 0 { - return true, nil - } - if b.installing { - return false, nil - } - rootVolume, snapshotsVolume, err := b.getStateSubvolumes(rootDir) if err != nil { return false, err } if (rootVolume != nil) && (snapshotsVolume != nil) { - id, err := b.getActiveSnapshot() - if err != nil { - return false, err - } - if id > 0 { - b.activeSnapshotID = id - return true, nil - } + return true, nil } return false, nil } -func firstSnapperSnapshotXML() SnapperSnapshotXML { - return SnapperSnapshotXML{ - Type: "single", - Num: 1, - Date: Date(time.Now()), - Description: "first root filesystem", - Cleanup: "number", - } -} - -func (b *Btrfs) writeSnapperSnapshotXML(filepath string, snapshot SnapperSnapshotXML) error { - data, err := xml.MarshalIndent(snapshot, "", " ") - if err != nil { - b.cfg.Logger.Errorf("failed marhsalling snapper's snapshot XML: %v", err) - return err - } - err = b.cfg.Fs.WriteFile(filepath, data, constants.FilePerm) - if err != nil { - b.cfg.Logger.Errorf("failed writing snapper's snapshot XML: %v", err) - return err - } - return nil -} - func (b *Btrfs) findSubvolumeByPath(path string) (int, error) { - subvolumes, err := b.getSubvolumes(b.rootDir) + out, err := b.cfg.Runner.Run("btrfs", "inspect-internal", "rootid", path) if err != nil { - b.cfg.Logger.Errorf("failed loading subvolumes: %v", err) + b.cfg.Logger.Errorf("failed inspecting btrfs subvolume path: %v", err) return 0, err } - - for _, subvol := range subvolumes { - if strings.Contains(subvol.path, path) { - return subvol.id, nil - } - } - - b.cfg.Logger.Errorf("could not find subvolume with path '%s' in subvolumes list '%v'", path, subvolumes) - return 0, fmt.Errorf("can't find subvolume '%s'", path) + return strconv.Atoi(strings.TrimSpace(string(out))) } func (b *Btrfs) getPassiveSnapshots() ([]int, error) { passives := []int{} - active, err := b.getActiveSnapshot() - if err != nil { - b.cfg.Logger.Warnf("failed getting current active snapshot: %v", err) - return nil, err - } - snapshots, err := b.GetSnapshots() if err != nil { return nil, err } for _, snapshot := range snapshots { - if snapshot != active { + if snapshot != b.activeSnapshotID { passives = append(passives, snapshot) } } @@ -800,21 +711,38 @@ func (b *Btrfs) getPassiveSnapshots() ([]int, error) { return passives, nil } +// cleanOldSnapshots deletes old snapshots to prevent exceeding the configured maximum +func (b *Btrfs) cleanOldSnapshots() error { + var errs error + + b.cfg.Logger.Infof("Cleaning old passive snapshots") + ids, err := b.getPassiveSnapshots() + if err != nil { + b.cfg.Logger.Warnf("could not get current snapshots") + return err + } + + sort.Ints(ids) + for len(ids) > b.snapshotterCfg.MaxSnaps-1 { + err = b.DeleteSnapshot(ids[0]) + if err != nil { + b.cfg.Logger.Warnf("could not delete snapshot %d", ids[0]) + errs = multierror.Append(errs, err) + } + ids = ids[1:] + } + return errs +} + // setBootloader sets the bootloader variables to update new passives func (b *Btrfs) setBootloader() error { var passives, fallbacks []string b.cfg.Logger.Infof("Setting bootloader with current passive snapshots") - active, err := b.getActiveSnapshot() - if err != nil { - b.cfg.Logger.Warnf("failed getting current active snapshot: %v", err) - return err - } - ids, err := b.getPassiveSnapshots() if err != nil { - b.cfg.Logger.Warnf("failed getting current passive snapshots: %v", err) + b.cfg.Logger.Warnf("failed getting passive snapshots: %v", err) return err } @@ -833,7 +761,7 @@ func (b *Btrfs) setBootloader() error { envs := map[string]string{ constants.GrubFallback: fallbackList, constants.GrubPassiveSnapshots: snapsList, - constants.GrubActiveSnapshot: strconv.Itoa(active), + constants.GrubActiveSnapshot: strconv.Itoa(b.activeSnapshotID), "snapshotter": constants.BtrfsSnapshotterType, } @@ -846,15 +774,56 @@ func (b *Btrfs) setBootloader() error { return err } -func (b *Btrfs) configureSnapper(snapshot *types.Snapshot) error { - defaultTmpl, err := utils.FindFile(b.cfg.Fs, snapshot.WorkDir, configTemplatesPaths()...) +func (b *Btrfs) configureSnapshotSnapper(snapshot *types.Snapshot) error { + var extraPaths map[string]string + + rootconfig := filepath.Join(snapshot.WorkDir, snapperRootConfig) + snapshotDir := filepath.Join(snapshot.WorkDir, snapshotsPath) + + if ok, err := utils.Exists(b.cfg.Fs, rootconfig); !ok { + // ensure snapshots directory does not exists otherwise create config will fail + err := utils.RemoveAll(b.cfg.Fs, snapshotDir) + if err != nil { + b.cfg.Logger.Errorf("unable to delete snapshots folder: %v", err) + return err + } + + // actually create the 'root' config file + cmdOut, err := b.runSnapshotSnapper(snapshot.WorkDir, extraPaths, + "--config", "root", "create-config", + "--fstype", "btrfs", "/") + if err != nil { + b.cfg.Logger.Errorf("unable to create snapper root config: %v", err) + b.cfg.Logger.Debugf("snapper output: %s", string(cmdOut)) + return err + } + + // create config will create a '.snapshot' subvolume. delete it. + args := []string{"subvolume", "delete", "-c", snapshotDir} + cmdOut, err = b.cfg.Runner.Run("btrfs", args...) + if err != nil { + b.cfg.Logger.Errorf("failed deleting snapper .snapshot subvolume: %s", string(cmdOut)) + return err + } + } else if err != nil { + b.cfg.Logger.Errorf("unable to stat snapper root config: %s", rootconfig) + return err + } + + // Make sure snapshots mountpoint folder is part of the resulting snapshot image + err := utils.MkdirAll(b.cfg.Fs, snapshotDir, constants.DirPerm) if err != nil { - b.cfg.Logger.Errorf("failed to find default snapper configuration template") + b.cfg.Logger.Errorf("failed creating snapshots folder: %v", err) return err } + // set global snapper configuration sysconfigData := map[string]string{} - sysconfig := filepath.Join(snapshot.WorkDir, snapperSysconfig) + sysconfig := filepath.Join(snapshot.WorkDir, snapperDefaultconfig) + if ok, _ := utils.Exists(b.cfg.Fs, sysconfig); !ok { + sysconfig = filepath.Join(snapshot.WorkDir, snapperSysconfig) + } + if ok, _ := utils.Exists(b.cfg.Fs, sysconfig); ok { sysconfigData, err = utils.LoadEnvFile(b.cfg.Fs, sysconfig) if err != nil { @@ -871,24 +840,24 @@ func (b *Btrfs) configureSnapper(snapshot *types.Snapshot) error { return err } - snapCfg, err := utils.LoadEnvFile(b.cfg.Fs, defaultTmpl) + rootconfigData, err := utils.LoadEnvFile(b.cfg.Fs, rootconfig) if err != nil { b.cfg.Logger.Errorf("failed to load default snapper templage configuration") return err } - snapCfg["TIMELINE_CREATE"] = "no" - snapCfg["QGROUP"] = "1/0" - snapCfg["NUMBER_LIMIT"] = strconv.Itoa(b.snapshotterCfg.MaxSnaps) - snapCfg["NUMBER_LIMIT_IMPORTANT"] = strconv.Itoa(b.snapshotterCfg.MaxSnaps) + rootconfigData["TIMELINE_CREATE"] = "no" + rootconfigData["QGROUP"] = "1/0" + rootconfigData["NUMBER_LIMIT"] = strconv.Itoa(b.snapshotterCfg.MaxSnaps) + rootconfigData["NUMBER_LIMIT_IMPORTANT"] = strconv.Itoa(b.snapshotterCfg.MaxSnaps) - rootCfg := filepath.Join(snapshot.WorkDir, snapperRootConfig) - b.cfg.Logger.Debugf("Creating 'root' snapper configuration at '%s'", rootCfg) - err = utils.WriteEnvFile(b.cfg.Fs, snapCfg, rootCfg) + b.cfg.Logger.Debugf("updating 'root' snapper configuration at '%s'", rootconfig) + err = utils.WriteEnvFile(b.cfg.Fs, rootconfigData, rootconfig) if err != nil { b.cfg.Logger.Errorf("failed writing snapper root configuration file: %v", err) return err } + return nil } @@ -907,40 +876,15 @@ func (b *Btrfs) remountStatePartition(state *types.Partition) error { return err } - if b.activeSnapshotID > 0 { - // XXX dynamically find active snapshot here - err = b.mountSnapshotsSubvolumeInSnapshot(state.Path, state.MountPoint, b.activeSnapshotID) - b.snapperArgs = []string{"--no-dbus", "--root", filepath.Join(state.MountPoint, fmt.Sprintf(snapshotPathTmpl, b.activeSnapshotID))} - } - b.rootDir = state.MountPoint - return err -} - -func (b *Btrfs) mountSnapshotsSubvolumeInSnapshot(device, root string, snapshotID int) error { - var mountpoint, subvol string - - b.snapshotsMount = func() error { - b.cfg.Logger.Debugf("Mount snapshots subvolume in active snapshot %d", snapshotID) - mountpoint = filepath.Join(filepath.Join(root, fmt.Sprintf(snapshotPathTmpl, snapshotID)), snapshotsPath) - subvol = fmt.Sprintf("subvol=%s", filepath.Join(rootSubvol, snapshotsPath)) - return b.cfg.Mounter.Mount(device, mountpoint, "btrfs", []string{"rw", subvol}) - } - err := b.snapshotsMount() - if err != nil { - b.cfg.Logger.Errorf("failed mounting subvolume %s at %s", subvol, mountpoint) - return err - } - b.snapshotsUmount = func() error { return b.cfg.Mounter.Unmount(mountpoint) } - return nil + return b.updateBtrfsContext(state.Path) } func (b *Btrfs) setBtrfsForFirstTime(state *types.Partition) error { - topDir, _, _, _, err := findStateMount(b.cfg.Runner, state.Path) + topDir, _, _, _, err := b.findStateMount(state.Path) if err != nil { - b.cfg.Logger.Errorf("could not find expected mountpoints") + b.cfg.Logger.Errorf("could not find expected btrfs top level directory") return err - } - if topDir == "" { + } else if topDir == "" { b.cfg.Logger.Errorf("btrfs root is not mounted, can't initialize the snapshotter within an existing subvolume") return err } @@ -971,34 +915,39 @@ func (b *Btrfs) setBtrfsForFirstTime(state *types.Partition) error { return nil } -func (b *Btrfs) configureSnapperAndRootDir(state *types.Partition) error { - _, rootDir, _, stateDir, err := findStateMount(b.cfg.Runner, state.Path) - - if stateDir == "" || rootDir == "" { +func (b *Btrfs) configureMountPointAndRootDir(state *types.Partition) error { + err := b.updateBtrfsContext(state.Path) + if b.stateDir == "" || b.rootDir == "" { err = fmt.Errorf("could not find expected mountpoints") return err } if err != nil { - b.cfg.Logger.Errorf("failed setting snapper root and state partition mountpoint: %v", err) + b.cfg.Logger.Errorf("failed setting snapper state partition mountpoint: %v", err) return err } // state.MountPoint must be updated otherwise state.yaml will fail to update - state.MountPoint = stateDir - b.rootDir = rootDir + state.MountPoint = b.stateDir - if b.rootDir != "/" { - b.snapperArgs = []string{"--no-dbus", "--root", b.rootDir} + return err +} + +// simple convenience function which returns a given path only if it exists and is a directory +func validateDirectory(fs types.FS, path string) (validated string) { + info, _ := fs.Lstat(path) + if info != nil && info.IsDir() { + validated = path } - return nil + + return validated } // General purpose function to retrieve all btrfs mount points for a given state partition // incoming path can be either a disk device or the path of a mounted btrfs filesystem // goal of this function is to be able to resolve path to all relevant btrfs directories -func findStateMount(runner types.Runner, path string) (topDir string, rootDir string, snapshotDir string, stateDir string, err error) { - output, err := runner.Run("findmnt", "-lno", "SOURCE,TARGET,FSTYPE", path) +func (b *Btrfs) findStateMount(path string) (topDir string, rootDir string, snapshotsDir string, stateDir string, err error) { + output, err := b.cfg.Runner.Run("findmnt", "-lno", "SOURCE,TARGET,FSTYPE", path) if err != nil { return "", "", "", "", err } @@ -1026,13 +975,13 @@ func findStateMount(runner types.Runner, path string) (topDir string, rootDir st for _, lineFields := range lines { subStart := strings.Index(lineFields[0], "[/") - // Additional feature: recursive logic if array length is 1 and device matches target + // Additional recursive logic if array length is 1 and device matches target if len(lines) == 1 && path == lineFields[1] { // Handle subStart logic for recursive call if subStart != -1 { - return findStateMount(runner, lineFields[0][0:subStart]) + return b.findStateMount(lineFields[0][0:subStart]) } - return findStateMount(runner, lineFields[0]) + return b.findStateMount(lineFields[0]) } subEnd := strings.LastIndex(lineFields[0], "]") @@ -1046,9 +995,10 @@ func findStateMount(runner types.Runner, path string) (topDir string, rootDir st if subVolume == rootSubvol { stateDir = lineFields[1] } else if subVolume == snapshotsSubvol { - snapshotDir = lineFields[1] - } else if r.MatchString(subVolume) { - rootDirMatches = append(rootDirMatches, lineFields[1]) // accumulate rootDir matches + snapshotsDir = lineFields[1] + } else if r.MatchString(subVolume) && lineFields[1] == "/" { + // only define rootDir if mounted as '/' + rootDirMatches = append(rootDirMatches, lineFields[1]) } } } @@ -1060,13 +1010,89 @@ func findStateMount(runner types.Runner, path string) (topDir string, rootDir st // If stateDir isn't found but topDir exists, append the rootSubvol to topDir if stateDir == "" && topDir != "" { - stateDir = filepath.Join(topDir, rootSubvol) + stateDir = validateDirectory(b.cfg.Fs, filepath.Join(topDir, rootSubvol)) + } + + // If snapshotsDir isn't found but rootDir or stateDir exists, append the subvolume to stateDir + if snapshotsDir == "" && rootDir != "" { + snapshotsDir = validateDirectory(b.cfg.Fs, filepath.Join(rootDir, snapshotsPath)) + } + if snapshotsDir == "" && stateDir != "" { + snapshotsDir = validateDirectory(b.cfg.Fs, filepath.Join(stateDir, snapshotsPath)) + } + + return topDir, rootDir, snapshotsDir, stateDir, err +} + +func (b *Btrfs) updateBtrfsContext(path string) (err error) { + _, rootDir, snapshotsDir, stateDir, err := b.findStateMount(path) + if err == nil { + b.rootDir = rootDir + b.snapshotsDir = snapshotsDir + b.stateDir = stateDir + b.activeSnapshotID, err = b.getActiveSnapshot() + } + + return err +} + +func (b *Btrfs) deleteBootstrapSubvolume() (bool, error) { + bootstrapDir := filepath.Join(b.stateDir, snapperBootstrapPath) + + if ok, err := utils.Exists(b.cfg.Fs, bootstrapDir); ok { + args := []string{"subvolume", "delete", "-c", bootstrapDir} + cmdOut, err := b.cfg.Runner.Run("btrfs", args...) + if err != nil { + b.cfg.Logger.Errorf("failed deleting bootstrap subvolume: %s", string(cmdOut)) + return false, err + } + + return true, nil + } else if err != nil { + b.cfg.Logger.Errorf("unable to stat bootstrap subvolume: %s", bootstrapDir) + return false, err } - // If snapshotDir isn't found but stateDir exists, append the subvolume to stateDir - if snapshotDir == "" && stateDir != "" { - snapshotDir = filepath.Join(stateDir, snapshotsPath) + return false, nil +} + +// wrapper function to execute snapper in the current context +func (b *Btrfs) runCurrentSnapper(args ...string) (out []byte, err error) { + snapperArgs := []string{} + + if !(elemental.IsActiveMode(b.cfg) || elemental.IsPassiveMode(b.cfg)) { + // Check if snapshots subvolume is mounted + rootDir := filepath.Join(filepath.Dir(b.snapshotsDir), fmt.Sprintf(snapshotPathTmpl, b.activeSnapshotID)) + snapshotsSubvolume := filepath.Join(rootDir, snapshotsPath) + if notMnt, _ := b.cfg.Mounter.IsLikelyNotMountPoint(snapshotsSubvolume); notMnt { + err = b.cfg.Mounter.Mount(b.snapshotsDir, snapshotsSubvolume, "bind", []string{"bind"}) + if err != nil { + return nil, err + } + defer func() { + err := b.cfg.Mounter.Unmount(snapshotsSubvolume) + + if err != nil { + b.cfg.Logger.Errorf("unable to find unmount snapper snapshot directory: %v", err) + } + }() + } + + snapperArgs = []string{"--no-dbus", "--root", rootDir} + } + args = append(snapperArgs, args...) + return b.cfg.Runner.Run("snapper", args...) +} + +// wrapper function to execute snapper in the snapshot context +func (b *Btrfs) runSnapshotSnapper(rootDir string, extraPaths map[string]string, args ...string) (out []byte, err error) { + callback := func() error { + snapperArgs := []string{"--no-dbus"} + args = append(snapperArgs, args...) + out, err = b.cfg.Runner.Run("snapper", args...) + return err } - return topDir, rootDir, snapshotDir, stateDir, err + err = utils.ChrootedCallback(&b.cfg, rootDir, extraPaths, callback) + return out, err }