From 87289a0c62141397877aeedfdd8aebc84f50cbaf Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Wed, 27 Mar 2019 14:50:12 -0700 Subject: [PATCH] devmapper: implement Usage Signed-off-by: Maksym Pavlenko --- snapshots/devmapper/pool_device.go | 29 +++++++++- snapshots/devmapper/pool_device_test.go | 8 +++ snapshots/devmapper/snapshotter.go | 56 ++++++++++++++++++- snapshots/devmapper/snapshotter_test.go | 73 +++++++++++++++++++++++-- 4 files changed, 157 insertions(+), 9 deletions(-) diff --git a/snapshots/devmapper/pool_device.go b/snapshots/devmapper/pool_device.go index b70593277..cd14b9412 100644 --- a/snapshots/devmapper/pool_device.go +++ b/snapshots/devmapper/pool_device.go @@ -21,11 +21,13 @@ package devmapper import ( "context" "path/filepath" + "strconv" + + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" "github.com/containerd/containerd/log" "github.com/containerd/containerd/snapshots/devmapper/dmsetup" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" ) // PoolDevice ties together data and metadata volumes, represents thin-pool and manages volumes, snapshots and device ids. @@ -298,6 +300,29 @@ func (p *PoolDevice) IsActivated(deviceName string) bool { return true } +// GetUsage reports total size in bytes consumed by a thin-device. +// It relies on the number of used blocks reported by 'dmsetup status'. +// The output looks like: +// device2: 0 204800 thin 17280 204799 +// Where 17280 is the number of used sectors +func (p *PoolDevice) GetUsage(deviceName string) (int64, error) { + status, err := dmsetup.Status(deviceName) + if err != nil { + return 0, errors.Wrapf(err, "can't get status for device %q", deviceName) + } + + if len(status.Params) == 0 { + return 0, errors.Errorf("failed to get the number of used blocks, unexpected output from dmsetup status") + } + + count, err := strconv.ParseInt(status.Params[0], 10, 64) + if err != nil { + return 0, errors.Wrapf(err, "failed to parse status params: %q", status.Params[0]) + } + + return count * dmsetup.SectorSize, nil +} + // RemoveDevice completely wipes out thin device from thin-pool and frees it's device ID func (p *PoolDevice) RemoveDevice(ctx context.Context, deviceName string) error { info, err := p.metadata.GetDevice(ctx, deviceName) diff --git a/snapshots/devmapper/pool_device_test.go b/snapshots/devmapper/pool_device_test.go index 20ff57942..4a54bda60 100644 --- a/snapshots/devmapper/pool_device_test.go +++ b/snapshots/devmapper/pool_device_test.go @@ -168,6 +168,10 @@ func testCreateThinDevice(t *testing.T, pool *PoolDevice) { assert.NilError(t, err) assert.Assert(t, deviceInfo1.DeviceID != deviceInfo2.DeviceID, "assigned device ids should be different") + + usage, err := pool.GetUsage(thinDevice1) + assert.NilError(t, err) + assert.Equal(t, usage, int64(0)) } func testMakeFileSystem(t *testing.T, pool *PoolDevice) { @@ -180,6 +184,10 @@ func testMakeFileSystem(t *testing.T, pool *PoolDevice) { output, err := exec.Command("mkfs.ext4", args...).CombinedOutput() assert.NilError(t, err, "failed to make filesystem on '%s': %s", thinDevice1, string(output)) + + usage, err := pool.GetUsage(thinDevice1) + assert.NilError(t, err) + assert.Assert(t, usage > 0) } func testCreateSnapshot(t *testing.T, pool *PoolDevice) { diff --git a/snapshots/devmapper/snapshotter.go b/snapshots/devmapper/snapshotter.go index b70c6fd13..5f8336ad7 100644 --- a/snapshots/devmapper/snapshotter.go +++ b/snapshots/devmapper/snapshotter.go @@ -156,11 +156,46 @@ func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpath return info, err } -// Usage not yet implemented +// Usage returns the resource usage of an active or committed snapshot excluding the usage of parent snapshots. func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { log.G(ctx).WithField("key", key).Debug("usage") - return snapshots.Usage{}, errors.New("usage not implemented") + var ( + id string + err error + info snapshots.Info + usage snapshots.Usage + ) + + err = s.withTransaction(ctx, false, func(ctx context.Context) error { + id, info, usage, err = storage.GetInfo(ctx, key) + if err != nil { + return err + } + + if info.Kind == snapshots.KindActive { + deviceName := s.getDeviceName(id) + usage.Size, err = s.pool.GetUsage(deviceName) + if err != nil { + return err + } + } + + if info.Parent != "" { + // GetInfo returns total number of bytes used by a snapshot (including parent). + // So subtract parent usage in order to get delta consumed by layer itself. + _, _, parentUsage, err := storage.GetInfo(ctx, info.Parent) + if err != nil { + return err + } + + usage.Size -= parentUsage.Size + } + + return err + }) + + return usage, err } // Mounts return the list of mounts for the active or view snapshot @@ -221,7 +256,22 @@ func (s *Snapshotter) Commit(ctx context.Context, name, key string, opts ...snap log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit") return s.withTransaction(ctx, true, func(ctx context.Context) error { - _, err := storage.CommitActive(ctx, key, name, snapshots.Usage{}, opts...) + id, _, _, err := storage.GetInfo(ctx, key) + if err != nil { + return err + } + + deviceName := s.getDeviceName(id) + size, err := s.pool.GetUsage(deviceName) + if err != nil { + return err + } + + usage := snapshots.Usage{ + Size: size, + } + + _, err = storage.CommitActive(ctx, key, name, usage, opts...) return err }) } diff --git a/snapshots/devmapper/snapshotter_test.go b/snapshots/devmapper/snapshotter_test.go index 0a533b9d7..1ecde0528 100644 --- a/snapshots/devmapper/snapshotter_test.go +++ b/snapshots/devmapper/snapshotter_test.go @@ -22,17 +22,23 @@ import ( "context" _ "crypto/sha256" "fmt" + "io/ioutil" + "os" "testing" "time" + "github.com/containerd/continuity/fs/fstest" + "github.com/hashicorp/go-multierror" + "github.com/sirupsen/logrus" + "gotest.tools/assert" + + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/pkg/testutil" "github.com/containerd/containerd/snapshots" "github.com/containerd/containerd/snapshots/devmapper/dmsetup" "github.com/containerd/containerd/snapshots/devmapper/losetup" "github.com/containerd/containerd/snapshots/testsuite" - "github.com/hashicorp/go-multierror" - "github.com/sirupsen/logrus" - "gotest.tools/assert" ) func TestSnapshotterSuite(t *testing.T) { @@ -40,7 +46,7 @@ func TestSnapshotterSuite(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) - testsuite.SnapshotterSuite(t, "devmapper", func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) { + snapshotterFn := func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) { // Create loopback devices for each test case _, loopDataDevice := createLoopbackDevice(t, root) _, loopMetaDevice := createLoopbackDevice(t, root) @@ -73,5 +79,64 @@ func TestSnapshotterSuite(t *testing.T) { snap.cleanupFn = append([]closeFunc{removePool}, snap.cleanupFn...) return snap, snap.Close, nil + } + + testsuite.SnapshotterSuite(t, "devmapper", snapshotterFn) + + ctx := context.Background() + ctx = namespaces.WithNamespace(ctx, "testsuite") + + t.Run("DevMapperUsage", func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "snapshot-suite-usage") + assert.NilError(t, err) + defer os.RemoveAll(tempDir) + + snapshotter, closer, err := snapshotterFn(ctx, tempDir) + assert.NilError(t, err) + defer closer() + + testUsage(t, snapshotter) }) } + +// testUsage tests devmapper's Usage implementation. This is an approximate test as it's hard to +// predict how many blocks will be consumed under different conditions and parameters. +func testUsage(t *testing.T, snapshotter snapshots.Snapshotter) { + ctx := context.Background() + + // Create empty base layer + _, err := snapshotter.Prepare(ctx, "prepare-1", "") + assert.NilError(t, err) + + emptyLayerUsage, err := snapshotter.Usage(ctx, "prepare-1") + assert.NilError(t, err) + + // Should be > 0 as just written file system also consumes blocks + assert.Assert(t, emptyLayerUsage.Size > 0) + + err = snapshotter.Commit(ctx, "layer-1", "prepare-1") + assert.NilError(t, err) + + // Create child layer with 1MB file + + var ( + sizeBytes int64 = 1048576 // 1MB + baseApplier = fstest.Apply(fstest.CreateRandomFile("/a", 12345679, sizeBytes, 0777)) + ) + + mounts, err := snapshotter.Prepare(ctx, "prepare-2", "layer-1") + assert.NilError(t, err) + + err = mount.WithTempMount(ctx, mounts, baseApplier.Apply) + assert.NilError(t, err) + + err = snapshotter.Commit(ctx, "layer-2", "prepare-2") + assert.NilError(t, err) + + layer2Usage, err := snapshotter.Usage(ctx, "layer-2") + assert.NilError(t, err) + + // Should be at least 1 MB + fs metadata + assert.Assert(t, layer2Usage.Size > sizeBytes) + assert.Assert(t, layer2Usage.Size < sizeBytes+256*dmsetup.SectorSize) +}