Skip to content

Commit 7687152

Browse files
committed
Add device mapper functionality
Signed-off-by: Amory Hoste <[email protected]>
1 parent 93ccbad commit 7687152

File tree

6 files changed

+774
-1
lines changed

6 files changed

+774
-1
lines changed

devmapper/deviceSnapshot.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package devmapper
2+
3+
import (
4+
"fmt"
5+
"github.com/pkg/errors"
6+
"io/ioutil"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
"sync"
11+
"syscall"
12+
)
13+
14+
// DeviceSnapshot represents a device mapper snapshot
15+
type DeviceSnapshot struct {
16+
sync.Mutex
17+
poolName string
18+
deviceName string
19+
deviceId string
20+
mountDir string
21+
mountedReadonly bool
22+
numMounted int
23+
numActivated int
24+
}
25+
26+
// GetDevicePath returns the path to the snapshot device.
27+
func (dsnp *DeviceSnapshot) GetDevicePath() string {
28+
return fmt.Sprintf("/dev/mapper/%s", dsnp.deviceName)
29+
}
30+
31+
// getPoolpath returns the path of the thin pool used by the snapshot.
32+
func (dsnp *DeviceSnapshot) getPoolPath() string {
33+
return fmt.Sprintf("/dev/mapper/%s", dsnp.poolName)
34+
}
35+
36+
// NewDeviceSnapshot initializes a new device mapper snapshot.
37+
func NewDeviceSnapshot(poolName, deviceName, deviceId string) *DeviceSnapshot {
38+
dsnp := new(DeviceSnapshot)
39+
dsnp.poolName = poolName
40+
dsnp.deviceName = deviceName
41+
dsnp.deviceId = deviceId
42+
dsnp.mountDir = ""
43+
dsnp.mountedReadonly = false
44+
dsnp.numMounted = 0
45+
dsnp.numActivated = 0
46+
return dsnp
47+
}
48+
49+
// Activate creates a snapshot.
50+
func (dsnp *DeviceSnapshot) Activate() error {
51+
dsnp.Lock()
52+
defer dsnp.Unlock()
53+
54+
if dsnp.numActivated == 0 {
55+
tableEntry := fmt.Sprintf("0 20971520 thin %s %s", dsnp.getPoolPath(), dsnp.deviceId)
56+
57+
cmd := exec.Command("sudo", "dmsetup", "create", dsnp.deviceName, "--table", fmt.Sprintf("%s", tableEntry))
58+
err := cmd.Run()
59+
if err != nil {
60+
return errors.Wrapf(err, "activating snapshot %s", dsnp.deviceName)
61+
}
62+
63+
}
64+
65+
dsnp.numActivated += 1
66+
67+
return nil
68+
}
69+
70+
// Deactivate removes a snapshot.
71+
func (dsnp *DeviceSnapshot) Deactivate() error {
72+
dsnp.Lock()
73+
defer dsnp.Unlock()
74+
75+
if dsnp.numActivated == 1 {
76+
cmd := exec.Command("sudo", "dmsetup", "remove", dsnp.deviceName)
77+
err := cmd.Run()
78+
if err != nil {
79+
return errors.Wrapf(err, "deactivating snapshot %s", dsnp.deviceName)
80+
}
81+
}
82+
83+
dsnp.numActivated -= 1
84+
return nil
85+
}
86+
87+
// Mount mounts a snapshot device and returns the path where it is mounted. For better performance and efficiency,
88+
// a snapshot is only mounted once and shared if it is already mounted.
89+
func (dsnp *DeviceSnapshot) Mount(readOnly bool) (string, error) {
90+
dsnp.Lock()
91+
defer dsnp.Unlock()
92+
93+
if dsnp.numActivated == 0 {
94+
return "", errors.New("failed to mount: snapshot not activated")
95+
}
96+
97+
if dsnp.numMounted != 0 && (!dsnp.mountedReadonly || dsnp.mountedReadonly && !readOnly) {
98+
return "", errors.New("failed to mount: can't mount snapshot for both reading and writing")
99+
}
100+
101+
if dsnp.numMounted == 0 {
102+
mountDir, err := ioutil.TempDir("", dsnp.deviceName)
103+
if err != nil {
104+
return "", err
105+
}
106+
mountDir = removeTrailingSlash(mountDir)
107+
108+
err = mountExt4(dsnp.GetDevicePath(), mountDir, readOnly)
109+
if err != nil {
110+
return "", errors.Wrapf(err, "mounting %s at %s", dsnp.GetDevicePath(), mountDir)
111+
}
112+
dsnp.mountDir = mountDir
113+
dsnp.mountedReadonly = readOnly
114+
}
115+
116+
dsnp.numMounted += 1
117+
118+
return dsnp.mountDir, nil
119+
}
120+
121+
// UnMounts a device snapshot. Due to mounted snapshot being shared, a snapshot is only actually unmounted if it is not
122+
// in use by anyone else.
123+
func (dsnp *DeviceSnapshot) UnMount() error {
124+
dsnp.Lock()
125+
defer dsnp.Unlock()
126+
127+
if dsnp.numMounted == 1 {
128+
err := unMountExt4(dsnp.mountDir)
129+
if err != nil {
130+
return errors.Wrapf(err, "unmounting %s", dsnp.mountDir)
131+
}
132+
133+
err = os.RemoveAll(dsnp.mountDir)
134+
if err != nil {
135+
return errors.Wrapf(err, "removing %s", dsnp.mountDir)
136+
}
137+
dsnp.mountDir = ""
138+
}
139+
140+
dsnp.numMounted -= 1
141+
return nil
142+
}
143+
144+
// mountExt4 mounts a snapshot device available at devicePath at the specified mountPath.
145+
func mountExt4(devicePath, mountPath string, readOnly bool) error {
146+
// Specify flags for faster mounting and performance:
147+
// * Do not update access times for (all types of) files on this filesystem.
148+
// * Do not allow access to devices (special files) on this filesystem.
149+
// * Do not allow programs to be executed from this filesystem.
150+
// * Do not honor set-user-ID and set-group-ID bits or file capabilities when executing programs from this filesystem.
151+
// * Suppress the display of certain (printk()) warning messages in the kernel log.
152+
var flags uintptr = syscall.MS_NOATIME | syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_SILENT
153+
options := make([]string, 0)
154+
155+
if readOnly {
156+
// Mount filesystem read-only.
157+
flags |= syscall.MS_RDONLY
158+
options = append(options, "noload")
159+
}
160+
161+
return syscall.Mount(devicePath, mountPath, "ext4", flags, strings.Join(options, ","))
162+
}
163+
164+
// unMountExt4 unmounts a snapshot device mounted at mountPath.
165+
func unMountExt4(mountPath string) error {
166+
return syscall.Unmount(mountPath, syscall.MNT_DETACH)
167+
}
168+
169+
// removeTrailingSlash returns a path with the trailing slash removed.
170+
func removeTrailingSlash(path string) string {
171+
if strings.HasSuffix(path, "/") {
172+
return path[:len(path)-1]
173+
} else {
174+
return path
175+
}
176+
}

0 commit comments

Comments
 (0)