diff --git a/snapshots/devmapper/losetup/losetup.go b/snapshots/devmapper/losetup/losetup.go new file mode 100644 index 000000000..9001f196d --- /dev/null +++ b/snapshots/devmapper/losetup/losetup.go @@ -0,0 +1,83 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package losetup + +import ( + "os/exec" + "strings" + + "github.com/pkg/errors" +) + +// FindAssociatedLoopDevices returns a list of loop devices attached to a given image +func FindAssociatedLoopDevices(imagePath string) ([]string, error) { + output, err := losetup("--list", "--output", "NAME", "--associated", imagePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to get loop devices: '%s'", output) + } + + if output == "" { + return []string{}, nil + } + + items := strings.Split(output, "\n") + if len(items) <= 1 { + return []string{}, nil + } + + // Skip header with column names + return items[1:], nil +} + +// AttachLoopDevice finds first available loop device and associates it with an image. +func AttachLoopDevice(imagePath string) (string, error) { + return losetup("--find", "--show", imagePath) +} + +// DetachLoopDevice detaches loop devices +func DetachLoopDevice(loopDevice ...string) error { + args := append([]string{"--detach"}, loopDevice...) + _, err := losetup(args...) + return err +} + +// RemoveLoopDevicesAssociatedWithImage detaches all loop devices attached to a given sparse image +func RemoveLoopDevicesAssociatedWithImage(imagePath string) error { + loopDevices, err := FindAssociatedLoopDevices(imagePath) + if err != nil { + return err + } + + for _, loopDevice := range loopDevices { + if err = DetachLoopDevice(loopDevice); err != nil { + return err + } + } + + return nil +} + +// losetup is a wrapper around losetup command line tool +func losetup(args ...string) (string, error) { + data, err := exec.Command("losetup", args...).CombinedOutput() + output := string(data) + if err != nil { + return "", errors.Wrapf(err, "losetup %s\nerror: %s\n", strings.Join(args, " "), output) + } + + return strings.TrimSuffix(output, "\n"), err +} diff --git a/snapshots/devmapper/losetup/losetup_test.go b/snapshots/devmapper/losetup/losetup_test.go new file mode 100644 index 000000000..702a27c85 --- /dev/null +++ b/snapshots/devmapper/losetup/losetup_test.go @@ -0,0 +1,115 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package losetup + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/containerd/containerd/pkg/testutil" + "github.com/docker/go-units" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestLosetup(t *testing.T) { + testutil.RequiresRoot(t) + + var ( + imagePath = createSparseImage(t) + loopDevice1 string + loopDevice2 string + ) + + defer func() { + err := os.Remove(imagePath) + assert.NilError(t, err) + }() + + t.Run("AttachLoopDevice", func(t *testing.T) { + dev1, err := AttachLoopDevice(imagePath) + assert.NilError(t, err) + assert.Assert(t, dev1 != "") + + dev2, err := AttachLoopDevice(imagePath) + assert.NilError(t, err) + assert.Assert(t, dev2 != dev1, "should attach different loop device") + + loopDevice1 = dev1 + loopDevice2 = dev2 + }) + + t.Run("AttachEmptyLoopDevice", func(t *testing.T) { + _, err := AttachLoopDevice("") + assert.Assert(t, err != nil, "shouldn't attach empty path") + }) + + t.Run("FindAssociatedLoopDevices", func(t *testing.T) { + devices, err := FindAssociatedLoopDevices(imagePath) + assert.NilError(t, err) + assert.Assert(t, is.Len(devices, 2), "unexpected number of attached devices") + assert.Assert(t, is.Contains(devices, loopDevice1)) + assert.Assert(t, is.Contains(devices, loopDevice2)) + }) + + t.Run("FindAssociatedLoopDevicesForInvalidImage", func(t *testing.T) { + devices, err := FindAssociatedLoopDevices("") + assert.NilError(t, err) + assert.Assert(t, is.Len(devices, 0)) + }) + + t.Run("DetachLoopDevice", func(t *testing.T) { + err := DetachLoopDevice(loopDevice2) + assert.NilError(t, err, "failed to detach %q", loopDevice2) + }) + + t.Run("DetachEmptyDevice", func(t *testing.T) { + err := DetachLoopDevice("") + assert.Assert(t, err != nil, "shouldn't detach empty path") + }) + + t.Run("RemoveLoopDevicesAssociatedWithImage", func(t *testing.T) { + err := RemoveLoopDevicesAssociatedWithImage(imagePath) + assert.NilError(t, err) + + devices, err := FindAssociatedLoopDevices(imagePath) + assert.NilError(t, err) + assert.Assert(t, is.Len(devices, 0)) + }) + + t.Run("RemoveLoopDevicesAssociatedWithInvalidImage", func(t *testing.T) { + err := RemoveLoopDevicesAssociatedWithImage("") + assert.NilError(t, err) + }) +} + +func createSparseImage(t *testing.T) string { + file, err := ioutil.TempFile("", "losetup-tests-") + assert.NilError(t, err) + + size, err := units.RAMInBytes("16Mb") + assert.NilError(t, err) + + err = file.Truncate(size) + assert.NilError(t, err) + + err = file.Close() + assert.NilError(t, err) + + return file.Name() +}