-
Notifications
You must be signed in to change notification settings - Fork 108
/
mounter.go
368 lines (298 loc) · 10.1 KB
/
mounter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/*
Copyright 2020 DigitalOcean
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 driver
import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
type findmntResponse struct {
FileSystems []fileSystem `json:"filesystems"`
}
type fileSystem struct {
Target string `json:"target"`
Propagation string `json:"propagation"`
FsType string `json:"fstype"`
Options string `json:"options"`
}
type volumeStatistics struct {
availableBytes, totalBytes, usedBytes int64
availableInodes, totalInodes, usedInodes int64
}
const (
// blkidExitStatusNoIdentifiers defines the exit code returned from blkid indicating that no devices have been found. See http://www.polarhome.com/service/man/?qf=blkid&tf=2&of=Alpinelinux for details.
blkidExitStatusNoIdentifiers = 2
)
// Mounter is responsible for formatting and mounting volumes
// TODO(timoreimann): find a more suitable name since the interface encompasses
// more than just mounting functionality by now.
type Mounter interface {
// Format formats the source with the given filesystem type
Format(source, fsType string) error
// Mount mounts source to target with the given fstype and options.
Mount(source, target, fsType string, options ...string) error
// Unmount unmounts the given target
Unmount(target string) error
// IsFormatted checks whether the source device is formatted or not. It
// returns true if the source device is already formatted.
IsFormatted(source string) (bool, error)
// IsMounted checks whether the target path is a correct mount (i.e:
// propagated). It returns true if it's mounted. An error is returned in
// case of system errors or if it's mounted incorrectly.
IsMounted(target string) (bool, error)
// GetStatistics returns capacity-related volume statistics for the given
// volume path.
GetStatistics(volumePath string) (volumeStatistics, error)
// IsBlockDevice checks whether the device at the path is a block device
IsBlockDevice(volumePath string) (bool, error)
}
// TODO(arslan): this is Linux only for now. Refactor this into a package with
// architecture specific code in the future, such as mounter_darwin.go,
// mounter_linux.go, etc..
type mounter struct {
log *logrus.Entry
}
// newMounter returns a new mounter instance
func newMounter(log *logrus.Entry) *mounter {
return &mounter{
log: log,
}
}
func (m *mounter) Format(source, fsType string) error {
mkfsCmd := fmt.Sprintf("mkfs.%s", fsType)
_, err := exec.LookPath(mkfsCmd)
if err != nil {
if err == exec.ErrNotFound {
return fmt.Errorf("%q executable not found in $PATH", mkfsCmd)
}
return err
}
mkfsArgs := []string{}
if fsType == "" {
return errors.New("fs type is not specified for formatting the volume")
}
if source == "" {
return errors.New("source is not specified for formatting the volume")
}
mkfsArgs = append(mkfsArgs, source)
if fsType == "ext4" || fsType == "ext3" {
mkfsArgs = []string{"-F", source}
}
m.log.WithFields(logrus.Fields{
"cmd": mkfsCmd,
"args": mkfsArgs,
}).Info("executing format command")
out, err := exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput()
if err != nil {
return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q",
err, mkfsCmd, strings.Join(mkfsArgs, " "), string(out))
}
return nil
}
func (m *mounter) Mount(source, target, fsType string, opts ...string) error {
mountCmd := "mount"
mountArgs := []string{}
if source == "" {
return errors.New("source is not specified for mounting the volume")
}
if target == "" {
return errors.New("target is not specified for mounting the volume")
}
// This is a raw block device mount. Create the mount point as a file
// since bind mount device node requires it to be a file
if fsType == "" {
// create directory for target, os.Mkdirall is noop if directory exists
err := os.MkdirAll(filepath.Dir(target), 0750)
if err != nil {
return fmt.Errorf("failed to create target directory for raw block bind mount: %v", err)
}
file, err := os.OpenFile(target, os.O_CREATE, 0660)
if err != nil {
return fmt.Errorf("failed to create target file for raw block bind mount: %v", err)
}
file.Close()
} else {
mountArgs = append(mountArgs, "-t", fsType)
// create target, os.Mkdirall is noop if directory exists
err := os.MkdirAll(target, 0750)
if err != nil {
return err
}
}
if len(opts) > 0 {
mountArgs = append(mountArgs, "-o", strings.Join(opts, ","))
}
mountArgs = append(mountArgs, source)
mountArgs = append(mountArgs, target)
m.log.WithFields(logrus.Fields{
"cmd": mountCmd,
"args": mountArgs,
}).Info("executing mount command")
out, err := exec.Command(mountCmd, mountArgs...).CombinedOutput()
if err != nil {
return fmt.Errorf("mounting failed: %v cmd: '%s %s' output: %q",
err, mountCmd, strings.Join(mountArgs, " "), string(out))
}
return nil
}
func (m *mounter) Unmount(target string) error {
umountCmd := "umount"
if target == "" {
return errors.New("target is not specified for unmounting the volume")
}
umountArgs := []string{target}
m.log.WithFields(logrus.Fields{
"cmd": umountCmd,
"args": umountArgs,
}).Info("executing umount command")
out, err := exec.Command(umountCmd, umountArgs...).CombinedOutput()
if err != nil {
return fmt.Errorf("unmounting failed: %v cmd: '%s %s' output: %q",
err, umountCmd, target, string(out))
}
return nil
}
func (m *mounter) IsFormatted(source string) (bool, error) {
if source == "" {
return false, errors.New("source is not specified")
}
blkidCmd := "blkid"
_, err := exec.LookPath(blkidCmd)
if err != nil {
if err == exec.ErrNotFound {
return false, fmt.Errorf("%q executable not found in $PATH", blkidCmd)
}
return false, err
}
blkidArgs := []string{source}
m.log.WithFields(logrus.Fields{
"cmd": blkidCmd,
"args": blkidArgs,
}).Info("checking if source is formatted")
exitCode := 0
cmd := exec.Command(blkidCmd, blkidArgs...)
err = cmd.Run()
if err != nil {
exitError, ok := err.(*exec.ExitError)
if !ok {
return false, fmt.Errorf("checking formatting failed: %v cmd: %q, args: %q", err, blkidCmd, blkidArgs)
}
ws := exitError.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
if exitCode == blkidExitStatusNoIdentifiers {
return false, nil
}
return false, fmt.Errorf("checking formatting failed: %v cmd: %q, args: %q", err, blkidCmd, blkidArgs)
}
return true, nil
}
func (m *mounter) IsMounted(target string) (bool, error) {
if target == "" {
return false, errors.New("target is not specified for checking the mount")
}
findmntCmd := "findmnt"
_, err := exec.LookPath(findmntCmd)
if err != nil {
if err == exec.ErrNotFound {
return false, fmt.Errorf("%q executable not found in $PATH", findmntCmd)
}
return false, err
}
findmntArgs := []string{"-o", "TARGET,PROPAGATION,FSTYPE,OPTIONS", "-M", target, "-J"}
m.log.WithFields(logrus.Fields{
"cmd": findmntCmd,
"args": findmntArgs,
}).Info("checking if target is mounted")
out, err := exec.Command(findmntCmd, findmntArgs...).CombinedOutput()
if err != nil {
// findmnt exits with non zero exit status if it couldn't find anything
if strings.TrimSpace(string(out)) == "" {
return false, nil
}
return false, fmt.Errorf("checking mounted failed: %v cmd: %q output: %q",
err, findmntCmd, string(out))
}
// no response means there is no mount
if string(out) == "" {
return false, nil
}
var resp *findmntResponse
err = json.Unmarshal(out, &resp)
if err != nil {
return false, fmt.Errorf("couldn't unmarshal data: %q: %s", string(out), err)
}
targetFound := false
for _, fs := range resp.FileSystems {
// check if the mount is propagated correctly. It should be set to shared.
if fs.Propagation != "shared" {
return true, fmt.Errorf("mount propagation for target %q is not enabled", target)
}
// the mountpoint should match as well
if fs.Target == target {
targetFound = true
}
}
return targetFound, nil
}
func (m *mounter) GetStatistics(volumePath string) (volumeStatistics, error) {
isBlock, err := m.IsBlockDevice(volumePath)
if err != nil {
return volumeStatistics{}, fmt.Errorf("failed to determine if volume %s is block device: %v", volumePath, err)
}
if isBlock {
// See http://man7.org/linux/man-pages/man8/blockdev.8.html for details
output, err := exec.Command("blockdev", "getsize64", volumePath).CombinedOutput()
if err != nil {
return volumeStatistics{}, fmt.Errorf("error when getting size of block volume at path %s: output: %s, err: %v", volumePath, string(output), err)
}
strOut := strings.TrimSpace(string(output))
gotSizeBytes, err := strconv.ParseInt(strOut, 10, 64)
if err != nil {
return volumeStatistics{}, fmt.Errorf("failed to parse size %s into int", strOut)
}
return volumeStatistics{
totalBytes: gotSizeBytes,
}, nil
}
var statfs unix.Statfs_t
// See http://man7.org/linux/man-pages/man2/statfs.2.html for details.
err = unix.Statfs(volumePath, &statfs)
if err != nil {
return volumeStatistics{}, err
}
volStats := volumeStatistics{
availableBytes: int64(statfs.Bavail) * int64(statfs.Bsize),
totalBytes: int64(statfs.Blocks) * int64(statfs.Bsize),
usedBytes: (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize),
availableInodes: int64(statfs.Ffree),
totalInodes: int64(statfs.Files),
usedInodes: int64(statfs.Files) - int64(statfs.Ffree),
}
return volStats, nil
}
func (m *mounter) IsBlockDevice(devicePath string) (bool, error) {
var stat unix.Stat_t
err := unix.Stat(devicePath, &stat)
if err != nil {
return false, err
}
return (stat.Mode & unix.S_IFMT) == unix.S_IFBLK, nil
}