Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use latest snapper installation helper #2246

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/green/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ ARG VERSION
ENV REPO=${REPO}
ENV VERSION=${VERSION}

# Add bleeding edge repo for snapper
RUN zypper ar https://download.opensuse.org/repositories/home:/aschnell:/snapper/openSUSE_Tumbleweed/home:aschnell:snapper.repo

# Install kernel, systemd, dracut, grub2 and other required tools
RUN ARCH=$(uname -m); \
if [[ "${ARCH}" != "riscv64" ]]; then \
Expand Down
2 changes: 2 additions & 0 deletions pkg/action/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ func (u *UpgradeAction) upgradeInstallStateYaml() error {
var oldActiveID int
var deletedIDs []int

u.cfg.Logger.Infof("Upgrading install state")

if u.spec.Partitions.Recovery == nil || u.spec.Partitions.State == nil {
return fmt.Errorf("undefined state or recovery partition")
}
Expand Down
69 changes: 11 additions & 58 deletions pkg/snapshotter/btrfs-backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (b *btrfsBackend) Probe(device string, mountpoint string) (backendStat, err
// On active or passive we must ensure the actual mountpoint reported by the state
// partition is the actual root, ghw only reports a single mountpoint per device...
if elemental.IsPassiveMode(*b.cfg) || elemental.IsActiveMode(*b.cfg) {
rootDir, stateMount, currentID, err := b.findStateMount(device)
rootDir, stateMount, currentID, err := findStateMount(b.cfg.Runner, device)
if err != nil {
return stat, err
}
Expand All @@ -158,27 +158,17 @@ func (b *btrfsBackend) Probe(device string, mountpoint string) (backendStat, err

// InitBrfsPartition is the method required to create snapshots structure on just formated partition
func (b *btrfsBackend) InitBrfsPartition(rootDir string) error {
b.cfg.Logger.Debug("Enabling btrfs quota")
cmdOut, err := b.cfg.Runner.Run("btrfs", "quota", "enable", rootDir)
err := initBtrfsQuotaAndRootSubvolume(b.cfg.Runner, b.cfg.Logger, rootDir)
if err != nil {
b.cfg.Logger.Errorf("failed setting quota for btrfs partition at %s: %s", rootDir, string(cmdOut))
b.cfg.Logger.Errorf("failed setting quota and root subvolume")
return err
}

b.cfg.Logger.Debug("Creating essential subvolumes")
for _, subvolume := range []string{filepath.Join(rootDir, rootSubvol), filepath.Join(rootDir, rootSubvol, snapshotsPath)} {
b.cfg.Logger.Debugf("Creating subvolume: %s", subvolume)
cmdOut, err = b.cfg.Runner.Run("btrfs", "subvolume", "create", subvolume)
if err != nil {
b.cfg.Logger.Errorf("failed creating subvolume %s: %s", subvolume, string(cmdOut))
return err
}
}

b.cfg.Logger.Debug("Create btrfs quota group")
cmdOut, err = b.cfg.Runner.Run("btrfs", "qgroup", "create", "1/0", rootDir)
subvolume := filepath.Join(rootDir, rootSubvol, snapshotsPath)
b.cfg.Logger.Debugf("Creating subvolume: %s", subvolume)
cmdOut, err := b.cfg.Runner.Run("btrfs", "subvolume", "create", subvolume)
if err != nil {
b.cfg.Logger.Errorf("failed creating quota group for %s: %s", rootDir, string(cmdOut))
b.cfg.Logger.Errorf("failed creating subvolume %s: %s", subvolume, string(cmdOut))
return err
}

Expand All @@ -189,6 +179,7 @@ func (b *btrfsBackend) InitBrfsPartition(rootDir string) error {
// assumes it will be creating the first snapshot.
func (b btrfsBackend) CreateNewSnapshot(rootDir string, baseID int) (*types.Snapshot, error) {
var workingDir string
var desc string

newID, err := b.computeNewID(rootDir)
if err != nil {
Expand All @@ -213,6 +204,7 @@ func (b btrfsBackend) CreateNewSnapshot(rootDir string, baseID int) (*types.Snap
return nil, err
}
workingDir = path
desc = fmt.Sprintf("first root filesystem, snapshot %d", newID)
} else {
b.cfg.Logger.Debugf("Creating snapshot %d", newID)
cmdOut, err := b.cfg.Runner.Run(
Expand All @@ -231,9 +223,10 @@ func (b btrfsBackend) CreateNewSnapshot(rootDir string, baseID int) (*types.Snap
_ = b.DeleteSnapshot(rootDir, newID)
return nil, err
}
desc = fmt.Sprintf("Update based on snapshot %d", baseID)
}
snapperXML := filepath.Join(rootDir, fmt.Sprintf(snapshotInfoPath, newID))
err = b.writeSnapperSnapshotXML(snapperXML, newSnapperSnapshotXML(newID, "first root filesystem"))
err = b.writeSnapperSnapshotXML(snapperXML, newSnapperSnapshotXML(newID, desc))
if err != nil {
b.cfg.Logger.Errorf("failed creating snapper info XML")
return nil, err
Expand Down Expand Up @@ -521,43 +514,3 @@ func (b btrfsBackend) computeNewID(rootDir string) (int, error) {
}
return slices.Max(list.IDs) + 1, nil
}

// findStateMount returns, from the given device, the mount point of the top subvolume (@),
// the mount point of the current snapshot and current snapshot ID. Elemental hardware
// utilities only return a single mountpoint per partition without having a reliable criteria
// on which one returns ('@', '.snapshots', '.snapshots/<ID>/snapshot', ...)
func (b btrfsBackend) findStateMount(device string) (rootDir string, stateMount string, snapshotID int, err error) {
output, err := b.cfg.Runner.Run("findmnt", "-lno", "SOURCE,TARGET", device)
if err != nil {
return "", "", 0, err
}
r := regexp.MustCompile(snapshotPathRegex)

scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(string(output))))
for scanner.Scan() {
lineFields := strings.Fields(scanner.Text())
if len(lineFields) != 2 {
continue
}

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])
}
}
}

if stateMount == "" || rootDir == "" {
err = fmt.Errorf("could not find expected mountpoints, findmnt output: %s", string(output))
}

return rootDir, stateMount, snapshotID, err
}
3 changes: 1 addition & 2 deletions pkg/snapshotter/btrfs-backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ var _ = Describe("btrfsBackend", Label("snapshotter", " btrfs"), func() {
Expect(runner.MatchMilestones([][]string{
{"btrfs", "quota", "enable"},
{"btrfs", "subvolume", "create"},
{"btrfs", "subvolume", "create"},
{"btrfs", "qgroup", "create"},
{"btrfs", "subvolume", "create"},
})).To(Succeed())
})

Expand Down Expand Up @@ -161,7 +161,6 @@ var _ = Describe("btrfsBackend", Label("snapshotter", " btrfs"), func() {
Expect(runner.MatchMilestones([][]string{
{"btrfs", "quota", "enable"},
{"btrfs", "subvolume", "create"},
{"btrfs", "subvolume", "create"},
{"btrfs", "qgroup", "create"},
})).To(Succeed())
})
Expand Down
83 changes: 76 additions & 7 deletions pkg/snapshotter/btrfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ limitations under the License.
package snapshotter

import (
"bufio"
"fmt"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -81,7 +83,7 @@ type Btrfs struct {
bootloader types.Bootloader
backend subvolumeBackend
snapshotsUmount func() error
snapshotsMount func() error
snapshotsMount func(id int) error
}

// newBtrfsSnapshotter creates a new btrfs snapshotter vased on the given configuration and the given bootloader
Expand All @@ -107,7 +109,7 @@ func newBtrfsSnapshotter(cfg types.Config, snapCfg types.SnapshotterConfig, boot
cfg: cfg, snapshotterCfg: snapCfg,
btrfsCfg: *btrfsCfg, bootloader: bootloader,
snapshotsUmount: func() error { return nil },
snapshotsMount: func() error { return nil },
snapshotsMount: func(_ int) error { return nil },
backend: NewSubvolumeBackend(&cfg, *btrfsCfg, snapCfg.MaxSnaps),
}, nil
}
Expand Down Expand Up @@ -299,7 +301,7 @@ func (b *Btrfs) GetSnapshots() (snapshots []int, err error) {
// 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()
err = b.snapshotsMount(b.activeSnapshotID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -443,17 +445,84 @@ func (b *Btrfs) remountStatePartition(state *types.Partition) error {
func (b *Btrfs) mountSnapshotsSubvolumeInSnapshot(root, device 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)
b.snapshotsMount = func(id int) error {
b.cfg.Logger.Debugf("Mount snapshots subvolume in active snapshot %d", id)
mountpoint = filepath.Join(filepath.Join(root, fmt.Sprintf(snapshotPathTmpl, id)), snapshotsPath)
subvol = fmt.Sprintf("subvol=%s", filepath.Join(rootSubvol, snapshotsPath))
return b.cfg.Mounter.Mount(device, mountpoint, "btrfs", []string{"rw", subvol})
}
err := b.snapshotsMount()
err := b.snapshotsMount(snapshotID)
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
}

// findStateMount returns, from the given device, the mount point of the top subvolume (@),
// the mount point of the current snapshot and current snapshot ID. Elemental hardware
// utilities only return a single mountpoint per partition without having a reliable criteria
// on which one returns ('@', '.snapshots', '.snapshots/<ID>/snapshot', ...)
func findStateMount(runner types.Runner, device string) (rootDir string, stateMount string, snapshotID int, err error) {
output, err := runner.Run("findmnt", "-lno", "SOURCE,TARGET", device)
if err != nil {
return "", "", 0, err
}
r := regexp.MustCompile(snapshotPathRegex)

scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(string(output))))
for scanner.Scan() {
lineFields := strings.Fields(scanner.Text())
if len(lineFields) != 2 {
continue
}

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])
}
}
}

