The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6396
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) ===
From e474717c85fd0ad269d35596faf85c79686af081 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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 }
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel