devmapper: implement Usage
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
This commit is contained in:
parent
010b4da36f
commit
87289a0c62
@ -21,11 +21,13 @@ package devmapper
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
"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.
|
// 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
|
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
|
// 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 {
|
func (p *PoolDevice) RemoveDevice(ctx context.Context, deviceName string) error {
|
||||||
info, err := p.metadata.GetDevice(ctx, deviceName)
|
info, err := p.metadata.GetDevice(ctx, deviceName)
|
||||||
|
@ -168,6 +168,10 @@ func testCreateThinDevice(t *testing.T, pool *PoolDevice) {
|
|||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
assert.Assert(t, deviceInfo1.DeviceID != deviceInfo2.DeviceID, "assigned device ids should be different")
|
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) {
|
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()
|
output, err := exec.Command("mkfs.ext4", args...).CombinedOutput()
|
||||||
assert.NilError(t, err, "failed to make filesystem on '%s': %s", thinDevice1, string(output))
|
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) {
|
func testCreateSnapshot(t *testing.T, pool *PoolDevice) {
|
||||||
|
@ -156,11 +156,46 @@ func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpath
|
|||||||
return info, err
|
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) {
|
func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
|
||||||
log.G(ctx).WithField("key", key).Debug("usage")
|
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
|
// 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")
|
log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit")
|
||||||
|
|
||||||
return s.withTransaction(ctx, true, func(ctx context.Context) error {
|
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
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,17 +22,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
_ "crypto/sha256"
|
_ "crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/pkg/testutil"
|
||||||
"github.com/containerd/containerd/snapshots"
|
"github.com/containerd/containerd/snapshots"
|
||||||
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
||||||
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
||||||
"github.com/containerd/containerd/snapshots/testsuite"
|
"github.com/containerd/containerd/snapshots/testsuite"
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gotest.tools/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSnapshotterSuite(t *testing.T) {
|
func TestSnapshotterSuite(t *testing.T) {
|
||||||
@ -40,7 +46,7 @@ func TestSnapshotterSuite(t *testing.T) {
|
|||||||
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
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
|
// Create loopback devices for each test case
|
||||||
_, loopDataDevice := createLoopbackDevice(t, root)
|
_, loopDataDevice := createLoopbackDevice(t, root)
|
||||||
_, loopMetaDevice := createLoopbackDevice(t, root)
|
_, loopMetaDevice := createLoopbackDevice(t, root)
|
||||||
@ -73,5 +79,64 @@ func TestSnapshotterSuite(t *testing.T) {
|
|||||||
snap.cleanupFn = append([]closeFunc{removePool}, snap.cleanupFn...)
|
snap.cleanupFn = append([]closeFunc{removePool}, snap.cleanupFn...)
|
||||||
|
|
||||||
return snap, snap.Close, nil
|
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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user