[lxc-devel] [lxd/master] Storage LVM driver - pool create & delete

tomponline on Github lxc-bot at linuxcontainers.org
Tue Dec 17 17:23:13 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 422 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20191217/1d207b52/attachment-0001.bin>
-------------- next part --------------
From 88d9b565713d2091933e48be282785ef8edf4ce7 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 13 Dec 2019 11:40:08 +0000
Subject: [PATCH 1/5] lxd/storage/drivers/interface: Comments on pool
 mount/unmount definitions

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/storage/drivers/interface.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lxd/storage/drivers/interface.go b/lxd/storage/drivers/interface.go
index 9dfed58b15..7bc8e93a02 100644
--- a/lxd/storage/drivers/interface.go
+++ b/lxd/storage/drivers/interface.go
@@ -28,7 +28,12 @@ type Driver interface {
 	// Pool.
 	Create() error
 	Delete(op *operations.Operation) error
+	// Mount mounts a storage pool if needed, returns true if we caused a new mount, false if
+	// already mounted.
 	Mount() (bool, error)
+
+	// Unmount unmounts a storage pool if needed, returns true if unmounted, false if was not
+	// mounted.
 	Unmount() (bool, error)
 	GetResources() (*api.ResourcesStoragePool, error)
 	Validate(config map[string]string) error

From 5417fd119ab9cbb1c289f34b5fb2f644e5df611c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 13 Dec 2019 14:13:29 +0000
Subject: [PATCH 2/5] shared/util: Adds comment to TryRunCommand

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 shared/util.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/shared/util.go b/shared/util.go
index e307f985fe..0a80f5dd7e 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -861,6 +861,8 @@ func RunCommandWithFds(stdin io.Reader, stdout io.Writer, name string, arg ...st
 	return nil
 }
 
+// TryRunCommand runs the specified command up to 20 times with a 500ms delay between each call
+// until it runs without an error. If after 20 times it is still failing then returns the error.
 func TryRunCommand(name string, arg ...string) (string, error) {
 	var err error
 	var output string

From 0fa2bc7d2001f249ee5992ddcad7fe6d735674a8 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 17 Dec 2019 17:03:59 +0000
Subject: [PATCH 3/5] lxd/storage/backend/lxd: Fixes bug with non-project aware
 vol storage name in RenameInstance

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/storage/backend_lxd.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
index 0c89abdaa2..faa57209eb 100644
--- a/lxd/storage/backend_lxd.go
+++ b/lxd/storage/backend_lxd.go
@@ -1079,7 +1079,7 @@ func (b *lxdBackend) RenameInstance(inst instance.Instance, newName string, op *
 		return err
 	}
 
-	err = b.ensureInstanceSymlink(inst.Type(), inst.Project(), newName, drivers.GetVolumeMountPath(b.name, volType, newName))
+	err = b.ensureInstanceSymlink(inst.Type(), inst.Project(), newName, drivers.GetVolumeMountPath(b.name, volType, newVolStorageName))
 	if err != nil {
 		return err
 	}

From fbfe9f34af084fd49df04e7c52faf7c9308e6b66 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 17 Dec 2019 17:04:38 +0000
Subject: [PATCH 4/5] lxd/storage/drivers/utils: Removes implication of project
 awareness from driver mount point helpers

Drivers are not project aware, and instead rely on the volume name to be prefixed with project before being passed into driver.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/storage/drivers/utils.go | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/lxd/storage/drivers/utils.go b/lxd/storage/drivers/utils.go
index 1ae7cdbfd5..f4df80eac8 100644
--- a/lxd/storage/drivers/utils.go
+++ b/lxd/storage/drivers/utils.go
@@ -10,7 +10,6 @@ import (
 
 	"golang.org/x/sys/unix"
 
-	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/shared"
 )
 
@@ -149,20 +148,19 @@ func GetPoolMountPath(poolName string) string {
 }
 
 // GetVolumeMountPath returns the mount path for a specific volume based on its pool and type and
-// whether it is a snapshot or not.
-// For VolumeTypeImage the volName is the image fingerprint.
+// whether it is a snapshot or not. For VolumeTypeImage the volName is the image fingerprint.
 func GetVolumeMountPath(poolName string, volType VolumeType, volName string) string {
 	if shared.IsSnapshot(volName) {
-		return shared.VarPath("storage-pools", poolName, fmt.Sprintf("%s-snapshots", string(volType)), project.Prefix("default", volName))
+		return shared.VarPath("storage-pools", poolName, fmt.Sprintf("%s-snapshots", string(volType)), volName)
 	}
 
-	return shared.VarPath("storage-pools", poolName, string(volType), project.Prefix("default", volName))
+	return shared.VarPath("storage-pools", poolName, string(volType), volName)
 }
 
 // GetVolumeSnapshotDir gets the snapshot mount directory for the parent volume.
 func GetVolumeSnapshotDir(poolName string, volType VolumeType, volName string) string {
 	parent, _, _ := shared.InstanceGetParentAndSnapshotName(volName)
-	return shared.VarPath("storage-pools", poolName, fmt.Sprintf("%s-snapshots", string(volType)), project.Prefix("default", parent))
+	return shared.VarPath("storage-pools", poolName, fmt.Sprintf("%s-snapshots", string(volType)), parent)
 }
 
 // GetSnapshotVolumeName returns the full volume name for a parent volume and snapshot name.

From 8f6d873ec922e20b04aa753e28216f88afe39908 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 17 Dec 2019 11:22:34 +0000
Subject: [PATCH 5/5] lxd/storage/drivers/lvm: LVM driver implementation

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/storage/drivers/drivers_lvm.go         | 493 +++++++++++++++++++++
 lxd/storage/drivers/drivers_lvm_utils.go   | 293 ++++++++++++
 lxd/storage/drivers/drivers_lvm_volumes.go | 178 ++++++++
 3 files changed, 964 insertions(+)
 create mode 100644 lxd/storage/drivers/drivers_lvm.go
 create mode 100644 lxd/storage/drivers/drivers_lvm_utils.go
 create mode 100644 lxd/storage/drivers/drivers_lvm_volumes.go

diff --git a/lxd/storage/drivers/drivers_lvm.go b/lxd/storage/drivers/drivers_lvm.go
new file mode 100644
index 0000000000..a88301d2bd
--- /dev/null
+++ b/lxd/storage/drivers/drivers_lvm.go
@@ -0,0 +1,493 @@
+package drivers
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/lxc/lxd/lxd/operations"
+	"github.com/lxc/lxd/lxd/storage/locking"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+	log "github.com/lxc/lxd/shared/log15"
+	"github.com/lxc/lxd/shared/units"
+)
+
+var lvmLoaded bool
+var lvmVersion string
+
+type lvm struct {
+	common
+	loopFile *os.File // Stores a reference to the "mounted" (opened) loopback file.
+}
+
+func (d *lvm) load() error {
+	if lvmLoaded {
+		return nil
+	}
+
+	// Validate the required binaries.
+	for _, tool := range []string{"lvm"} {
+		_, err := exec.LookPath(tool)
+		if err != nil {
+			return fmt.Errorf("Required tool '%s' is missing", tool)
+		}
+	}
+
+	// Detect and record the version.
+	if lvmVersion == "" {
+		output, err := shared.RunCommand("lvm", "version")
+		if err != nil {
+			return fmt.Errorf("Error getting LVM version: %v", err)
+		}
+
+		lines := strings.Split(output, "\n")
+		for idx, line := range lines {
+			fields := strings.SplitAfterN(line, ":", 2)
+			if len(fields) < 2 {
+				continue
+			}
+
+			if !strings.Contains(line, "version:") {
+				continue
+			}
+
+			if idx > 0 {
+				lvmVersion += " / "
+			}
+
+			lvmVersion += strings.TrimSpace(fields[1])
+		}
+	}
+
+	lvmLoaded = true
+	return nil
+}
+
+// Info returns info about the driver and its environment.
+func (d *lvm) Info() Info {
+	return Info{
+		Name:                  "lvm",
+		Version:               lvmVersion,
+		OptimizedImages:       true,
+		PreservesInodes:       !d.state.OS.RunningInUserNS,
+		Remote:                false,
+		VolumeTypes:           []VolumeType{VolumeTypeCustom},
+		BlockBacking:          false,
+		RunningQuotaResize:    false,
+		RunningSnapshotFreeze: false,
+	}
+}
+
+// usesThinpool indicates whether the config specifies to use a thin pool or not.
+func (d *lvm) usesThinpool() bool {
+	// Default is to use a thinpool.
+	if d.config["lvm.use_thinpool"] == "" {
+		return true
+	}
+
+	return shared.IsTrue(d.config["lvm.use_thinpool"])
+}
+
+// getLvmThinpoolName returns the thinpool volume to use.
+func (d *lvm) getLvmThinpoolName() string {
+	if d.config["lvm.thinpool_name"] != "" {
+		return d.config["lvm.thinpool_name"]
+	}
+
+	return "LXDThinPool"
+}
+
+// getLvmFilesystem returns the filesystem to use for logical volumes.
+func (d *lvm) getLvmFilesystem() string {
+	if d.config["block.filesystem"] != "" {
+		return d.config["block.filesystem"]
+	}
+
+	if d.config["volume.block.filesystem"] != "" {
+		return d.config["volume.block.filesystem"]
+	}
+
+	return "ext4"
+}
+
+// getLvmVolumeSize returns the size to use when creating new logical volumes.
+func (d *lvm) getLvmVolumeSize() (string, error) {
+	sz, err := units.ParseByteSizeString(d.config["size"])
+	if err != nil {
+		return "", err
+	}
+
+	// Safety net: Set to default value.
+	if sz == 0 {
+		sz, _ = units.ParseByteSizeString("10GB")
+	}
+
+	return fmt.Sprintf("%d", sz), nil
+}
+
+// Create creates the storage pool on the storage device.
+func (d *lvm) Create() error {
+	d.config["volatile.initial_source"] = d.config["source"]
+
+	defaultSource := filepath.Join(shared.VarPath("disks"), fmt.Sprintf("%s.img", d.name))
+	var err error
+	var pvExists, vgExists bool
+	var pvName string
+
+	revertFuncs := []func(){}
+	defer func() {
+		for _, revertFunc := range revertFuncs {
+			revertFunc()
+		}
+	}()
+
+	if d.config["source"] == "" || d.config["source"] == defaultSource {
+		// We are using an LXD internal loopback file.
+		d.config["source"] = defaultSource
+		if d.config["lvm.vg_name"] == "" {
+			d.config["lvm.vg_name"] = d.name
+		}
+
+		size, err := units.ParseByteSizeString(d.config["size"])
+		if err != nil {
+			return err
+		}
+
+		err = createSparseFile(d.config["source"], size)
+		if err != nil {
+			return fmt.Errorf("Failed to create sparse file %q: %s", d.config["source"], err)
+		}
+
+		revertFuncs = append(revertFuncs, func() { os.Remove(d.config["source"]) })
+
+		// Open the loop file.
+		_, err = d.Mount()
+		if err != nil {
+			return err
+		}
+		defer d.Unmount()
+
+		// Check if the physical volume already exists.
+		pvName = d.loopFile.Name()
+		pvExists, err = d.pysicalVolumeExists(pvName)
+		if err != nil {
+			return err
+		}
+
+		// Check if the volume group already exists.
+		vgExists, err = d.volumeGroupExists(d.config["lvm.vg_name"])
+		if err != nil {
+			return err
+		}
+	} else if filepath.IsAbs(d.config["source"]) {
+		// We are using an existing physical device.
+		srcPath := shared.HostPath(d.config["source"])
+
+		// Size is ignored as the physical device is a fixed size.
+		d.config["size"] = ""
+
+		if d.config["lvm.vg_name"] == "" {
+			d.config["lvm.vg_name"] = d.name
+		}
+		d.config["source"] = d.config["lvm.vg_name"]
+
+		if !shared.IsBlockdevPath(srcPath) {
+			return fmt.Errorf("Custom loop file locations are not supported")
+		}
+
+		// Check if the physical volume already exists.
+		pvName = srcPath
+		pvExists, err = d.pysicalVolumeExists(pvName)
+		if err != nil {
+			return err
+		}
+
+		// Check if the volume group already exists.
+		vgExists, err = d.volumeGroupExists(d.config["lvm.vg_name"])
+		if err != nil {
+			return err
+		}
+	} else if d.config["source"] != "" {
+		// We are using an existing volume group, so physical must exist already.
+		pvExists = true
+
+		// Size is ignored as the existing device is a fixed size.
+		d.config["size"] = ""
+
+		if d.config["lvm.vg_name"] != "" && d.config["lvm.vg_name"] != d.config["source"] {
+			return fmt.Errorf("Invalid combination of \"source\" and \"lvm.vg_name\" property")
+		}
+
+		d.config["lvm.vg_name"] = d.config["source"]
+
+		// Check the volume group already exists.
+		vgExists, err = d.volumeGroupExists(d.config["lvm.vg_name"])
+		if err != nil {
+			return err
+		}
+
+		if !vgExists {
+			return fmt.Errorf("The requested volume group \"%s\" does not exist", d.config["lvm.vg_name"])
+		}
+	} else {
+		return fmt.Errorf("Invalid \"source\" property")
+	}
+
+	// This is an internal error condition which should never be hit.
+	if d.config["lvm.vg_name"] == "" {
+		return fmt.Errorf("No name for volume group detected")
+	}
+
+	// Used to track the result of checking whether the thin pool exists during the existing
+	// volume group empty checks to avoid having to do it twice.
+	thinPoolExists := false
+
+	if vgExists {
+		// Check that the volume group is empty. Otherwise we will refuse to use it.
+		// The LV count returned includes both normal logical volumes and thin volumes.
+		// This will naturally also check for the LXD-created reserved volume "lxd" which
+		// indicates this volume is already in use by another storage pool.
+		count, err := d.countLogicalVolumes(d.config["lvm.vg_name"])
+		if err != nil {
+			return fmt.Errorf("Failed to determine whether the volume group \"%s\" is empty: %v", d.config["lvm.vg_name"], err)
+		}
+
+		empty := false
+		if count > 0 {
+			if d.usesThinpool() {
+				// Always check if the thin pool exists as we may need to create it.
+				thinPoolExists, err = d.thinpoolExists(d.config["lvm.vg_name"], d.getLvmThinpoolName())
+				if err != nil {
+					return fmt.Errorf("Failed to determine whether thinpool \"%s\" exists in volume group \"%s\": %s", d.config["lvm.vg_name"], d.getLvmThinpoolName(), err)
+				}
+
+				// If the single volume is the storage pool's thin pool LV then we
+				// still consider this an empty volume group.
+				if thinPoolExists && count == 1 {
+					empty = true
+				}
+			}
+		} else {
+			empty = true
+		}
+
+		if !empty {
+			return fmt.Errorf("Volume group \"%s\" is not empty", d.config["lvm.vg_name"])
+		}
+	} else {
+		// Create phyiscal volume if doesn't exist.
+		if !pvExists {
+			// This is an internal error condition which should never be hit.
+			if pvName == "" {
+				return fmt.Errorf("No name for physical volume detected")
+			}
+
+			_, err := shared.TryRunCommand("pvcreate", pvName)
+			if err != nil {
+				return fmt.Errorf("Failed to create the physical volume for the lvm storage pool: %v", err)
+			}
+
+			revertFuncs = append(revertFuncs, func() { shared.TryRunCommand("pvremove", pvName) })
+		}
+
+		// Create volume group.
+		_, err := shared.TryRunCommand("vgcreate", d.config["lvm.vg_name"], pvName)
+		if err != nil {
+			return fmt.Errorf("Failed to create the volume group for the lvm storage pool: %v", err)
+		}
+
+		revertFuncs = append(revertFuncs, func() { shared.TryRunCommand("vgremove", d.config["lvm.vg_name"]) })
+	}
+
+	// Create thin pool if needed.
+	if d.usesThinpool() {
+		if !thinPoolExists {
+			err = d.createThinpool(d.Info().Version, d.config["lvm.vg_name"], d.getLvmThinpoolName())
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	// Create the "lxd" special volume to indicate this volume group is in use.
+	// Use the default project as this volume is associated to the storage pool rather than a
+	// specific project, and storage pools don't support projects.
+	// Use 512 as the size, as this is the minimal size allowed by LVMCreateLogicalVolume.
+	err = d.createLogicalVolume("default", d.config["lvm.vg_name"], d.getLvmThinpoolName(), "lxd", d.getLvmFilesystem(), "512", "", d.usesThinpool())
+	if err != nil {
+		return fmt.Errorf("Error Creating LVM LV for new image: %v", err)
+	}
+
+	revertFuncs = nil
+	return nil
+}
+
+// Delete removes the storage pool from the storage device.
+func (d *lvm) Delete(op *operations.Operation) error {
+	if d.config["source"] == "" {
+		return fmt.Errorf("No \"source\" property found for the storage pool")
+	}
+
+	// Open the loop file if needed.
+	_, err := d.Mount()
+	if err != nil {
+		return err
+	}
+	defer d.Unmount()
+
+	vgExists, err := d.volumeGroupExists(d.config["lvm.vg_name"])
+	if err != nil {
+		return err
+	}
+
+	if vgExists {
+		// Delete the "lxd" special volume if exists.
+		// Use the default project as this volume is associated to the storage pool rather
+		// than a specific project, and storage pools don't support projects.
+		lxdVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], "", "lxd")
+		lxdVolExists, err := d.logicalVolumeExists(lxdVolDevPath)
+		if err != nil {
+			return err
+		}
+
+		if lxdVolExists {
+			output, err := shared.TryRunCommand("lvremove", "-f", lxdVolDevPath)
+			if err != nil {
+				return fmt.Errorf("Failed to delete \"lxd\" volume from volume group \"%s\": %s", d.config["lvm.vg_name"], output)
+			}
+		}
+
+		// Check if thin pool is empty and remove.
+		if d.usesThinpool() {
+			thinVolCount, err := d.countThinVolumes(d.config["lvm.vg_name"], d.getLvmThinpoolName())
+			if err != nil && err != errLVMNotFound {
+				return err
+			}
+
+			// Check that the count in the thin pool is zero. If not, we need to
+			// assume that other users are using the volume group, so don't remove
+			// it. This actually goes against policy since we explicitly state: our
+			// pool, and nothing but our pool but still, let's not hurt users.
+			if err == nil && thinVolCount == 0 {
+				// Use default project as storage pools don't support projects.
+				devPath := d.lvmDevPath(d.config["lvm.vg_name"], "", d.getLvmThinpoolName())
+				output, err := shared.TryRunCommand("lvremove", "-f", devPath)
+				if err != nil {
+					return fmt.Errorf("Failed to delete thin pool \"%s\" from volume group \"%s\": %s", d.getLvmThinpoolName(), d.config["lvm.vg_name"], output)
+				}
+			}
+		}
+
+		// Check if volume group is empty and remove.
+		lvCount, err := d.countLogicalVolumes(d.config["lvm.vg_name"])
+		if err != nil && err != errLVMNotFound {
+			return err
+		}
+
+		// Check that the count in the volume group is zero. If not, we need to assume that
+		// other users are using the volume group, so don't remove it. This actually goes
+		// against policy since we explicitly state: our pool, and nothing but our pool but
+		// still, let's not hurt users.
+		if err == nil && lvCount == 0 {
+			_, err := shared.TryRunCommand("vgremove", "-f", d.config["lvm.vg_name"])
+			if err != nil {
+				return fmt.Errorf("Failed to delete the volume group for the lvm storage pool: %v", err)
+			}
+		}
+	}
+
+	if d.loopFile != nil {
+		// Set LO_FLAGS_AUTOCLEAR before removing the loop file otherwise we will get EBADF.
+		err = SetAutoclearOnLoopDev(int(d.loopFile.Fd()))
+		if err != nil {
+			d.logger.Warn("Failed to set LO_FLAGS_AUTOCLEAR on loop device, manual cleanup needed", log.Ctx{"err": err})
+		}
+
+		_, err := shared.TryRunCommand("pvremove", "-f", d.loopFile.Name())
+		if err != nil {
+			d.logger.Warn("Failed to destroy the physical volume for the lvm storage pool", log.Ctx{"err": err})
+		}
+
+		d.loopFile.Close()
+	}
+
+	if filepath.IsAbs(d.config["source"]) && !shared.IsBlockdevPath(d.config["source"]) {
+		// This is a loop file so deconfigure the associated loop device.
+		err = os.Remove(d.config["source"])
+		if err != nil {
+			return err
+		}
+	}
+
+	// Wipe everything in the storage pool directory.
+	err = wipeDirectory(GetPoolMountPath(d.name))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (d *lvm) Validate(config map[string]string) error {
+	return nil
+}
+
+func (d *lvm) Update(changedConfig map[string]string) error {
+	return nil
+}
+
+// Mount mounts the storage pool (this does nothing for external LVM pools, but for loopback image
+// LVM pools this creates a loop device).
+func (d *lvm) Mount() (bool, error) {
+	source := d.config["source"]
+	if source == "" {
+		return false, fmt.Errorf("no \"source\" property found for the storage pool")
+	}
+
+	if filepath.IsAbs(source) && !shared.IsBlockdevPath(source) {
+		unlock := locking.Lock(d.name, "", "")
+		defer unlock()
+
+		// Try to prepare new loop device.
+		loopF, loopErr := PrepareLoopDev(source, 0)
+		if loopErr != nil {
+			return false, loopErr
+		}
+
+		// Make sure that LO_FLAGS_AUTOCLEAR is unset, so that the loopback device will not
+		// autodestruct on last close.
+		loopErr = UnsetAutoclearOnLoopDev(int(loopF.Fd()))
+		if loopErr != nil {
+			return false, loopErr
+		}
+
+		d.loopFile = loopF
+
+		// We opened a new file handle to the loop device.
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// Unmount unmounts the storage pool (this does nothing for external LVM pools, but for loopback
+// image LVM pools this closes the loop device handle if needed).
+func (d *lvm) Unmount() (bool, error) {
+	if d.loopFile != nil {
+		d.loopFile.Close()
+		d.loopFile = nil
+
+		// We closed the open file handle.
+		return true, nil
+	}
+
+	// No loop device was open, so nothing to close.
+	return false, nil
+}
+
+func (d *lvm) GetResources() (*api.ResourcesStoragePool, error) {
+	return d.vfsGetResources()
+}
diff --git a/lxd/storage/drivers/drivers_lvm_utils.go b/lxd/storage/drivers/drivers_lvm_utils.go
new file mode 100644
index 0000000000..0f7417d308
--- /dev/null
+++ b/lxd/storage/drivers/drivers_lvm_utils.go
@@ -0,0 +1,293 @@
+package drivers
+
+import (
+	"fmt"
+	"os/exec"
+	"strconv"
+	"strings"
+	"syscall"
+
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/units"
+	"github.com/lxc/lxd/shared/version"
+)
+
+var errLVMNotFound = fmt.Errorf("Not found")
+
+// isLVMNotFoundExitError checks whether the supplied error is an exit error from an LVM command
+// meaning that the object was not found. Returns true if it is (exit status 5) false if not.
+func (d *lvm) isLVMNotFoundExitError(err error) bool {
+	runErr, ok := err.(shared.RunError)
+	if ok {
+		exitError, ok := runErr.Err.(*exec.ExitError)
+		if ok {
+			waitStatus := exitError.Sys().(syscall.WaitStatus)
+			if waitStatus.ExitStatus() == 5 {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+// pysicalVolumeExists checks if an LVM Physical Volume exists.
+func (d *lvm) pysicalVolumeExists(pvName string) (bool, error) {
+	_, err := shared.RunCommand("pvs", "--noheadings", "-o", "lv_attr", pvName)
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return false, nil
+		}
+
+		return false, fmt.Errorf("error checking for physical volume \"%s\"", pvName)
+	}
+
+	return true, nil
+}
+
+// volumeGroupExists checks if an LVM Volume Group exists.
+func (d *lvm) volumeGroupExists(vgName string) (bool, error) {
+	_, err := shared.RunCommand("vgs", "--noheadings", "-o", "lv_attr", vgName)
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return false, nil
+		}
+
+		return false, fmt.Errorf("error checking for volume group \"%s\"", vgName)
+	}
+
+	return true, nil
+}
+
+// countLogicalVolumes gets the count of volumes in a volume group.
+func (d *lvm) countLogicalVolumes(vgName string) (int, error) {
+	output, err := shared.RunCommand("vgs", "--noheadings", "-o", "lv_count", vgName)
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return -1, errLVMNotFound
+		}
+
+		return -1, err
+	}
+
+	output = strings.TrimSpace(output)
+	return strconv.Atoi(output)
+}
+
+// countThinVolumes gets the count of thin volumes in a thin pool.
+func (d *lvm) countThinVolumes(vgName, poolName string) (int, error) {
+	output, err := shared.RunCommand("lvs", "--noheadings", "-o", "thin_count", fmt.Sprintf("%s/%s", vgName, poolName))
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return -1, errLVMNotFound
+		}
+
+		return -1, err
+	}
+
+	output = strings.TrimSpace(output)
+	return strconv.Atoi(output)
+}
+
+// thinpoolExists checks whether the specified thinpool exists in a volume group.
+func (d *lvm) thinpoolExists(vgName string, poolName string) (bool, error) {
+	output, err := shared.RunCommand("vgs", "--noheadings", "-o", "lv_attr", fmt.Sprintf("%s/%s", vgName, poolName))
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return false, nil
+		}
+
+		return false, fmt.Errorf("error checking for pool \"%s\"", poolName)
+	}
+	// Found LV named poolname, check type:
+	attrs := strings.TrimSpace(string(output[:]))
+	if strings.HasPrefix(attrs, "t") {
+		return true, nil
+	}
+
+	return false, fmt.Errorf("pool named \"%s\" exists but is not a thin pool", poolName)
+}
+
+// lvmDevPath returns the path to the LVM volume device.
+func (d *lvm) lvmDevPath(lvmPool string, volType VolumeType, lvmVolume string) string {
+	if volType == "" {
+		return fmt.Sprintf("/dev/%s/%s", lvmPool, lvmVolume)
+	}
+
+	volTypePrefix, err := d.volumeTypeToLVMType(volType)
+	if err != nil {
+		return ""
+	}
+
+	return fmt.Sprintf("/dev/%s/%s_%s", lvmPool, volTypePrefix, lvmVolume)
+}
+
+// logicalVolumeExists checks whether the specified logical volume exists.
+func (d *lvm) logicalVolumeExists(lvDevPath string) (bool, error) {
+	_, err := shared.RunCommand("lvs", "--noheadings", "-o", "lv_attr", lvDevPath)
+	if err != nil {
+		if d.isLVMNotFoundExitError(err) {
+			return false, nil
+		}
+
+		return false, fmt.Errorf("error checking for logical volume \"%s\"", lvDevPath)
+	}
+
+	return true, nil
+}
+
+// createThinpool creates a thin pool logical volume.
+func (d *lvm) createThinpool(lvmVersion string, vgName string, thinPoolName string) error {
+	exists, err := d.thinpoolExists(vgName, thinPoolName)
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		return nil
+	}
+
+	err = d.createDefaultThinPool(lvmVersion, vgName, thinPoolName)
+	if err != nil {
+		return err
+	}
+
+	poolExists, err := d.thinpoolExists(vgName, thinPoolName)
+	if err != nil {
+		return fmt.Errorf("Error checking for thin pool \"%s\" in \"%s\": %v", thinPoolName, vgName, err)
+	}
+
+	if !poolExists {
+		return fmt.Errorf("Thin pool \"'%s\" does not exist in Volume Group \"%s\"", thinPoolName, vgName)
+	}
+
+	return nil
+}
+
+// createDefaultThinPool creates the default thinpool as 100% the size of the volume group with a 1G
+// meta data volume.
+func (d *lvm) createDefaultThinPool(lvmVersion string, vgName string, thinPoolName string) error {
+	isRecent, err := d.lvmVersionIsAtLeast(lvmVersion, "2.02.99")
+	if err != nil {
+		return fmt.Errorf("Error checking LVM version: %s", err)
+	}
+
+	// Create the thin pool
+	lvmThinPool := fmt.Sprintf("%s/%s", vgName, thinPoolName)
+	if isRecent {
+		_, err = shared.TryRunCommand(
+			"lvcreate",
+			"-Wy", "--yes",
+			"--poolmetadatasize", "1G",
+			"-l", "100%FREE",
+			"--thinpool", lvmThinPool)
+	} else {
+		_, err = shared.TryRunCommand(
+			"lvcreate",
+			"-Wy", "--yes",
+			"--poolmetadatasize", "1G",
+			"-L", "1G",
+			"--thinpool", lvmThinPool)
+	}
+
+	if err != nil {
+		return fmt.Errorf("Could not create LVM thin pool named %s: %v", thinPoolName, err)
+	}
+
+	if !isRecent {
+		// Grow it to the maximum VG size (two step process required by old LVM)
+		_, err = shared.TryRunCommand("lvextend", "--alloc", "anywhere", "-l", "100%FREE", lvmThinPool)
+
+		if err != nil {
+			return fmt.Errorf("Could not grow LVM thin pool named %s: %v", thinPoolName, err)
+		}
+	}
+
+	return nil
+}
+
+// lvmVersionIsAtLeast checks whether the installed version of LVM is at least the specific version.
+func (d *lvm) lvmVersionIsAtLeast(sTypeVersion string, versionString string) (bool, error) {
+	lvmVersionString := strings.Split(sTypeVersion, "/")[0]
+
+	lvmVersion, err := version.Parse(lvmVersionString)
+	if err != nil {
+		return false, err
+	}
+
+	inVersion, err := version.Parse(versionString)
+	if err != nil {
+		return false, err
+	}
+
+	if lvmVersion.Compare(inVersion) < 0 {
+		return false, nil
+	}
+
+	return true, nil
+}
+
+// createLogicalVolume creates a logical volume.
+func (d *lvm) createLogicalVolume(projectName, vgName, thinPoolName, lvName, lvFsType, lvSize string, volType VolumeType, makeThinLv bool) error {
+	var output string
+	var err error
+
+	// Round the size to closest 512 bytes
+	lvSizeInt, err := units.ParseByteSizeString(lvSize)
+	if err != nil {
+		return err
+	}
+
+	lvSizeInt = int64(lvSizeInt/512) * 512
+	lvSizeString := units.GetByteSizeString(lvSizeInt, 0)
+
+	lvmPoolVolumeName := d.lvmFullVolumeName(volType, lvName)
+	if makeThinLv {
+		targetVg := fmt.Sprintf("%s/%s", vgName, thinPoolName)
+		_, err = shared.TryRunCommand("lvcreate", "-Wy", "--yes", "--thin", "-n", lvmPoolVolumeName, "--virtualsize", lvSizeString, targetVg)
+	} else {
+		_, err = shared.TryRunCommand("lvcreate", "-Wy", "--yes", "-n", lvmPoolVolumeName, "--size", lvSizeString, vgName)
+	}
+	if err != nil {
+		return fmt.Errorf("Could not create thin LV named %s: %v", lvmPoolVolumeName, err)
+	}
+
+	fsPath := d.lvmDevPath(vgName, volType, lvName)
+
+	output, err = MakeFSType(fsPath, lvFsType, nil)
+	if err != nil {
+		return fmt.Errorf("Error making filesystem on image LV: %v (%s)", err, output)
+	}
+
+	return nil
+}
+
+// lvmFullVolumeName returns the logical volume's full name with volume type prefix.
+func (d *lvm) lvmFullVolumeName(volType VolumeType, lvmVolume string) string {
+	if volType == "" {
+		return lvmVolume
+	}
+
+	volTypePrefix, err := d.volumeTypeToLVMType(volType)
+	if err != nil {
+		return ""
+	}
+
+	return fmt.Sprintf("%s_%s", volTypePrefix, lvmVolume)
+}
+
+// volumeTypeToLVMType converts volume type to internal volume prefix.
+func (d *lvm) volumeTypeToLVMType(volType VolumeType) (string, error) {
+	switch volType {
+	case VolumeTypeContainer:
+		return "containers", nil
+	case VolumeTypeVM:
+		return "virtual-machines", nil
+	case VolumeTypeImage:
+		return "images", nil
+	case VolumeTypeCustom:
+		return "custom", nil
+	}
+
+	return "", fmt.Errorf("Invalid storage volume type")
+}
diff --git a/lxd/storage/drivers/drivers_lvm_volumes.go b/lxd/storage/drivers/drivers_lvm_volumes.go
new file mode 100644
index 0000000000..7ba21f1543
--- /dev/null
+++ b/lxd/storage/drivers/drivers_lvm_volumes.go
@@ -0,0 +1,178 @@
+package drivers
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/operations"
+)
+
+// GetVolumeUsage returns the disk space used by the volume.
+func (d *lvm) GetVolumeUsage(vol Volume) (int64, error) {
+	return 0, ErrNotImplemented
+
+}
+
+// ValidateVolume validates the supplied volume config.
+func (d *lvm) ValidateVolume(vol Volume, removeUnknownKeys bool) error {
+	return d.validateVolume(vol, nil, removeUnknownKeys)
+}
+
+// HasVolume indicates whether a specific volume exists on the storage pool.
+func (d *lvm) HasVolume(vol Volume) bool {
+	lxdVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.name)
+	lxdVolExists, err := d.logicalVolumeExists(lxdVolDevPath)
+	if err != nil {
+		return false
+	}
+
+	return lxdVolExists
+}
+
+// GetVolumeDiskPath returns the location of a disk volume.
+func (d *lvm) GetVolumeDiskPath(vol Volume) (string, error) {
+	return "", ErrNotImplemented
+}
+
+// CreateVolume creates an empty volume and can optionally fill it by executing the supplied
+// filler function.
+func (d *lvm) CreateVolume(vol Volume, filler *VolumeFiller, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// MigrateVolume sends a volume for migration.
+func (d *lvm) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, op *operations.Operation) error {
+	if vol.contentType != ContentTypeFS {
+		return fmt.Errorf("Content type not supported")
+	}
+
+	if volSrcArgs.MigrationType.FSType != migration.MigrationFSType_RSYNC {
+		return fmt.Errorf("Migration type not supported")
+	}
+
+	return ErrNotImplemented
+}
+
+// CreateVolumeFromMigration creates a volume being sent via a migration.
+func (d *lvm) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, preFiller *VolumeFiller, op *operations.Operation) error {
+	if vol.contentType != ContentTypeFS {
+		return fmt.Errorf("Content type not supported")
+	}
+
+	if volTargetArgs.MigrationType.FSType != migration.MigrationFSType_RSYNC {
+		return fmt.Errorf("Migration type not supported")
+	}
+
+	return ErrNotImplemented
+}
+
+// CreateVolumeFromCopy provides same-pool volume copying functionality.
+func (d *lvm) CreateVolumeFromCopy(vol Volume, srcVol Volume, copySnapshots bool, op *operations.Operation) error {
+	var err error
+	var srcSnapshots []Volume
+
+	if copySnapshots && !srcVol.IsSnapshot() {
+		// Get the list of snapshots from the source.
+		srcSnapshots, err = srcVol.Snapshots(op)
+		if err != nil {
+			return err
+		}
+	}
+
+	return d.copyVolume(vol, srcVol, srcSnapshots, op)
+}
+
+// RefreshVolume provides same-pool volume and specific snapshots syncing functionality.
+func (d *lvm) RefreshVolume(vol Volume, srcVol Volume, srcSnapshots []Volume, op *operations.Operation) error {
+	return d.copyVolume(vol, srcVol, srcSnapshots, op)
+}
+
+// copyVolume copies a volume and its specific snapshots.
+func (d *lvm) copyVolume(vol Volume, srcVol Volume, srcSnapshots []Volume, op *operations.Operation) error {
+	if vol.contentType != ContentTypeFS || srcVol.contentType != ContentTypeFS {
+		return fmt.Errorf("Content type not supported")
+	}
+
+	return ErrNotImplemented
+}
+
+// VolumeSnapshots returns a list of snapshots for the volume.
+func (d *lvm) VolumeSnapshots(vol Volume, op *operations.Operation) ([]string, error) {
+	return nil, ErrNotImplemented
+}
+
+// UpdateVolume applies config changes to the volume.
+func (d *lvm) UpdateVolume(vol Volume, changedConfig map[string]string) error {
+	return ErrNotImplemented
+}
+
+// RenameVolume renames a volume and its snapshots.
+func (d *lvm) RenameVolume(vol Volume, newVolName string, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// RestoreVolume restores a volume from a snapshot.
+func (d *lvm) RestoreVolume(vol Volume, snapshotName string, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// DeleteVolume deletes a volume of the storage device. If any snapshots of the volume remain then
+// this function will return an error.
+func (d *lvm) DeleteVolume(vol Volume, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// MountVolume simulates mounting a volume. As dir driver doesn't have volumes to mount it returns
+// false indicating that there is no need to issue an unmount.
+func (d *lvm) MountVolume(vol Volume, op *operations.Operation) (bool, error) {
+	return false, ErrNotImplemented
+}
+
+// MountVolumeSnapshot sets up a read-only mount on top of the snapshot to avoid accidental modifications.
+func (d *lvm) MountVolumeSnapshot(snapVol Volume, op *operations.Operation) (bool, error) {
+	return false, ErrNotImplemented
+}
+
+// UnmountVolume simulates unmounting a volume. As dir driver doesn't have volumes to unmount it
+// returns false indicating the volume was already unmounted.
+func (d *lvm) UnmountVolume(vol Volume, op *operations.Operation) (bool, error) {
+	return false, ErrNotImplemented
+}
+
+// UnmountVolumeSnapshot removes the read-only mount placed on top of a snapshot.
+func (d *lvm) UnmountVolumeSnapshot(snapVol Volume, op *operations.Operation) (bool, error) {
+	return false, ErrNotImplemented
+}
+
+// SetVolumeQuota sets the quota on the volume.
+func (d *lvm) SetVolumeQuota(vol Volume, size string, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// CreateVolumeSnapshot creates a snapshot of a volume.
+func (d *lvm) CreateVolumeSnapshot(snapVol Volume, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// DeleteVolumeSnapshot removes a snapshot from the storage device. The volName and snapshotName
+// must be bare names and should not be in the format "volume/snapshot".
+func (d *lvm) DeleteVolumeSnapshot(snapVol Volume, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// RenameVolumeSnapshot renames a volume snapshot.
+func (d *lvm) RenameVolumeSnapshot(snapVol Volume, newSnapshotName string, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// BackupVolume copies a volume (and optionally its snapshots) to a specified target path.
+// This driver does not support optimized backups.
+func (d *lvm) BackupVolume(vol Volume, targetPath string, _, snapshots bool, op *operations.Operation) error {
+	return ErrNotImplemented
+}
+
+// CreateVolumeFromBackup restores a backup tarball onto the storage device.
+func (d *lvm) CreateVolumeFromBackup(vol Volume, snapshots []string, srcData io.ReadSeeker, optimizedStorage bool, op *operations.Operation) (func(vol Volume) error, func(), error) {
+	return nil, nil, ErrNotImplemented
+}


More information about the lxc-devel mailing list