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 }