Browse code

Add option to auto-configure blkdev for devmapper

Instead of forcing users to manually configure a block device to use
with devmapper, this gives the user the option to let the devmapper
driver configure a device for them.

Adds several new options to the devmapper storage-opts:

- dm.directlvm_device="" - path to the block device to configure for
direct-lvm
- dm.thinp_percent=95 - sets the percentage of space to use for
storage from the passed in block device
- dm.thinp_metapercent=1 - sets the percentage of space to for metadata
storage from the passed in block device
- dm.thinp_autoextend_threshold=80 - sets the threshold for when `lvm`
should automatically extend the thin pool as a percentage of the total
storage space
- dm.thinp_autoextend_percent=20 - sets the percentage to increase the
thin pool by when an autoextend is triggered.

Defaults are taken from
[here](https://docs.docker.com/engine/userguide/storagedriver/device-mapper-driver/#/configure-direct-lvm-mode-for-production)

The only option that is required is `dm.directlvm_device` for docker to
set everything up.

Changes to these settings are not currently supported and will error
out.
Future work could support allowing changes to these values.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2017/02/17 05:33:03
Showing 5 changed files
... ...
@@ -5,7 +5,9 @@
5 5
 The device mapper graphdriver uses the device mapper thin provisioning
6 6
 module (dm-thinp) to implement CoW snapshots. The preferred model is
7 7
 to have a thin pool reserved outside of Docker and passed to the
8
-daemon via the `--storage-opt dm.thinpooldev` option.
8
+daemon via the `--storage-opt dm.thinpooldev` option. Alternatively,
9
+the device mapper graphdriver can setup a block device to handle this
10
+for you via the `--storage-opt dm.directlvm_device` option.
9 11
 
10 12
 As a fallback if no thin pool is provided, loopback files will be
11 13
 created.  Loopback is very slow, but can be used without any
12 14
new file mode 100644
... ...
@@ -0,0 +1,247 @@
0
+package devmapper
1
+
2
+import (
3
+	"bufio"
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"os"
9
+	"os/exec"
10
+	"path/filepath"
11
+	"reflect"
12
+	"strings"
13
+
14
+	"github.com/Sirupsen/logrus"
15
+	"github.com/pkg/errors"
16
+)
17
+
18
+type directLVMConfig struct {
19
+	Device              string
20
+	ThinpPercent        uint64
21
+	ThinpMetaPercent    uint64
22
+	AutoExtendPercent   uint64
23
+	AutoExtendThreshold uint64
24
+}
25
+
26
+var (
27
+	errThinpPercentMissing = errors.New("must set both `dm.thinp_percent` and `dm.thinp_metapercent` if either is specified")
28
+	errThinpPercentTooBig  = errors.New("combined `dm.thinp_percent` and `dm.thinp_metapercent` must not be greater than 100")
29
+	errMissingSetupDevice  = errors.New("must provide device path in `dm.setup_device` in order to configure direct-lvm")
30
+)
31
+
32
+func validateLVMConfig(cfg directLVMConfig) error {
33
+	if reflect.DeepEqual(cfg, directLVMConfig{}) {
34
+		return nil
35
+	}
36
+	if cfg.Device == "" {
37
+		return errMissingSetupDevice
38
+	}
39
+	if (cfg.ThinpPercent > 0 && cfg.ThinpMetaPercent == 0) || cfg.ThinpMetaPercent > 0 && cfg.ThinpPercent == 0 {
40
+		return errThinpPercentMissing
41
+	}
42
+
43
+	if cfg.ThinpPercent+cfg.ThinpMetaPercent > 100 {
44
+		return errThinpPercentTooBig
45
+	}
46
+	return nil
47
+}
48
+
49
+func checkDevAvailable(dev string) error {
50
+	lvmScan, err := exec.LookPath("lvmdiskscan")
51
+	if err != nil {
52
+		logrus.Debug("could not find lvmdiskscan")
53
+		return nil
54
+	}
55
+
56
+	out, err := exec.Command(lvmScan).CombinedOutput()
57
+	if err != nil {
58
+		logrus.WithError(err).Error(string(out))
59
+		return nil
60
+	}
61
+
62
+	if !bytes.Contains(out, []byte(dev)) {
63
+		return errors.Errorf("%s is not available for use with devicemapper", dev)
64
+	}
65
+	return nil
66
+}
67
+
68
+func checkDevInVG(dev string) error {
69
+	pvDisplay, err := exec.LookPath("pvdisplay")
70
+	if err != nil {
71
+		logrus.Debug("could not find pvdisplay")
72
+		return nil
73
+	}
74
+
75
+	out, err := exec.Command(pvDisplay, dev).CombinedOutput()
76
+	if err != nil {
77
+		logrus.WithError(err).Error(string(out))
78
+		return nil
79
+	}
80
+
81
+	scanner := bufio.NewScanner(bytes.NewReader(bytes.TrimSpace(out)))
82
+	for scanner.Scan() {
83
+		fields := strings.SplitAfter(strings.TrimSpace(scanner.Text()), "VG Name")
84
+		if len(fields) > 1 {
85
+			// got "VG Name" line"
86
+			vg := strings.TrimSpace(fields[1])
87
+			if len(vg) > 0 {
88
+				return errors.Errorf("%s is already part of a volume group %q: must remove this device from any volume group or provide a different device", dev, vg)
89
+			}
90
+			logrus.Error(fields)
91
+			break
92
+		}
93
+	}
94
+	return nil
95
+}
96
+
97
+func checkDevHasFS(dev string) error {
98
+	blkid, err := exec.LookPath("blkid")
99
+	if err != nil {
100
+		logrus.Debug("could not find blkid")
101
+		return nil
102
+	}
103
+
104
+	out, err := exec.Command(blkid, dev).CombinedOutput()
105
+	if err != nil {
106
+		logrus.WithError(err).Error(string(out))
107
+		return nil
108
+	}
109
+
110
+	fields := bytes.Fields(out)
111
+	for _, f := range fields {
112
+		kv := bytes.Split(f, []byte{'='})
113
+		if bytes.Equal(kv[0], []byte("TYPE")) {
114
+			v := bytes.Trim(kv[1], "\"")
115
+			if len(v) > 0 {
116
+				return errors.Errorf("%s has a filesystem already, use dm.directlvm_device_force=true if you want to wipe the device", dev)
117
+			}
118
+			return nil
119
+		}
120
+	}
121
+	return nil
122
+}
123
+
124
+func verifyBlockDevice(dev string, force bool) error {
125
+	if err := checkDevAvailable(dev); err != nil {
126
+		return err
127
+	}
128
+	if err := checkDevInVG(dev); err != nil {
129
+		return err
130
+	}
131
+
132
+	if force {
133
+		return nil
134
+	}
135
+
136
+	if err := checkDevHasFS(dev); err != nil {
137
+		return err
138
+	}
139
+	return nil
140
+}
141
+
142
+func readLVMConfig(root string) (directLVMConfig, error) {
143
+	var cfg directLVMConfig
144
+
145
+	p := filepath.Join(root, "setup-config.json")
146
+	b, err := ioutil.ReadFile(p)
147
+	if err != nil {
148
+		if os.IsNotExist(err) {
149
+			return cfg, nil
150
+		}
151
+		return cfg, errors.Wrap(err, "error reading existing setup config")
152
+	}
153
+
154
+	// check if this is just an empty file, no need to produce a json error later if so
155
+	if len(b) == 0 {
156
+		return cfg, nil
157
+	}
158
+
159
+	err = json.Unmarshal(b, &cfg)
160
+	return cfg, errors.Wrap(err, "error unmarshaling previous device setup config")
161
+}
162
+
163
+func writeLVMConfig(root string, cfg directLVMConfig) error {
164
+	p := filepath.Join(root, "setup-config.json")
165
+	b, err := json.Marshal(cfg)
166
+	if err != nil {
167
+		return errors.Wrap(err, "error marshalling direct lvm config")
168
+	}
169
+	err = ioutil.WriteFile(p, b, 0600)
170
+	return errors.Wrap(err, "error writing direct lvm config to file")
171
+}
172
+
173
+func setupDirectLVM(cfg directLVMConfig) error {
174
+	pvCreate, err := exec.LookPath("pvcreate")
175
+	if err != nil {
176
+		return errors.Wrap(err, "error lookuping up command `pvcreate` while setting up direct lvm")
177
+	}
178
+
179
+	vgCreate, err := exec.LookPath("vgcreate")
180
+	if err != nil {
181
+		return errors.Wrap(err, "error lookuping up command `vgcreate` while setting up direct lvm")
182
+	}
183
+
184
+	lvCreate, err := exec.LookPath("lvcreate")
185
+	if err != nil {
186
+		return errors.Wrap(err, "error lookuping up command `lvcreate` while setting up direct lvm")
187
+	}
188
+
189
+	lvConvert, err := exec.LookPath("lvconvert")
190
+	if err != nil {
191
+		return errors.Wrap(err, "error lookuping up command `lvconvert` while setting up direct lvm")
192
+	}
193
+
194
+	lvChange, err := exec.LookPath("lvchange")
195
+	if err != nil {
196
+		return errors.Wrap(err, "error lookuping up command `lvchange` while setting up direct lvm")
197
+	}
198
+
199
+	if cfg.AutoExtendPercent == 0 {
200
+		cfg.AutoExtendPercent = 20
201
+	}
202
+
203
+	if cfg.AutoExtendThreshold == 0 {
204
+		cfg.AutoExtendThreshold = 80
205
+	}
206
+
207
+	if cfg.ThinpPercent == 0 {
208
+		cfg.ThinpPercent = 95
209
+	}
210
+	if cfg.ThinpMetaPercent == 0 {
211
+		cfg.ThinpMetaPercent = 1
212
+	}
213
+
214
+	out, err := exec.Command(pvCreate, "-f", cfg.Device).CombinedOutput()
215
+	if err != nil {
216
+		return errors.Wrap(err, string(out))
217
+	}
218
+
219
+	out, err = exec.Command(vgCreate, "docker", cfg.Device).CombinedOutput()
220
+	if err != nil {
221
+		return errors.Wrap(err, string(out))
222
+	}
223
+
224
+	out, err = exec.Command(lvCreate, "--wipesignatures", "y", "-n", "thinpool", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpPercent)).CombinedOutput()
225
+	if err != nil {
226
+		return errors.Wrap(err, string(out))
227
+	}
228
+	out, err = exec.Command(lvCreate, "--wipesignatures", "y", "-n", "thinpoolmeta", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpMetaPercent)).CombinedOutput()
229
+	if err != nil {
230
+		return errors.Wrap(err, string(out))
231
+	}
232
+
233
+	out, err = exec.Command(lvConvert, "-y", "--zero", "n", "-c", "512K", "--thinpool", "docker/thinpool", "--poolmetadata", "docker/thinpoolmeta").CombinedOutput()
234
+	if err != nil {
235
+		return errors.Wrap(err, string(out))
236
+	}
237
+
238
+	profile := fmt.Sprintf("activation{\nthin_pool_autoextend_threshold=%d\nthin_pool_autoextend_percent=%d\n}", cfg.AutoExtendThreshold, cfg.AutoExtendPercent)
239
+	err = ioutil.WriteFile("/etc/lvm/profile/docker-thinpool.profile", []byte(profile), 0600)
240
+	if err != nil {
241
+		return errors.Wrap(err, "error writing docker thinp autoextend profile")
242
+	}
243
+
244
+	out, err = exec.Command(lvChange, "--metadataprofile", "docker-thinpool", "docker/thinpool").CombinedOutput()
245
+	return errors.Wrap(err, string(out))
246
+}
... ...
@@ -5,7 +5,6 @@ package devmapper
5 5
 import (
6 6
 	"bufio"
7 7
 	"encoding/json"
8
-	"errors"
9 8
 	"fmt"
10 9
 	"io"
11 10
 	"io/ioutil"
... ...
@@ -13,6 +12,7 @@ import (
13 13
 	"os/exec"
14 14
 	"path"
15 15
 	"path/filepath"
16
+	"reflect"
16 17
 	"strconv"
17 18
 	"strings"
18 19
 	"sync"
... ...
@@ -29,6 +29,7 @@ import (
29 29
 	"github.com/docker/docker/pkg/mount"
30 30
 	"github.com/docker/docker/pkg/parsers"
31 31
 	units "github.com/docker/go-units"
32
+	"github.com/pkg/errors"
32 33
 
33 34
 	"github.com/opencontainers/runc/libcontainer/label"
34 35
 )
... ...
@@ -50,6 +51,7 @@ var (
50 50
 	enableDeferredDeletion              = false
51 51
 	userBaseSize                        = false
52 52
 	defaultMinFreeSpacePercent   uint32 = 10
53
+	lvmSetupConfigForce          bool
53 54
 )
54 55
 
55 56
 const deviceSetMetaFile string = "deviceset-metadata"
... ...
@@ -123,6 +125,7 @@ type DeviceSet struct {
123 123
 	gidMaps               []idtools.IDMap
124 124
 	minFreeSpacePercent   uint32 //min free space percentage in thinpool
125 125
 	xfsNospaceRetries     string // max retries when xfs receives ENOSPC
126
+	lvmSetupConfig        directLVMConfig
126 127
 }
127 128
 
128 129
 // DiskUsage contains information about disk usage and is used when reporting Status of a device.
... ...
@@ -1730,8 +1733,36 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error {
1730 1730
 		return err
1731 1731
 	}
1732 1732
 
1733
-	// Set the device prefix from the device id and inode of the docker root dir
1733
+	prevSetupConfig, err := readLVMConfig(devices.root)
1734
+	if err != nil {
1735
+		return err
1736
+	}
1737
+
1738
+	if !reflect.DeepEqual(devices.lvmSetupConfig, directLVMConfig{}) {
1739
+		if devices.thinPoolDevice != "" {
1740
+			return errors.New("cannot setup direct-lvm when `dm.thinpooldev` is also specified")
1741
+		}
1742
+
1743
+		if !reflect.DeepEqual(prevSetupConfig, devices.lvmSetupConfig) {
1744
+			if !reflect.DeepEqual(prevSetupConfig, directLVMConfig{}) {
1745
+				return errors.New("changing direct-lvm config is not supported")
1746
+			}
1747
+			logrus.WithField("storage-driver", "devicemapper").WithField("direct-lvm-config", devices.lvmSetupConfig).Debugf("Setting up direct lvm mode")
1748
+			if err := verifyBlockDevice(devices.lvmSetupConfig.Device, lvmSetupConfigForce); err != nil {
1749
+				return err
1750
+			}
1751
+			if err := setupDirectLVM(devices.lvmSetupConfig); err != nil {
1752
+				return err
1753
+			}
1754
+			if err := writeLVMConfig(devices.root, devices.lvmSetupConfig); err != nil {
1755
+				return err
1756
+			}
1757
+		}
1758
+		devices.thinPoolDevice = "docker-thinpool"
1759
+		logrus.WithField("storage-driver", "devicemapper").Debugf("Setting dm.thinpooldev to %q", devices.thinPoolDevice)
1760
+	}
1734 1761
 
1762
+	// Set the device prefix from the device id and inode of the docker root dir
1735 1763
 	st, err := os.Stat(devices.root)
1736 1764
 	if err != nil {
1737 1765
 		return fmt.Errorf("devmapper: Error looking up dir %s: %s", devices.root, err)
... ...
@@ -2605,6 +2636,7 @@ func NewDeviceSet(root string, doInit bool, options []string, uidMaps, gidMaps [
2605 2605
 	}
2606 2606
 
2607 2607
 	foundBlkDiscard := false
2608
+	var lvmSetupConfig directLVMConfig
2608 2609
 	for _, option := range options {
2609 2610
 		key, val, err := parsers.ParseKeyValueOpt(option)
2610 2611
 		if err != nil {
... ...
@@ -2699,11 +2731,60 @@ func NewDeviceSet(root string, doInit bool, options []string, uidMaps, gidMaps [
2699 2699
 				return nil, err
2700 2700
 			}
2701 2701
 			devices.xfsNospaceRetries = val
2702
+		case "dm.directlvm_device":
2703
+			lvmSetupConfig.Device = val
2704
+		case "dm.directlvm_device_force":
2705
+			lvmSetupConfigForce, err = strconv.ParseBool(val)
2706
+			if err != nil {
2707
+				return nil, err
2708
+			}
2709
+		case "dm.thinp_percent":
2710
+			per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
2711
+			if err != nil {
2712
+				return nil, errors.Wrapf(err, "could not parse `dm.thinp_percent=%s`", val)
2713
+			}
2714
+			if per >= 100 {
2715
+				return nil, errors.New("dm.thinp_percent must be greater than 0 and less than 100")
2716
+			}
2717
+			lvmSetupConfig.ThinpPercent = per
2718
+		case "dm.thinp_metapercent":
2719
+			per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
2720
+			if err != nil {
2721
+				return nil, errors.Wrapf(err, "could not parse `dm.thinp_metapercent=%s`", val)
2722
+			}
2723
+			if per >= 100 {
2724
+				return nil, errors.New("dm.thinp_metapercent must be greater than 0 and less than 100")
2725
+			}
2726
+			lvmSetupConfig.ThinpMetaPercent = per
2727
+		case "dm.thinp_autoextend_percent":
2728
+			per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
2729
+			if err != nil {
2730
+				return nil, errors.Wrapf(err, "could not parse `dm.thinp_autoextend_percent=%s`", val)
2731
+			}
2732
+			if per > 100 {
2733
+				return nil, errors.New("dm.thinp_autoextend_percent must be greater than 0 and less than 100")
2734
+			}
2735
+			lvmSetupConfig.AutoExtendPercent = per
2736
+		case "dm.thinp_autoextend_threshold":
2737
+			per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
2738
+			if err != nil {
2739
+				return nil, errors.Wrapf(err, "could not parse `dm.thinp_autoextend_threshold=%s`", val)
2740
+			}
2741
+			if per > 100 {
2742
+				return nil, errors.New("dm.thinp_autoextend_threshold must be greater than 0 and less than 100")
2743
+			}
2744
+			lvmSetupConfig.AutoExtendThreshold = per
2702 2745
 		default:
2703 2746
 			return nil, fmt.Errorf("devmapper: Unknown option %s\n", key)
2704 2747
 		}
2705 2748
 	}
2706 2749
 
2750
+	if err := validateLVMConfig(lvmSetupConfig); err != nil {
2751
+		return nil, err
2752
+	}
2753
+
2754
+	devices.lvmSetupConfig = lvmSetupConfig
2755
+
2707 2756
 	// By default, don't do blk discard hack on raw devices, its rarely useful and is expensive
2708 2757
 	if !foundBlkDiscard && (devices.dataDevice != "" || devices.thinPoolDevice != "") {
2709 2758
 		devices.doBlkDiscard = false
... ...
@@ -322,6 +322,60 @@ not use loopback in production. Ensure your Engine daemon has a
322 322
 $ sudo dockerd --storage-opt dm.thinpooldev=/dev/mapper/thin-pool
323 323
 ```
324 324
 
325
+##### `dm.directlvm_device`
326
+
327
+As an alternative to providing a thin pool as above, Docker can setup a block
328
+device for you.
329
+
330
+###### Example:
331
+
332
+```bash
333
+$ sudo dockerd --storage-opt dm.directlvm_device=/dev/xvdf
334
+```
335
+
336
+##### `dm.thinp_percent`
337
+
338
+Sets the percentage of passed in block device to use for storage.
339
+
340
+###### Example:
341
+
342
+```bash
343
+$ sudo dockerd --storage-opt dm.thinp_percent=95
344
+```
345
+
346
+##### `dm.thinp_metapercent`
347
+
348
+Sets the percentage of the passed in block device to use for metadata storage.
349
+
350
+###### Example:
351
+
352
+```bash
353
+$ sudo dockerd --storage-opt dm.thinp_metapercent=1
354
+```
355
+
356
+##### `dm.thinp_autoextend_threshold`
357
+
358
+Sets the value of the percentage of space used before `lvm` attempts to
359
+autoextend the available space [100 = disabled]
360
+
361
+###### Example:
362
+
363
+```bash
364
+$ sudo dockerd --storage-opt dm.thinp_autoextend_threshold=80
365
+```
366
+
367
+##### `dm.thinp_autoextend_percent`
368
+
369
+Sets the value percentage value to increase the thin pool by when when `lvm`
370
+attempts to autoextend the available space [100 = disabled]
371
+
372
+###### Example:
373
+
374
+```bash
375
+$ sudo dockerd --storage-opt dm.thinp_autoextend_percent=20
376
+```
377
+
378
+
325 379
 ##### `dm.basesize`
326 380
 
327 381
 Specifies the size to use when creating the base device, which limits the
... ...
@@ -418,6 +418,54 @@ Example use:
418 418
    $ dockerd \
419 419
          --storage-opt dm.thinpooldev=/dev/mapper/thin-pool
420 420
 
421
+#### dm.directlvm_device
422
+
423
+As an alternative to manually creating a thin pool as above, Docker can
424
+automatically configure a block device for you.
425
+
426
+Example use:
427
+
428
+   $ dockerd \
429
+         --storage-opt dm.directlvm_device=/dev/xvdf
430
+
431
+##### dm.thinp_percent
432
+
433
+Sets the percentage of passed in block device to use for storage.
434
+
435
+###### Example:
436
+
437
+   $ sudo dockerd \
438
+        --storage-opt dm.thinp_percent=95
439
+
440
+##### `dm.thinp_metapercent`
441
+
442
+Sets the percentage of the passed in block device to use for metadata storage.
443
+
444
+###### Example:
445
+
446
+   $ sudo dockerd \
447
+         --storage-opt dm.thinp_metapercent=1
448
+
449
+##### dm.thinp_autoextend_threshold
450
+
451
+Sets the value of the percentage of space used before `lvm` attempts to
452
+autoextend the available space [100 = disabled]
453
+
454
+###### Example:
455
+
456
+   $ sudo dockerd \
457
+         --storage-opt dm.thinp_autoextend_threshold=80
458
+
459
+##### dm.thinp_autoextend_percent
460
+
461
+Sets the value percentage value to increase the thin pool by when when `lvm`
462
+attempts to autoextend the available space [100 = disabled]
463
+
464
+###### Example:
465
+
466
+   $ sudo dockerd \
467
+         --storage-opt dm.thinp_autoextend_percent=20
468
+
421 469
 #### dm.basesize
422 470
 
423 471
 Specifies the size to use when creating the base device, which limits