devmapper: implement Usage

Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
This commit is contained in:
Maksym Pavlenko 2019-03-27 14:50:12 -07:00
parent 010b4da36f
commit 87289a0c62
4 changed files with 157 additions and 9 deletions

View File

@ -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)

View File

@ -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) {

View File

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

View File

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