[lxc-devel] [lxd/master] Storage Delete Instance and Delete Instance Snapshot

tomponline on Github lxc-bot at linuxcontainers.org
Mon Nov 4 20:08:21 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20191104/8dce56fc/attachment-0001.bin>
-------------- next part --------------
From e474717c85fd0ad269d35596faf85c79686af081 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 09:09:20 +0000
Subject: [PATCH 1/8] lxd/container/lxc: Links container Delete() to new
 storage package

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 199 ++++++++++++++++++++++++++++++-------------
 1 file changed, 140 insertions(+), 59 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 8dc40f7292..7cef710a01 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -3522,7 +3522,11 @@ func (c *containerLXC) Delete() error {
 		return err
 	}
 
-	// Check if we're dealing with "lxd import"
+	// Check if we're dealing with "lxd import".
+	// "lxd import" is used for disaster recovery, where you already have a container and
+	// snapshots on disk but no DB entry. As such if something has gone wrong during the
+	// creation of the instance and we are now being asked to delete the instance, we should
+	// not remove the storage volumes themselves as this would cause data loss.
 	isImport := false
 	if c.storage != nil {
 		_, poolName, _ := c.storage.GetContainerPoolInfo()
@@ -3539,93 +3543,170 @@ func (c *containerLXC) Delete() error {
 		}
 	}
 
-	// Attempt to initialize storage interface for the container.
-	err := c.initStorage()
+	// Get the storage pool name of the instance.
+	poolName, err := c.state.Cluster.ContainerPool(c.Project(), c.Name())
 	if err != nil {
-		logger.Warnf("Failed to init storage: %v", err)
+		return err
 	}
 
