Skip to content

Commit fe05e4d

Browse files
committed
devmapper: add losetup
Signed-off-by: Maksym Pavlenko <[email protected]>
1 parent 0cff074 commit fe05e4d

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package losetup
18+
19+
import (
20+
"os/exec"
21+
"strings"
22+
23+
"github.com/pkg/errors"
24+
)
25+
26+
// FindAssociatedLoopDevices returns a list of loop devices attached to a given image
27+
func FindAssociatedLoopDevices(imagePath string) ([]string, error) {
28+
output, err := losetup("--list", "--output", "NAME", "--associated", imagePath)
29+
if err != nil {
30+
return nil, errors.Wrapf(err, "failed to get loop devices: '%s'", output)
31+
}
32+
33+
if output == "" {
34+
return []string{}, nil
35+
}
36+
37+
items := strings.Split(output, "\n")
38+
if len(items) <= 1 {
39+
return []string{}, nil
40+
}
41+
42+
// Skip header with column names
43+
return items[1:], nil
44+
}
45+
46+
// AttachLoopDevice finds first available loop device and associates it with an image.
47+
func AttachLoopDevice(imagePath string) (string, error) {
48+
return losetup("--find", "--show", imagePath)
49+
}
50+
51+
// DetachLoopDevice detaches loop devices
52+
func DetachLoopDevice(loopDevice ...string) error {
53+
args := append([]string{"--detach"}, loopDevice...)
54+
_, err := losetup(args...)
55+
return err
56+
}
57+
58+
// RemoveLoopDevicesAssociatedWithImage detaches all loop devices attached to a given sparse image
59+
func RemoveLoopDevicesAssociatedWithImage(imagePath string) error {
60+
loopDevices, err := FindAssociatedLoopDevices(imagePath)
61+
if err != nil {
62+
return err
63+
}
64+
65+
for _, loopDevice := range loopDevices {
66+
if err = DetachLoopDevice(loopDevice); err != nil {
67+
return err
68+
}
69+
}
70+
71+
return nil
72+
}
73+
74+
// losetup is a wrapper around losetup command line tool
75+
func losetup(args ...string) (string, error) {
76+
data, err := exec.Command("losetup", args...).CombinedOutput()
77+
output := string(data)
78+
if err != nil {
79+
return "", errors.Wrapf(err, "losetup %s\nerror: %s\n", strings.Join(args, " "), output)
80+
}
81+
82+
return strings.TrimSuffix(output, "\n"), err
83+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package losetup
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"testing"
23+
24+
"github.com/containerd/containerd/pkg/testutil"
25+
"github.com/docker/go-units"
26+
"gotest.tools/assert"
27+
is "gotest.tools/assert/cmp"
28+
)
29+
30+
func TestLosetup(t *testing.T) {
31+
testutil.RequiresRoot(t)
32+
33+
var (
34+
imagePath = createSparseImage(t)
35+
loopDevice1 string
36+
loopDevice2 string
37+
)
38+
39+
defer func() {
40+
err := os.Remove(imagePath)
41+
assert.NilError(t, err)
42+
}()
43+
44+
t.Run("AttachLoopDevice", func(t *testing.T) {
45+
dev1, err := AttachLoopDevice(imagePath)
46+
assert.NilError(t, err)
47+
assert.Assert(t, dev1 != "")
48+
49+
dev2, err := AttachLoopDevice(imagePath)
50+
assert.NilError(t, err)
51+
assert.Assert(t, dev2 != dev1, "should attach different loop device")
52+
53+
loopDevice1 = dev1
54+
loopDevice2 = dev2
55+
})
56+
57+
t.Run("AttachEmptyLoopDevice", func(t *testing.T) {
58+
_, err := AttachLoopDevice("")
59+
assert.Assert(t, err != nil, "shouldn't attach empty path")
60+
})
61+
62+
t.Run("FindAssociatedLoopDevices", func(t *testing.T) {
63+
devices, err := FindAssociatedLoopDevices(imagePath)
64+
assert.NilError(t, err)
65+
assert.Assert(t, is.Len(devices, 2), "unexpected number of attached devices")
66+
assert.Assert(t, is.Contains(devices, loopDevice1))
67+
assert.Assert(t, is.Contains(devices, loopDevice2))
68+
})
69+
70+
t.Run("FindAssociatedLoopDevicesForInvalidImage", func(t *testing.T) {
71+
devices, err := FindAssociatedLoopDevices("")
72+
assert.NilError(t, err)
73+
assert.Assert(t, is.Len(devices, 0))
74+
})
75+
76+
t.Run("DetachLoopDevice", func(t *testing.T) {
77+
err := DetachLoopDevice(loopDevice2)
78+
assert.NilError(t, err, "failed to detach %q", loopDevice2)
79+
})
80+
81+
t.Run("DetachEmptyDevice", func(t *testing.T) {
82+
err := DetachLoopDevice("")
83+
assert.Assert(t, err != nil, "shouldn't detach empty path")
84+
})
85+
86+
t.Run("RemoveLoopDevicesAssociatedWithImage", func(t *testing.T) {
87+
err := RemoveLoopDevicesAssociatedWithImage(imagePath)
88+
assert.NilError(t, err)
89+
90+
devices, err := FindAssociatedLoopDevices(imagePath)
91+
assert.NilError(t, err)
92+
assert.Assert(t, is.Len(devices, 0))
93+
})
94+
95+
t.Run("RemoveLoopDevicesAssociatedWithInvalidImage", func(t *testing.T) {
96+
err := RemoveLoopDevicesAssociatedWithImage("")
97+
assert.NilError(t, err)
98+
})
99+
}
100+
101+
func createSparseImage(t *testing.T) string {
102+
file, err := ioutil.TempFile("", "losetup-tests-")
103+
assert.NilError(t, err)
104+
105+
size, err := units.RAMInBytes("16Mb")
106+
assert.NilError(t, err)
107+
108+
err = file.Truncate(size)
109+
assert.NilError(t, err)
110+
111+
err = file.Close()
112+
assert.NilError(t, err)
113+
114+
return file.Name()
115+
}

0 commit comments

Comments
 (0)