diff --git a/snapshot/btrfs/btrfs.go b/snapshot/btrfs/btrfs.go index 5705f3c61..fbed3dfbe 100644 --- a/snapshot/btrfs/btrfs.go +++ b/snapshot/btrfs/btrfs.go @@ -7,51 +7,69 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/Sirupsen/logrus" "github.com/containerd/btrfs" "github.com/containerd/containerd" "github.com/containerd/containerd/log" + "github.com/containerd/containerd/mountinfo" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/snapshot" "github.com/containerd/containerd/snapshot/storage" "github.com/pkg/errors" ) -type btrfsConfig struct { - Device string `toml:"device"` -} - func init() { plugin.Register("snapshot-btrfs", &plugin.Registration{ - Type: plugin.SnapshotPlugin, - Config: &btrfsConfig{}, + Type: plugin.SnapshotPlugin, Init: func(ic *plugin.InitContext) (interface{}, error) { root := filepath.Join(ic.Root, "snapshot", "btrfs") - conf := ic.Config.(*btrfsConfig) - if conf.Device == "" { - // TODO: check device for root - return nil, errors.Errorf("btrfs requires \"device\" configuration") - } - return NewSnapshotter(conf.Device, root) + return NewSnapshotter(root) }, }) } type snapshotter struct { - device string // maybe we can resolve it with path? + device string // device of the root root string // root provides paths for internal storage. ms *storage.MetaStore } +func getBtrfsDevice(root string, mounts []*mountinfo.Info) (string, error) { + device := "" + deviceMountpoint := "" + for _, info := range mounts { + if (info.Root == "/" || info.Root == "") && strings.HasPrefix(root, info.Mountpoint) { + if info.Fstype == "btrfs" && len(info.Mountpoint) > len(deviceMountpoint) { + device = info.Source + deviceMountpoint = info.Mountpoint + } + if root == info.Mountpoint && info.Fstype != "btrfs" { + return "", fmt.Errorf("%s needs to be btrfs, but seems %s", root, info.Fstype) + } + } + } + if device == "" { + // TODO: automatically mount loopback device here? + return "", fmt.Errorf("%s is not mounted as btrfs", root) + } + return device, nil +} + // NewSnapshotter returns a Snapshotter using btrfs. Uses the provided -// device and root directory for snapshots and stores the metadata in +// root directory for snapshots and stores the metadata in // a file in the provided root. -func NewSnapshotter(device, root string) (snapshot.Snapshotter, error) { - if err := os.MkdirAll(root, 0700); err != nil { +// root needs to be a mount point of btrfs. +func NewSnapshotter(root string) (snapshot.Snapshotter, error) { + mounts, err := mountinfo.GetMounts() + if err != nil { + return nil, err + } + device, err := getBtrfsDevice(root, mounts) + if err != nil { return nil, err } - var ( active = filepath.Join(root, "active") snapshots = filepath.Join(root, "snapshots") @@ -193,7 +211,7 @@ func (b *snapshotter) mounts(dir string) ([]containerd.Mount, error) { return []containerd.Mount{ { Type: "btrfs", - Source: b.device, // device? + Source: b.device, // NOTE(stevvooe): While it would be nice to use to uuids for // mounts, they don't work reliably if the uuids are missing. Options: options, diff --git a/snapshot/btrfs/btrfs_test.go b/snapshot/btrfs/btrfs_test.go index a2e5c6a5e..732833713 100644 --- a/snapshot/btrfs/btrfs_test.go +++ b/snapshot/btrfs/btrfs_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/containerd/containerd" + "github.com/containerd/containerd/mountinfo" "github.com/containerd/containerd/snapshot" "github.com/containerd/containerd/snapshot/testsuite" "github.com/containerd/containerd/testutil" @@ -24,7 +25,7 @@ const ( func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshot.Snapshotter, func(), error) { return func(ctx context.Context, root string) (snapshot.Snapshotter, func(), error) { device := setupBtrfsLoopbackDevice(t, root) - snapshotter, err := NewSnapshotter(device.deviceName, root) + snapshotter, err := NewSnapshotter(root) if err != nil { t.Fatal(err) } @@ -215,3 +216,64 @@ func (device *testDevice) remove(t *testing.T) { t.Error(err) } } + +func TestGetBtrfsDevice(t *testing.T) { + testCases := []struct { + expectedDevice string + expectedError string + root string + mounts []*mountinfo.Info + }{ + { + expectedDevice: "/dev/loop0", + root: "/var/lib/containerd/snapshot/btrfs", + mounts: []*mountinfo.Info{ + {Root: "/", Mountpoint: "/", Fstype: "ext4", Source: "/dev/sda1"}, + {Root: "/", Mountpoint: "/var/lib/containerd/snapshot/btrfs", Fstype: "btrfs", Source: "/dev/loop0"}, + }, + }, + { + expectedError: "/var/lib/containerd/snapshot/btrfs is not mounted as btrfs", + root: "/var/lib/containerd/snapshot/btrfs", + mounts: []*mountinfo.Info{ + {Root: "/", Mountpoint: "/", Fstype: "ext4", Source: "/dev/sda1"}, + }, + }, + { + expectedDevice: "/dev/sda1", + root: "/var/lib/containerd/snapshot/btrfs", + mounts: []*mountinfo.Info{ + {Root: "/", Mountpoint: "/", Fstype: "btrfs", Source: "/dev/sda1"}, + }, + }, + { + expectedDevice: "/dev/sda2", + root: "/var/lib/containerd/snapshot/btrfs", + mounts: []*mountinfo.Info{ + {Root: "/", Mountpoint: "/", Fstype: "btrfs", Source: "/dev/sda1"}, + {Root: "/", Mountpoint: "/var/lib/containerd/snapshot/btrfs", Fstype: "btrfs", Source: "/dev/sda2"}, + }, + }, + { + expectedDevice: "/dev/sda2", + root: "/var/lib/containerd/snapshot/btrfs", + mounts: []*mountinfo.Info{ + {Root: "/", Mountpoint: "/var/lib/containerd/snapshot/btrfs", Fstype: "btrfs", Source: "/dev/sda2"}, + {Root: "/", Mountpoint: "/var/lib/foooooooooooooooooooo/baaaaaaaaaaaaaaaaaaaar", Fstype: "btrfs", Source: "/dev/sda3"}, // mountpoint length longer than /var/lib/containerd/snapshot/btrfs + {Root: "/", Mountpoint: "/", Fstype: "btrfs", Source: "/dev/sda1"}, + }, + }, + } + for i, tc := range testCases { + device, err := getBtrfsDevice(tc.root, tc.mounts) + if err != nil && tc.expectedError == "" { + t.Fatalf("%d: expected nil, got %v", i, err) + } + if err != nil && !strings.Contains(err.Error(), tc.expectedError) { + t.Fatalf("%d: expected %s, got %v", i, tc.expectedError, err) + } + if err == nil && device != tc.expectedDevice { + t.Fatalf("%d: expected %s, got %s", i, tc.expectedDevice, device) + } + } +}