-	if c.IsSnapshot() {
-		// Remove the snapshot
-		if c.storage != nil && !isImport {
-			err := c.storage.ContainerSnapshotDelete(c)
+	// Check if we can load new storage layer for pool driver type.
+	pool, err := storagePools.GetPoolByName(c.state, poolName)
+	if err != storageDrivers.ErrUnknownDriver {
+		if err != nil {
+			return err
+		}
+
+		if c.IsSnapshot() {
+			if !isImport {
+				// Remove snapshot volume and database record.
+				err = pool.DeleteInstanceSnapshot(c, nil)
+				if err != nil {
+					return err
+				}
+			}
+		} else {
+			// Remove all snapshots by initialising each snapshot as an Instance and
+			// calling its Delete function.
+			err := containerDeleteSnapshots(c.state, c.Project(), c.Name())
+			if err != nil {
+				logger.Error("Failed to delete instance snapshots", log.Ctx{"project": c.Project(), "instance": c.Name(), "err": err})
+				return err
+			}
+
+			// Remove all backups.
+			backups, err := c.Backups()
+			if err != nil {
+				return err
+			}
+
+			for _, backup := range backups {
+				err = backup.Delete()
+				if err != nil {
+					return err
+				}
+			}
+
+			if !isImport {
+				// Remove the storage volume, snapshot volumes and database records.
+				err = pool.DeleteInstance(c, nil)
+				if err != nil {
+					return err
+				}
+			}
+
+			// Clean things up.
+			c.cleanup()
+
+			// Delete the MAAS entry.
+			err = c.maasDelete()
 			if err != nil {
-				logger.Warn("Failed to delete snapshot", log.Ctx{"name": c.Name(), "err": err})
+				logger.Error("Failed deleting instance MAAS record", log.Ctx{"project": c.Project(), "instance": c.Name(), "err": err})
 				return err
 			}
+
+			// Run device removal function for each device.
+			for k, m := range c.expandedDevices {
+				err = c.deviceRemove(k, m)
+				if err != nil && err != device.ErrUnsupportedDevType {
+					return errors.Wrapf(err, "Failed to remove device '%s'", k)
+				}
+			}
 		}
-	} else {
-		// Remove all snapshots
-		err := containerDeleteSnapshots(c.state, c.Project(), c.Name())
-		if err != nil {
-			logger.Warn("Failed to delete snapshots", log.Ctx{"name": c.Name(), "err": err})
+
+		// Remove the database record of the instance or snapshot instance.
+		if err := c.state.Cluster.ContainerRemove(c.Project(), c.Name()); err != nil {
+			logger.Error("Failed deleting instance entry", log.Ctx{"project": c.Project(), "instance": c.Name(), "err": err})
 			return err
 		}
-
-		// Remove all backups
-		backups, err := c.Backups()
+	} else {
+		// Attempt to initialize storage interface for the container.
+		err := c.initStorage()
 		if err != nil {
-			return err
+			logger.Warnf("Failed to init storage: %v", err)
 		}
 
-		for _, backup := range backups {
-			err = backup.Delete()
+		if c.IsSnapshot() {
+			// Remove the snapshot
+			if c.storage != nil && !isImport {
+				err := c.storage.ContainerSnapshotDelete(c)
+				if err != nil {
+					logger.Warn("Failed to delete snapshot", log.Ctx{"name": c.Name(), "err": err})
+					return err
+				}
+			}
+		} else {
+			// Remove all snapshots
+			err := containerDeleteSnapshots(c.state, c.Project(), c.Name())
 			if err != nil {
+				logger.Warn("Failed to delete snapshots", log.Ctx{"name": c.Name(), "err": err})
 				return err
 			}
-		}
 
-		// Clean things up
-		c.cleanup()
+			// Remove all backups
+			backups, err := c.Backups()
+			if err != nil {
+				return err
+			}
 
-		// Delete the container from disk
-		if c.storage != nil && !isImport {
-			_, poolName, _ := c.storage.GetContainerPoolInfo()
-			containerMountPoint := storagePools.GetContainerMountPoint(c.Project(), poolName, c.Name())
-			if shared.PathExists(c.Path()) ||
-				shared.PathExists(containerMountPoint) {
-				err := c.storage.ContainerDelete(c)
+			for _, backup := range backups {
+				err = backup.Delete()
 				if err != nil {
-					logger.Error("Failed deleting container storage", log.Ctx{"name": c.Name(), "err": err})
 					return err
 				}
 			}
-		}
 
-		// Delete the MAAS entry
-		err = c.maasDelete()
-		if err != nil {
-			logger.Error("Failed deleting container MAAS record", log.Ctx{"name": c.Name(), "err": err})
-			return err
-		}
+			// Clean things up
+			c.cleanup()
 
-		// Remove devices from container.
-		for k, m := range c.expandedDevices {
-			err = c.deviceRemove(k, m)
-			if err != nil && err != device.ErrUnsupportedDevType {
-				return errors.Wrapf(err, "Failed to remove device '%s'", k)
+			// Delete the container from disk
+			if c.storage != nil && !isImport {
+				_, poolName, _ := c.storage.GetContainerPoolInfo()
+				containerMountPoint := storagePools.GetContainerMountPoint(c.Project(), poolName, c.Name())
+				if shared.PathExists(c.Path()) ||
+					shared.PathExists(containerMountPoint) {
+					err := c.storage.ContainerDelete(c)
+					if err != nil {
+						logger.Error("Failed deleting container storage", log.Ctx{"name": c.Name(), "err": err})
+						return err
+					}
+				}
 			}
-		}
-	}
 
-	// Remove the database record
-	if err := c.state.Cluster.ContainerRemove(c.project, c.Name()); err != nil {
-		logger.Error("Failed deleting container entry", log.Ctx{"name": c.Name(), "err": err})
-		return err
-	}
+			// Delete the MAAS entry
+			err = c.maasDelete()
+			if err != nil {
+				logger.Error("Failed deleting container MAAS record", log.Ctx{"name": c.Name(), "err": err})
+				return err
+			}
 
-	// Remove the database entry for the pool device
-	if c.storage != nil {
-		// Get the name of the storage pool the container is attached to. This
-		// reverse-engineering works because container names are globally
-		// unique.
-		poolID, _, _ := c.storage.GetContainerPoolInfo()
+			// Remove devices from container.
+			for k, m := range c.expandedDevices {
+				err = c.deviceRemove(k, m)
+				if err != nil && err != device.ErrUnsupportedDevType {
+					return errors.Wrapf(err, "Failed to remove device '%s'", k)
+				}
+			}
+		}
 
-		// Remove volume from storage pool.
-		err := c.state.Cluster.StoragePoolVolumeDelete(c.Project(), c.Name(), storagePoolVolumeTypeContainer, poolID)
-		if err != nil {
+		// Remove the database record
+		if err := c.state.Cluster.ContainerRemove(c.project, c.Name()); err != nil {
+			logger.Error("Failed deleting container entry", log.Ctx{"name": c.Name(), "err": err})
 			return err
 		}
+
+		// Remove the database entry for the pool device
+		if c.storage != nil {
+			// Get the name of the storage pool the container is attached to. This
+			// reverse-engineering works because container names are globally
+			// unique.
+			poolID, _, _ := c.storage.GetContainerPoolInfo()
+
+			// Remove volume from storage pool.
+			err := c.state.Cluster.StoragePoolVolumeDelete(c.Project(), c.Name(), storagePoolVolumeTypeContainer, poolID)
+			if err != nil {
+				return err
+			}
+		}
 	}
 
 	logger.Info("Deleted container", ctxMap)

From 82f593c56aa18c8a251122e5ee013224a92fd4f9 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 09:09:47 +0000
Subject: [PATCH 2/8] lxd/container/lxc: Improves error logging in diskState

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

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 7cef710a01..3e193d5756 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5992,13 +5992,13 @@ func (c *containerLXC) diskState() map[string]api.InstanceStateDisk {
 		pool, err := storagePools.GetPoolByName(c.state, dev.Config["pool"])
 		if err != storageDrivers.ErrUnknownDriver {
 			if err != nil {
-				logger.Errorf("Error loading storage pool for %s: %v", c.Name(), err)
+				logger.Error("Error loading storage pool", log.Ctx{"project": c.Project(), "instance": c.Name(), "err": err})
 				continue
 			}
 
 			usage, err = pool.GetInstanceUsage(c)
 			if err != nil {
-				logger.Errorf("Error getting disk usage for %s: %v", c.Name(), err)
+				logger.Error("Error getting disk usage", log.Ctx{"project": c.Project(), "instance": c.Name(), "err": err})
 				continue
 			}
 		} else {

From e2dfe1cc77e2c7f6c9685c83173409f54e2b7332 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 09:37:04 +0000
Subject: [PATCH 3/8] lxd/storage/backend/lxd: Removes duplicated code from
 DeleteCustomVolume

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

diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
index 281497532f..3ff3928184 100644
--- a/lxd/storage/backend_lxd.go
+++ b/lxd/storage/backend_lxd.go
@@ -801,22 +801,9 @@ func (b *lxdBackend) DeleteCustomVolume(volName string, op *operations.Operation
 		return err
 	}
 
-	// Remove the database entry and volume from the storage device for each snapshot.
+	// Remove each snapshot.
 	for _, snapshot := range snapshots {
-		// Extract just the snapshot name from the snapshot.
-		_, snapName, _ := shared.ContainerGetParentAndSnapshotName(snapshot.Name)
-
-		// Delete the snapshot volume from the storage device.
-		// Must come before Cluster.StoragePoolVolumeDelete otherwise driver won't be able
-		// to get volume ID.
-		err = b.driver.DeleteVolumeSnapshot(drivers.VolumeTypeCustom, volName, snapName, op)
-		if err != nil {
-			return err
-		}
-
-		// Remove the snapshot volume record from the database.
-		// Must come after driver.DeleteVolume so that volume ID is still available.
-		err = b.state.Cluster.StoragePoolVolumeDelete("default", snapshot.Name, db.StoragePoolVolumeTypeCustom, b.ID())
+		err = b.DeleteCustomVolumeSnapshot(snapshot.Name, op)
 		if err != nil {
 			return err
 		}
@@ -962,12 +949,13 @@ func (b *lxdBackend) DeleteCustomVolumeSnapshot(volName string, op *operations.O
 	}
 
 	// Delete the snapshot from the storage device.
+	// Must come before DB StoragePoolVolumeDelete so that the volume ID is still available.
 	err := b.driver.DeleteVolumeSnapshot(drivers.VolumeTypeCustom, parentName, snapName, op)
 	if err != nil {
 		return err
 	}
 
-	// Finally, remove the volume record from the database.
+	// Remove the snapshot volume record from the database.
 	err = b.state.Cluster.StoragePoolVolumeDelete("default", volName, db.StoragePoolVolumeTypeCustom, b.ID())
 	if err != nil {
 		return err

From 44a3878704664e3f17d44b8283087d325ebe15d6 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 20:04:46 +0000
Subject: [PATCH 4/8] lxd/storage/backend/lxd: Adds symlink management
 functions

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

diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
index 3ff3928184..005a78b4ae 100644
--- a/lxd/storage/backend_lxd.go
+++ b/lxd/storage/backend_lxd.go
@@ -186,6 +186,46 @@ func (b *lxdBackend) createInstanceSymlink(inst Instance, mountPath string) erro
 	return nil
 }
 
+// removeInstanceSymlink removes a symlink in the instance directory to the instance's mount path.
+func (b *lxdBackend) removeInstanceSymlink(inst Instance) error {
+	symlinkPath := inst.Path()
+	if shared.PathExists(symlinkPath) {
+		err := os.Remove(symlinkPath)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// createInstanceSnapshotSymlink creates a symlink in the snapshot directory to the instance's
+// snapshot path.
+func (b *lxdBackend) createInstanceSnapshotSymlink(inst Instance, mountPath string) error {
+	// Check we can convert the instance to the volume types needed.
+	volType, err := InstanceTypeToVolumeType(inst.Type())
+	if err != nil {
+		return err
+	}
+
+	snapshotMntPointSymlink := shared.VarPath("snapshots", project.Prefix(inst.Project(), inst.Name()))
+	volStorageName := project.Prefix(inst.Project(), inst.Name())
+
+	snapshotTargetPath, err := drivers.GetVolumeSnapshotDir(b.name, volType, volStorageName)
+	if err != nil {
+		return err
+	}
+
+	if !shared.PathExists(snapshotMntPointSymlink) {
+		err := os.Symlink(snapshotTargetPath, snapshotMntPointSymlink)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // imageFiller returns a function that can be used as a filler function with CreateVolume(). This
 // function will unpack the specified image archive into the specified mount path of the volume.
 func (b *lxdBackend) imageFiller(fingerprint string, op *operations.Operation) func(mountPath string) error {

From 82a4d3d77f1b6c92a174163277c5d3ecab260b56 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 20:05:07 +0000
Subject: [PATCH 5/8] lxd/storage/backend/lxd: Adds Instance and Instance
 Snapshot delete functions

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

diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
index 005a78b4ae..9c1f4651e5 100644
--- a/lxd/storage/backend_lxd.go
+++ b/lxd/storage/backend_lxd.go
@@ -312,12 +312,62 @@ func (b *lxdBackend) RenameInstance(inst Instance, newName string, op *operation
 	return ErrNotImplemented
 }
 
+// DeleteInstance removes the Instance's root volume (all snapshots need to be removed first).
 func (b *lxdBackend) DeleteInstance(inst Instance, op *operations.Operation) error {
 	logger := logging.AddContext(b.logger, log.Ctx{"project": inst.Project(), "instance": inst.Name()})
 	logger.Debug("DeleteInstance started")
 	defer logger.Debug("DeleteInstance finished")
 
-	return ErrNotImplemented
+	if inst.IsSnapshot() {
+		return fmt.Errorf("Instance must not be a snapshot")
+	}
+
+	// Check we can convert the instance to the volume types needed.
+	volType, err := InstanceTypeToVolumeType(inst.Type())
+	if err != nil {
+		return err
+	}
+
+	volDBType, err := VolumeTypeToDBType(volType)
+	if err != nil {
+		return err
+	}
+
+	// Get any snapshots the instance has in the format <instance name>/<snapshot name>.
+	snapshots, err := b.state.Cluster.ContainerGetSnapshots(inst.Project(), inst.Name())
+	if err != nil {
+		return err
+	}
+
+	// Check all snapshots are already removed.
+	if len(snapshots) > 0 {
+		return fmt.Errorf("Cannot remove an instance volume that has snapshots")
+	}
+
+	// Get the volume name on storage.
+	volStorageName := project.Prefix(inst.Project(), inst.Name())
+
+	// Delete the volume from the storage device. Must come after snapshots are removed.
+	// Must come before DB StoragePoolVolumeDelete so that the volume ID is still available.
+	logger.Debug("Deleting instance volume", log.Ctx{"volName": volStorageName})
+	err = b.driver.DeleteVolume(volType, volStorageName, op)
+	if err != nil {
+		return err
+	}
+
+	// Remove symlink.
+	err = b.removeInstanceSymlink(inst)
+	if err != nil {
+		return err
+	}
+
+	// Remove the volume record from the database.
+	err = b.state.Cluster.StoragePoolVolumeDelete(inst.Project(), inst.Name(), volDBType, b.ID())
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 func (b *lxdBackend) MigrateInstance(inst Instance, snapshots bool, args migration.SourceArgs) (migration.StorageSourceDriver, error) {
@@ -369,8 +419,46 @@ func (b *lxdBackend) RenameInstanceSnapshot(inst Instance, newName string, op *o
 	return ErrNotImplemented
 }
 
+// DeleteInstanceSnapshot removes the snapshot volume for the supplied snapshot instance.
 func (b *lxdBackend) DeleteInstanceSnapshot(inst Instance, op *operations.Operation) error {
-	return ErrNotImplemented
+	logger := logging.AddContext(b.logger, log.Ctx{"project": inst.Project(), "instance": inst.Name()})
+	logger.Debug("DeleteInstanceSnapshot started")
+	defer logger.Debug("DeleteInstanceSnapshot finished")
+
+	parentName, snapName, isSnap := shared.ContainerGetParentAndSnapshotName(inst.Name())
+	if !inst.IsSnapshot() || !isSnap {
+		return fmt.Errorf("Instance must be a snapshot")
+	}
+
+	// Check we can convert the instance to the volume types needed.
+	volType, err := InstanceTypeToVolumeType(inst.Type())
+	if err != nil {
+		return err
+	}
+
+	volDBType, err := VolumeTypeToDBType(volType)
+	if err != nil {
+		return err
+	}
+
+	// Get the parent volume name on storage.
+	parentStorageName := project.Prefix(inst.Project(), parentName)
+
+	// Delete the snapshot from the storage device.
+	// Must come before DB StoragePoolVolumeDelete so that the volume ID is still available.
+	logger.Debug("Deleting instance snapshot volume", log.Ctx{"volName": parentStorageName, "snapshotName": snapName})
+	err = b.driver.DeleteVolumeSnapshot(volType, parentStorageName, snapName, op)
+	if err != nil {
+		return err
+	}
+
+	// Remove the snapshot volume record from the database.
+	err = b.state.Cluster.StoragePoolVolumeDelete(inst.Project(), drivers.GetSnapshotVolumeName(parentName, snapName), volDBType, b.ID())
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 func (b *lxdBackend) RestoreInstanceSnapshot(inst Instance, op *operations.Operation) error {

From e6787daf016b12f517d9ce9896b170eeb6e5c7ca Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 20:06:05 +0000
Subject: [PATCH 6/8] lxd/storage/drivers/driver/dir: Reinstates
 DeleteParentSnapshotDirIfEmpty for volume and snapshot deletion

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

diff --git a/lxd/storage/drivers/driver_dir.go b/lxd/storage/drivers/driver_dir.go
index 61ed55a992..7336d4fb18 100644
--- a/lxd/storage/drivers/driver_dir.go
+++ b/lxd/storage/drivers/driver_dir.go
@@ -644,12 +644,7 @@ func (d *dir) DeleteVolume(volType VolumeType, volName string, op *operations.Op
 
 	// Although the volume snapshot directory should already be removed, lets remove it here
 	// to just in case the top-level directory is left.
-	snapshotDir, err := GetVolumeSnapshotDir(d.name, volType, volName)
-	if err != nil {
-		return err
-	}
-
-	err = os.RemoveAll(snapshotDir)
+	err = DeleteParentSnapshotDirIfEmpty(d.name, volType, volName)
 	if err != nil {
 		return err
 	}
@@ -830,6 +825,12 @@ func (d *dir) DeleteVolumeSnapshot(volType VolumeType, volName string, snapshotN
 		return err
 	}
 
+	// Remove the parent snapshot directory if this is the last snapshot being removed.
+	err = DeleteParentSnapshotDirIfEmpty(d.name, volType, volName)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 

From f5a73736968a6f391e053dff512549d204747342 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 20:06:34 +0000
Subject: [PATCH 7/8] lxd/storage/drivers/utils: Updates
 DeleteParentSnapshotDirIfEmpty to also remove symlink

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

diff --git a/lxd/storage/drivers/utils.go b/lxd/storage/drivers/utils.go
index c0bbdafe82..d205498e64 100644
--- a/lxd/storage/drivers/utils.go
+++ b/lxd/storage/drivers/utils.go
@@ -5,7 +5,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 
 	"golang.org/x/sys/unix"
@@ -175,27 +174,37 @@ func GetSnapshotVolumeName(parentName, snapshotName string) string {
 }
 
 // DeleteParentSnapshotDirIfEmpty removes the parent snapshot directory if it is empty.
-// It accepts the volume name of a snapshot in the form "volume/snap" and the volume path of the
-// snapshot. It will then remove the snapshots directory above "/snap" if it is empty.
-func DeleteParentSnapshotDirIfEmpty(volName string, volPath string) error {
-	_, snapName, isSnap := shared.ContainerGetParentAndSnapshotName(volName)
-	if !isSnap {
-		return fmt.Errorf("Volume is not a snapshot")
-	}
-
-	// Extract just the snapshot name from the volume name and then remove that suffix
-	// from the volume path. This will get us the parent snapshots directory we need.
-	snapshotsPath := strings.TrimSuffix(volPath, snapName)
-	isEmpty, err := shared.PathIsEmpty(snapshotsPath)
+// It accepts the pool name, volume type and parent volume name.
+func DeleteParentSnapshotDirIfEmpty(poolName string, volType VolumeType, volName string) error {
+	snapshotsPath, err := GetVolumeSnapshotDir(poolName, volType, volName)
 	if err != nil {
 		return err
 	}
 
-	if isEmpty {
-		err := os.Remove(snapshotsPath)
+	// If it exists, try to delete it.
+	if shared.PathExists(snapshotsPath) {
+		isEmpty, err := shared.PathIsEmpty(snapshotsPath)
 		if err != nil {
 			return err
 		}
+
+		if isEmpty {
+			err := os.Remove(snapshotsPath)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	// If it no longer exists (may have just removed it), remove symlink.
+	if !shared.PathExists(snapshotsPath) {
+		snapshotSymlink := shared.VarPath("snapshots", volName)
+		if shared.PathExists(snapshotSymlink) {
+			err := os.Remove(snapshotSymlink)
+			if err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil

From 10b7061332f6d10b9ff1f05481acbd9cd7a30f7c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 4 Nov 2019 20:06:54 +0000
Subject: [PATCH 8/8] lxd/storage/interfaces: Adds IsSnapshot to Instance
 interface

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

diff --git a/lxd/storage/interfaces.go b/lxd/storage/interfaces.go
index 592b28b14f..4e787f07bb 100644
--- a/lxd/storage/interfaces.go
+++ b/lxd/storage/interfaces.go
@@ -19,6 +19,7 @@ type Instance interface {
 	Path() string
 
 	IsRunning() bool
+	IsSnapshot() bool
 	TemplateApply(trigger string) error
 }
 


More information about the lxc-devel mailing list