if stateMount == "" || rootDir == "" {
err = fmt.Errorf("could not find expected mountpoints, findmnt output: %s", string(output))
}

return rootDir, stateMount, snapshotID, err
}

// createRootSubvolume is the method required to create root subvolume '@' and enable quota
func initBtrfsQuotaAndRootSubvolume(runner types.Runner, log types.Logger, rootDir string) error {
log.Debug("Enabling btrfs quota")
cmdOut, err := runner.Run("btrfs", "quota", "enable", rootDir)
if err != nil {
log.Errorf("failed setting quota for btrfs partition at %s: %s", rootDir, string(cmdOut))
return err
}

subvolume := filepath.Join(rootDir, rootSubvol)
log.Debugf("Creating subvolume: %s", subvolume)
cmdOut, err = runner.Run("btrfs", "subvolume", "create", filepath.Join(rootDir, rootSubvol))
if err != nil {
log.Errorf("failed creating subvolume %s: %s", subvolume, string(cmdOut))
return err
}

log.Debug("Create btrfs quota group")
cmdOut, err = runner.Run("btrfs", "qgroup", "create", "1/0", rootDir)
if err != nil {
log.Errorf("failed creating quota group for %s: %s", rootDir, string(cmdOut))
return err
}

return nil
}
Loading
Loading