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