-
Notifications
You must be signed in to change notification settings - Fork 906
/
volume.go
600 lines (484 loc) · 19.7 KB
/
volume.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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
package drivers
import (
"context"
"errors"
"fmt"
"os"
"github.com/canonical/lxd/lxd/locking"
"github.com/canonical/lxd/lxd/operations"
"github.com/canonical/lxd/lxd/refcount"
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/revert"
"github.com/canonical/lxd/shared/units"
)
// tmpVolSuffix Suffix to use for any temporary volumes created by LXD.
const tmpVolSuffix = ".lxdtmp"
// isoVolSuffix suffix used for iso content type volumes.
const isoVolSuffix = ".iso"
// DefaultBlockSize is the default size of block volumes.
const DefaultBlockSize = "10GiB"
// DefaultFilesystem filesytem to use for block devices by default.
const DefaultFilesystem = "ext4"
// defaultFilesystemMountOpts mount options to use for filesystem block devices by default.
const defaultFilesystemMountOptions = "discard"
// volIDQuotaSkip is used to indicate to drivers that quotas should not be setup, used during backup import.
const volIDQuotaSkip = int64(-1)
// VolumeType represents a storage volume type.
type VolumeType string
// IsInstance indicates if the VolumeType represents an instance type.
func (t VolumeType) IsInstance() bool {
if t == VolumeTypeContainer || t == VolumeTypeVM {
return true
}
return false
}
// VolumeTypeBucket represents a bucket storage volume.
const VolumeTypeBucket = VolumeType("buckets")
// VolumeTypeImage represents an image storage volume.
const VolumeTypeImage = VolumeType("images")
// VolumeTypeCustom represents a custom storage volume.
const VolumeTypeCustom = VolumeType("custom")
// VolumeTypeContainer represents a container storage volume.
const VolumeTypeContainer = VolumeType("containers")
// VolumeTypeVM represents a virtual-machine storage volume.
const VolumeTypeVM = VolumeType("virtual-machines")
// ContentType indicates the format of the volume.
type ContentType string
// ContentTypeFS indicates the volume will be populated with a mountabble filesystem.
const ContentTypeFS = ContentType("filesystem")
// ContentTypeBlock indicates the volume will be a block device and its contents and we do not
// know which filesystem(s) (if any) are in use.
const ContentTypeBlock = ContentType("block")
// ContentTypeISO indicates the volume will be an ISO which is read-only, and uses the ISO 9660 filesystem.
const ContentTypeISO = ContentType("iso")
// VolumePostHook function returned from a storage action that should be run later to complete the action.
type VolumePostHook func(vol Volume) error
// BaseDirectories maps volume types to the expected directories.
var BaseDirectories = map[VolumeType][]string{
VolumeTypeBucket: {"buckets"},
VolumeTypeContainer: {"containers", "containers-snapshots"},
VolumeTypeCustom: {"custom", "custom-snapshots"},
VolumeTypeImage: {"images"},
VolumeTypeVM: {"virtual-machines", "virtual-machines-snapshots"},
}
// Volume represents a storage volume, and provides functions to mount and unmount it.
type Volume struct {
name string
pool string
poolConfig map[string]string
volType VolumeType
contentType ContentType
config map[string]string
driver Driver
mountCustomPath string // Mount the filesystem volume at a custom location.
mountFilesystemProbe bool // Probe filesystem type when mounting volume (when needed).
hasSource bool // Whether the volume is created from a source volume.
parentUUID string // Set to the parent volume's volatile.uuid (if snapshot).
}
// VolumeCopy represents a volume and its snapshots for copy and refresh operations.
type VolumeCopy struct {
Volume
Snapshots []Volume
}
// NewVolume instantiates a new Volume struct.
func NewVolume(driver Driver, poolName string, volType VolumeType, contentType ContentType, volName string, volConfig map[string]string, poolConfig map[string]string) Volume {
return Volume{
name: volName,
pool: poolName,
poolConfig: poolConfig,
volType: volType,
contentType: contentType,
config: volConfig,
driver: driver,
}
}
// Name returns volume's name.
func (v Volume) Name() string {
return v.name
}
// Pool returns the volume's pool name.
func (v Volume) Pool() string {
return v.pool
}
// Config returns the volume's (unexpanded) config.
func (v Volume) Config() map[string]string {
return v.config
}
// ExpandedConfig returns either the value of the volume's config key or the pool's config "volume.{key}" value.
func (v Volume) ExpandedConfig(key string) string {
volVal, ok := v.config[key]
if ok {
return volVal
}
return v.poolConfig[fmt.Sprintf("volume.%s", key)]
}
// NewSnapshot instantiates a new Volume struct representing a snapshot of the parent volume.
// This creates a logical representation of the snapshot with the cloned config from its parent volume.
// The parent's UUID is not included.
// Load the snapshot from the database instead if you want to access its own UUID.
func (v Volume) NewSnapshot(snapshotName string) (Volume, error) {
if v.IsSnapshot() {
return Volume{}, fmt.Errorf("Cannot create a snapshot volume from a snapshot")
}
// Deep copy the volume's config.
// A snapshot can have different config keys like its UUID.
// When instantiating a new snapshot from its parent volume,
// this ensures that modifications on the snapshots config
// aren't propagated to the parent volume.
snapConfig := make(map[string]string, len(v.config))
for key, value := range v.config {
if key == "volatile.uuid" {
// Don't copy the parent volume's UUID.
continue
}
snapConfig[key] = value
}
fullSnapName := GetSnapshotVolumeName(v.name, snapshotName)
vol := NewVolume(v.driver, v.pool, v.volType, v.contentType, fullSnapName, snapConfig, v.poolConfig)
// Propagate filesystem probe mode of parent volume.
vol.SetMountFilesystemProbe(v.mountFilesystemProbe)
return vol, nil
}
// IsSnapshot indicates if volume is a snapshot.
func (v Volume) IsSnapshot() bool {
return shared.IsSnapshot(v.name)
}
// MountPath returns the path where the volume will be mounted.
func (v Volume) MountPath() string {
if v.mountCustomPath != "" {
return v.mountCustomPath
}
volName := v.name
if v.volType == VolumeTypeCustom && v.contentType == ContentTypeISO {
volName = fmt.Sprintf("%s%s", volName, isoVolSuffix)
}
return GetVolumeMountPath(v.pool, v.volType, volName)
}
// mountLockName returns the lock name to use for mount/unmount operations on a volume.
func (v Volume) mountLockName() string {
return OperationLockName("Mount", v.pool, v.volType, v.contentType, v.name)
}
// MountLock attempts to lock the mount lock for the volume and returns the UnlockFunc.
func (v Volume) MountLock() (locking.UnlockFunc, error) {
return locking.Lock(context.TODO(), v.mountLockName())
}
// MountRefCountIncrement increments the mount ref counter for the volume and returns the new value.
func (v Volume) MountRefCountIncrement() uint {
return refcount.Increment(v.mountLockName(), 1)
}
// MountRefCountDecrement decrements the mount ref counter for the volume and returns the new value.
func (v Volume) MountRefCountDecrement() uint {
return refcount.Decrement(v.mountLockName(), 1)
}
// MountInUse returns whether the volume has a mount ref counter >0.
func (v Volume) MountInUse() bool {
return refcount.Get(v.mountLockName()) > 0
}
// EnsureMountPath creates the volume's mount path if missing, then sets the correct permission for the type.
// If permission setting fails and the volume is a snapshot then the error is ignored as snapshots are read only.
func (v Volume) EnsureMountPath() error {
volPath := v.MountPath()
revert := revert.New()
defer revert.Fail()
// Create volume's mount path if missing, with any created directories set to 0711.
if !shared.PathExists(volPath) {
if v.IsSnapshot() {
// Create the parent directory if needed.
parentName, _, _ := api.GetParentAndSnapshotName(v.name)
err := createParentSnapshotDirIfMissing(v.pool, v.volType, parentName)
if err != nil {
return err
}
}
err := os.Mkdir(volPath, 0711)
if err != nil {
return fmt.Errorf("Failed to create mount directory %q: %w", volPath, err)
}
revert.Add(func() { _ = os.Remove(volPath) })
}
// Set very restrictive mode 0100 for non-custom, non-bucket and non-image volumes.
mode := os.FileMode(0711)
if v.volType != VolumeTypeCustom && v.volType != VolumeTypeImage && v.volType != VolumeTypeBucket {
mode = os.FileMode(0100)
}
fInfo, err := os.Lstat(volPath)
if err != nil {
return fmt.Errorf("Error getting mount directory info %q: %w", volPath, err)
}
// We expect the mount path to be a directory, so use this for comparison.
compareMode := os.ModeDir | mode
// Set mode of actual volume's mount path if needed.
if fInfo.Mode() != compareMode {
err = os.Chmod(volPath, mode)
// If the chmod failed, return the error as long as the volume is not a snapshot.
// If the volume is a snapshot, we must ignore the error as snapshots are readonly and cannot be
// modified after they are taken, such that any permission error is not fixable at mount time.
if err != nil && !v.IsSnapshot() {
return fmt.Errorf("Failed to chmod mount directory %q (%04o): %w", volPath, mode, err)
}
}
revert.Success()
return nil
}
// MountTask runs the supplied task after mounting the volume if needed. If the volume was mounted
// for this then it is unmounted when the task finishes.
func (v Volume) MountTask(task func(mountPath string, op *operations.Operation) error, op *operations.Operation) error {
// If the volume is a snapshot then call the snapshot specific mount/unmount functions as
// these will mount the snapshot read only.
var err error
if v.IsSnapshot() {
err = v.driver.MountVolumeSnapshot(v, op)
} else {
err = v.driver.MountVolume(v, op)
}
if err != nil {
return err
}
taskErr := task(v.MountPath(), op)
// Try and unmount, even on task error.
if v.IsSnapshot() {
_, err = v.driver.UnmountVolumeSnapshot(v, op)
} else {
_, err = v.driver.UnmountVolume(v, false, op)
}
// Return task error if failed.
if taskErr != nil {
return taskErr
}
// Return unmount error if failed.
if err != nil && !errors.Is(err, ErrInUse) {
return err
}
return nil
}
// UnmountTask runs the supplied task after unmounting the volume if needed.
// If the volume was unmounted for this then it is mounted when the task finishes.
// keepBlockDev indicates if backing block device should be not be deactivated if volume is unmounted.
func (v Volume) UnmountTask(task func(op *operations.Operation) error, keepBlockDev bool, op *operations.Operation) error {
// If the volume is a snapshot then call the snapshot specific mount/unmount functions as
// these will mount the snapshot read only.
if v.IsSnapshot() {
ourUnmount, err := v.driver.UnmountVolumeSnapshot(v, op)
if err != nil {
return err
}
if ourUnmount {
defer func() { _ = v.driver.MountVolumeSnapshot(v, op) }()
}
} else {
ourUnmount, err := v.driver.UnmountVolume(v, keepBlockDev, op)
if err != nil {
return err
}
if ourUnmount {
defer func() { _ = v.driver.MountVolume(v, op) }()
}
}
return task(op)
}
// Snapshots returns a list of snapshots for the volume (in no particular order).
func (v Volume) Snapshots(op *operations.Operation) ([]Volume, error) {
if v.IsSnapshot() {
return nil, fmt.Errorf("Volume is a snapshot")
}
snapshots, err := v.driver.VolumeSnapshots(v, op)
if err != nil {
return nil, err
}
snapVols := make([]Volume, 0, len(snapshots))
for _, snapName := range snapshots {
snapshot, err := v.NewSnapshot(snapName)
if err != nil {
return nil, err
}
snapVols = append(snapVols, snapshot)
}
return snapVols, nil
}
// IsBlockBacked indicates whether storage device is block backed.
func (v Volume) IsBlockBacked() bool {
return v.driver.isBlockBacked(v) || v.mountFilesystemProbe
}
// Type returns the volume type.
func (v Volume) Type() VolumeType {
return v.volType
}
// ContentType returns the content type.
func (v Volume) ContentType() ContentType {
return v.contentType
}
// IsVMBlock returns true if volume is a block volume for virtual machines or associated images.
func (v Volume) IsVMBlock() bool {
return (v.volType == VolumeTypeVM || v.volType == VolumeTypeImage) && v.contentType == ContentTypeBlock
}
// IsCustomBlock returns true if volume is a custom block volume.
func (v Volume) IsCustomBlock() bool {
return (v.volType == VolumeTypeCustom && v.contentType == ContentTypeBlock)
}
// NewVMBlockFilesystemVolume returns a copy of the volume with the content type set to ContentTypeFS and the
// config "size" property set to "size.state" or DefaultVMBlockFilesystemSize if not set.
func (v Volume) NewVMBlockFilesystemVolume() Volume {
// Copy volume config so modifications don't affect original volume.
newConf := make(map[string]string, len(v.config))
for k, v := range v.config {
if k == "zfs.block_mode" {
continue // VM filesystem volumes never use ZFS block mode.
}
newConf[k] = v
}
if v.config["size.state"] != "" {
newConf["size"] = v.config["size.state"]
} else {
// Fallback to the default VM filesystem size.
newConf["size"] = v.driver.Info().DefaultVMBlockFilesystemSize
}
vol := NewVolume(v.driver, v.pool, v.volType, ContentTypeFS, v.name, newConf, v.poolConfig)
// Propagate filesystem probe mode of parent volume.
vol.SetMountFilesystemProbe(v.mountFilesystemProbe)
return vol
}
// SetQuota calls SetVolumeQuota on the Volume's driver.
func (v Volume) SetQuota(size string, allowUnsafeResize bool, op *operations.Operation) error {
return v.driver.SetVolumeQuota(v, size, allowUnsafeResize, op)
}
// SetConfigSize sets the size config property on the Volume (does not resize volume).
func (v Volume) SetConfigSize(size string) {
v.config["size"] = size
}
// SetConfigStateSize sets the size.state config property on the Volume (does not resize volume).
func (v Volume) SetConfigStateSize(size string) {
v.config["size.state"] = size
}
// ConfigBlockFilesystem returns the filesystem to use for block volumes. Returns config value "block.filesystem"
// if defined in volume or pool's volume config, otherwise the DefaultFilesystem.
func (v Volume) ConfigBlockFilesystem() string {
fs := v.ExpandedConfig("block.filesystem")
if fs != "" {
return fs
}
return DefaultFilesystem
}
// ConfigBlockMountOptions returns the filesystem mount options to use for block volumes. Returns config value
// "block.mount_options" if defined in volume or pool's volume config, otherwise defaultFilesystemMountOptions.
func (v Volume) ConfigBlockMountOptions() string {
fs := v.ExpandedConfig("block.mount_options")
if fs != "" {
return fs
}
// Use some special options if the filesystem for the volume is BTRFS.
if v.ConfigBlockFilesystem() == "btrfs" {
return "user_subvol_rm_allowed,discard"
}
return defaultFilesystemMountOptions
}
// ConfigSize returns the size to use when creating new a volume. Returns config value "size" if defined in volume
// or pool's volume config, otherwise for block volumes and block-backed volumes the defaultBlockSize. For other
// volumes an empty string is returned if no size is defined.
func (v Volume) ConfigSize() string {
size := v.ExpandedConfig("size")
// If volume size isn't defined in either volume or pool config, then for block volumes or block-backed
// volumes return the defaultBlockSize.
if (size == "" || size == "0") && (v.contentType == ContentTypeBlock || v.IsBlockBacked()) {
return DefaultBlockSize
}
// Return defined size or empty string if not defined.
return size
}
// ConfigSizeFromSource derives the volume size to use for a new volume when copying from a source volume.
// Where possible (if the source volume has a volatile.rootfs.size property), it checks that the source volume
// isn't larger than the volume's "size" setting and the pool's "volume.size" setting.
func (v Volume) ConfigSizeFromSource(srcVol Volume) (string, error) {
// If source is not an image, then only use volume specified size. This is so the pool volume size isn't
// taken into account for non-image volume copies.
if srcVol.volType != VolumeTypeImage {
return v.config["size"], nil
}
// VM config filesystem volumes should always have a fixed specified size, so just return volume size.
if v.volType == VolumeTypeVM && v.contentType == ContentTypeFS {
return v.config["size"], nil
}
// If the source image doesn't have any size information, then use volume/pool/default size in that order.
if srcVol.config["volatile.rootfs.size"] == "" {
return v.ConfigSize(), nil
}
imgSizeBytes, err := units.ParseByteSizeString(srcVol.config["volatile.rootfs.size"])
if err != nil {
return "", err
}
// If volume/pool size is specified (excluding default size), then check it against the image minimum size.
volSize := v.ExpandedConfig("size")
if volSize != "" && volSize != "0" {
volSizeBytes, err := units.ParseByteSizeString(volSize)
if err != nil {
return volSize, err
}
// Round the vol size (for comparison only) because some storage drivers round volumes they create,
// and so the published images created from those volumes will also be rounded and will not be
// directly usable with the same size setting without also rounding for this check.
// Because we are not altering the actual size returned to use for the new volume, this will not
// affect storage drivers that do not use rounding.
volSizeBytes = v.driver.roundVolumeBlockSizeBytes(volSizeBytes)
// The volume/pool specified size is smaller than image minimum size. We must not continue as
// these specified sizes provide protection against unpacking a massive image and filling the pool.
if volSizeBytes < imgSizeBytes {
return "", fmt.Errorf("Source image size (%d) exceeds specified volume size (%d)", imgSizeBytes, volSizeBytes)
}
// Use the specified volume size.
return volSize, nil
}
// If volume/pool size not specified above, then fallback to default volume size (if relevant) and compare.
volSize = v.ConfigSize()
if volSize != "" && volSize != "0" {
volSizeBytes, err := units.ParseByteSizeString(volSize)
if err != nil {
return "", err
}
// Use image minimum size as volSize if the default volume size is smaller.
if volSizeBytes < imgSizeBytes {
return srcVol.config["volatile.rootfs.size"], nil
}
}
// Use the default volume size.
return volSize, nil
}
// SetMountFilesystemProbe enables or disables the probing mode when mounting the filesystem volume.
func (v *Volume) SetMountFilesystemProbe(probe bool) {
v.mountFilesystemProbe = probe
}
// SetHasSource indicates whether the Volume is created from a source.
func (v *Volume) SetHasSource(hasSource bool) {
v.hasSource = hasSource
}
// SetParentUUID sets the parent volume's UUID for snapshots.
func (v *Volume) SetParentUUID(parentUUID string) {
v.parentUUID = parentUUID
}
// Clone returns a copy of the volume.
func (v Volume) Clone() Volume {
// Copy the config map to avoid internal modifications affecting external state.
newConfig := make(map[string]string, len(v.config))
for k, v := range v.config {
newConfig[k] = v
}
// Copy the pool config map to avoid internal modifications affecting external state.
newPoolConfig := make(map[string]string, len(v.poolConfig))
for k, v := range v.poolConfig {
newPoolConfig[k] = v
}
return NewVolume(v.driver, v.pool, v.volType, v.contentType, v.name, newConfig, newPoolConfig)
}
// NewVolumeCopy returns a container for copying a volume and its snapshots.
func NewVolumeCopy(vol Volume, snapshots ...Volume) VolumeCopy {
modifiedSnapshots := make([]Volume, 0, len(snapshots))
// Set the parent volume's UUID for each snapshot.
// If the parent volume doesn't have an UUID it's a noop.
for _, snapshot := range snapshots {
snapshot.SetParentUUID(vol.config["volatile.uuid"])
modifiedSnapshots = append(modifiedSnapshots, snapshot)
}
return VolumeCopy{
Volume: vol,
Snapshots: modifiedSnapshots,
}
}