github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/snapshots/devmapper/pool_device_test.go (about)

     1  // +build linux
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package devmapper
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/containerd/containerd/mount"
    32  	"github.com/containerd/containerd/pkg/testutil"
    33  	"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
    34  	"github.com/containerd/containerd/snapshots/devmapper/losetup"
    35  	"github.com/docker/go-units"
    36  	"github.com/sirupsen/logrus"
    37  	"gotest.tools/v3/assert"
    38  )
    39  
    40  const (
    41  	thinDevice1 = "thin-1"
    42  	thinDevice2 = "thin-2"
    43  	snapDevice1 = "snap-1"
    44  	device1Size = 100000
    45  	device2Size = 200000
    46  	testsPrefix = "devmapper-snapshotter-tests-"
    47  )
    48  
    49  // TestPoolDevice runs integration tests for pool device.
    50  // The following scenario implemented:
    51  // - Create pool device with name 'test-pool-device'
    52  // - Create two thin volumes 'thin-1' and 'thin-2'
    53  // - Write ext4 file system on 'thin-1' and make sure it'errs moutable
    54  // - Write v1 test file on 'thin-1' volume
    55  // - Take 'thin-1' snapshot 'snap-1'
    56  // - Change v1 file to v2 on 'thin-1'
    57  // - Mount 'snap-1' and make sure test file is v1
    58  // - Unmount volumes and remove all devices
    59  func TestPoolDevice(t *testing.T) {
    60  	testutil.RequiresRoot(t)
    61  
    62  	logrus.SetLevel(logrus.DebugLevel)
    63  	ctx := context.Background()
    64  
    65  	tempDir, err := ioutil.TempDir("", "pool-device-test-")
    66  	assert.NilError(t, err, "couldn't get temp directory for testing")
    67  
    68  	_, loopDataDevice := createLoopbackDevice(t, tempDir)
    69  	_, loopMetaDevice := createLoopbackDevice(t, tempDir)
    70  
    71  	poolName := fmt.Sprintf("test-pool-device-%d", time.Now().Nanosecond())
    72  	err = dmsetup.CreatePool(poolName, loopDataDevice, loopMetaDevice, 64*1024/dmsetup.SectorSize)
    73  	assert.NilError(t, err, "failed to create pool %q", poolName)
    74  
    75  	defer func() {
    76  		// Detach loop devices and remove images
    77  		err := losetup.DetachLoopDevice(loopDataDevice, loopMetaDevice)
    78  		assert.NilError(t, err)
    79  
    80  		err = os.RemoveAll(tempDir)
    81  		assert.NilError(t, err, "couldn't cleanup temp directory")
    82  	}()
    83  
    84  	config := &Config{
    85  		PoolName:           poolName,
    86  		RootPath:           tempDir,
    87  		BaseImageSize:      "16mb",
    88  		BaseImageSizeBytes: 16 * 1024 * 1024,
    89  	}
    90  
    91  	pool, err := NewPoolDevice(ctx, config)
    92  	assert.NilError(t, err, "can't create device pool")
    93  	assert.Assert(t, pool != nil)
    94  
    95  	defer func() {
    96  		err := pool.RemovePool(ctx)
    97  		assert.NilError(t, err, "can't close device pool")
    98  	}()
    99  
   100  	// Create thin devices
   101  	t.Run("CreateThinDevice", func(t *testing.T) {
   102  		testCreateThinDevice(t, pool)
   103  	})
   104  
   105  	// Make ext4 filesystem on 'thin-1'
   106  	t.Run("MakeFileSystem", func(t *testing.T) {
   107  		testMakeFileSystem(t, pool)
   108  	})
   109  
   110  	// Mount 'thin-1' and write v1 test file on 'thin-1' device
   111  	err = mount.WithTempMount(ctx, getMounts(thinDevice1), func(thin1MountPath string) error {
   112  		// Write v1 test file on 'thin-1' device
   113  		thin1TestFilePath := filepath.Join(thin1MountPath, "TEST")
   114  		err := ioutil.WriteFile(thin1TestFilePath, []byte("test file (v1)"), 0700)
   115  		assert.NilError(t, err, "failed to write test file v1 on '%s' volume", thinDevice1)
   116  
   117  		return nil
   118  	})
   119  
   120  	// Take snapshot of 'thin-1'
   121  	t.Run("CreateSnapshotDevice", func(t *testing.T) {
   122  		testCreateSnapshot(t, pool)
   123  	})
   124  
   125  	// Update TEST file on 'thin-1' to v2
   126  	err = mount.WithTempMount(ctx, getMounts(thinDevice1), func(thin1MountPath string) error {
   127  		thin1TestFilePath := filepath.Join(thin1MountPath, "TEST")
   128  		err = ioutil.WriteFile(thin1TestFilePath, []byte("test file (v2)"), 0700)
   129  		assert.NilError(t, err, "failed to write test file v2 on 'thin-1' volume after taking snapshot")
   130  
   131  		return nil
   132  	})
   133  
   134  	assert.NilError(t, err)
   135  
   136  	// Mount 'snap-1' and make sure TEST file is v1
   137  	err = mount.WithTempMount(ctx, getMounts(snapDevice1), func(snap1MountPath string) error {
   138  		// Read test file from snapshot device and make sure it's v1
   139  		fileData, err := ioutil.ReadFile(filepath.Join(snap1MountPath, "TEST"))
   140  		assert.NilError(t, err, "couldn't read test file from '%s' device", snapDevice1)
   141  		assert.Equal(t, "test file (v1)", string(fileData), "test file content is invalid on snapshot")
   142  
   143  		return nil
   144  	})
   145  
   146  	assert.NilError(t, err)
   147  
   148  	t.Run("DeactivateDevice", func(t *testing.T) {
   149  		testDeactivateThinDevice(t, pool)
   150  	})
   151  
   152  	t.Run("RemoveDevice", func(t *testing.T) {
   153  		testRemoveThinDevice(t, pool)
   154  	})
   155  
   156  	t.Run("rollbackActivate", func(t *testing.T) {
   157  		testCreateThinDevice(t, pool)
   158  
   159  		ctx := context.Background()
   160  
   161  		snapDevice := "snap2"
   162  
   163  		err := pool.CreateSnapshotDevice(ctx, thinDevice1, snapDevice, device1Size)
   164  		assert.NilError(t, err)
   165  
   166  		info, err := pool.metadata.GetDevice(ctx, snapDevice)
   167  		assert.NilError(t, err)
   168  
   169  		// Simulate a case that the device cannot be activated.
   170  		err = pool.DeactivateDevice(ctx, info.Name, false, false)
   171  		assert.NilError(t, err)
   172  
   173  		err = pool.rollbackActivate(ctx, info, err)
   174  		assert.NilError(t, err)
   175  	})
   176  }
   177  
   178  func TestPoolDeviceMarkFaulty(t *testing.T) {
   179  	tempDir, store := createStore(t)
   180  	defer cleanupStore(t, tempDir, store)
   181  
   182  	err := store.AddDevice(testCtx, &DeviceInfo{Name: "1", State: Unknown})
   183  	assert.NilError(t, err)
   184  
   185  	// Note: do not use 'Activated' here because pool.ensureDeviceStates() will
   186  	// try to activate the real dm device, which will fail on a faked device.
   187  	err = store.AddDevice(testCtx, &DeviceInfo{Name: "2", State: Deactivated})
   188  	assert.NilError(t, err)
   189  
   190  	pool := &PoolDevice{metadata: store}
   191  	err = pool.ensureDeviceStates(testCtx)
   192  	assert.NilError(t, err)
   193  
   194  	called := 0
   195  	err = pool.metadata.WalkDevices(testCtx, func(info *DeviceInfo) error {
   196  		called++
   197  
   198  		switch called {
   199  		case 1:
   200  			assert.Equal(t, Faulty, info.State)
   201  			assert.Equal(t, "1", info.Name)
   202  		case 2:
   203  			assert.Equal(t, Deactivated, info.State)
   204  			assert.Equal(t, "2", info.Name)
   205  		default:
   206  			t.Error("unexpected walk call")
   207  		}
   208  
   209  		return nil
   210  	})
   211  	assert.NilError(t, err)
   212  	assert.Equal(t, 2, called)
   213  }
   214  
   215  func testCreateThinDevice(t *testing.T, pool *PoolDevice) {
   216  	ctx := context.Background()
   217  
   218  	err := pool.CreateThinDevice(ctx, thinDevice1, device1Size)
   219  	assert.NilError(t, err, "can't create first thin device")
   220  
   221  	err = pool.CreateThinDevice(ctx, thinDevice1, device1Size)
   222  	assert.Assert(t, err != nil, "device pool allows duplicated device names")
   223  
   224  	err = pool.CreateThinDevice(ctx, thinDevice2, device2Size)
   225  	assert.NilError(t, err, "can't create second thin device")
   226  
   227  	deviceInfo1, err := pool.metadata.GetDevice(ctx, thinDevice1)
   228  	assert.NilError(t, err)
   229  
   230  	deviceInfo2, err := pool.metadata.GetDevice(ctx, thinDevice2)
   231  	assert.NilError(t, err)
   232  
   233  	assert.Assert(t, deviceInfo1.DeviceID != deviceInfo2.DeviceID, "assigned device ids should be different")
   234  
   235  	usage, err := pool.GetUsage(thinDevice1)
   236  	assert.NilError(t, err)
   237  	assert.Equal(t, usage, int64(0))
   238  }
   239  
   240  func testMakeFileSystem(t *testing.T, pool *PoolDevice) {
   241  	devicePath := dmsetup.GetFullDevicePath(thinDevice1)
   242  	args := []string{
   243  		devicePath,
   244  		"-E",
   245  		"nodiscard,lazy_itable_init=0,lazy_journal_init=0",
   246  	}
   247  
   248  	output, err := exec.Command("mkfs.ext4", args...).CombinedOutput()
   249  	assert.NilError(t, err, "failed to make filesystem on '%s': %s", thinDevice1, string(output))
   250  
   251  	usage, err := pool.GetUsage(thinDevice1)
   252  	assert.NilError(t, err)
   253  	assert.Assert(t, usage > 0)
   254  }
   255  
   256  func testCreateSnapshot(t *testing.T, pool *PoolDevice) {
   257  	err := pool.CreateSnapshotDevice(context.Background(), thinDevice1, snapDevice1, device1Size)
   258  	assert.NilError(t, err, "failed to create snapshot from '%s' volume", thinDevice1)
   259  }
   260  
   261  func testDeactivateThinDevice(t *testing.T, pool *PoolDevice) {
   262  	deviceList := []string{
   263  		thinDevice2,
   264  		snapDevice1,
   265  	}
   266  
   267  	for _, deviceName := range deviceList {
   268  		assert.Assert(t, pool.IsActivated(deviceName))
   269  
   270  		err := pool.DeactivateDevice(context.Background(), deviceName, false, true)
   271  		assert.NilError(t, err, "failed to remove '%s'", deviceName)
   272  
   273  		assert.Assert(t, !pool.IsActivated(deviceName))
   274  	}
   275  }
   276  
   277  func testRemoveThinDevice(t *testing.T, pool *PoolDevice) {
   278  	err := pool.RemoveDevice(testCtx, thinDevice1)
   279  	assert.NilError(t, err, "should delete thin device from pool")
   280  
   281  	err = pool.RemoveDevice(testCtx, thinDevice2)
   282  	assert.NilError(t, err, "should delete thin device from pool")
   283  }
   284  
   285  func getMounts(thinDeviceName string) []mount.Mount {
   286  	return []mount.Mount{
   287  		{
   288  			Source: dmsetup.GetFullDevicePath(thinDeviceName),
   289  			Type:   "ext4",
   290  		},
   291  	}
   292  }
   293  
   294  func createLoopbackDevice(t *testing.T, dir string) (string, string) {
   295  	file, err := ioutil.TempFile(dir, testsPrefix)
   296  	assert.NilError(t, err)
   297  
   298  	size, err := units.RAMInBytes("128Mb")
   299  	assert.NilError(t, err)
   300  
   301  	err = file.Truncate(size)
   302  	assert.NilError(t, err)
   303  
   304  	err = file.Close()
   305  	assert.NilError(t, err)
   306  
   307  	imagePath := file.Name()
   308  
   309  	loopDevice, err := losetup.AttachLoopDevice(imagePath)
   310  	assert.NilError(t, err)
   311  
   312  	return imagePath, loopDevice
   313  }