package drivers

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"math"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"golang.org/x/sys/unix"

	"github.com/canonical/lxd/lxd/backup"
	"github.com/canonical/lxd/lxd/instancewriter"
	"github.com/canonical/lxd/lxd/migration"
	"github.com/canonical/lxd/lxd/operations"
	"github.com/canonical/lxd/lxd/rsync"
	"github.com/canonical/lxd/lxd/storage/block"
	"github.com/canonical/lxd/lxd/storage/filesystem"
	"github.com/canonical/lxd/shared"
	"github.com/canonical/lxd/shared/api"
	"github.com/canonical/lxd/shared/logger"
	"github.com/canonical/lxd/shared/revert"
	"github.com/canonical/lxd/shared/validate"
)

// 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 {
	revert := revert.New()
	defer revert.Fail()

	volPath := vol.MountPath()
	err := vol.EnsureMountPath()
	if err != nil {
		return err
	}

	revert.Add(func() { _ = os.RemoveAll(volPath) })

	err = d.createLogicalVolume(d.config["lvm.vg_name"], d.thinpoolName(), vol, d.usesThinpool())
	if err != nil {
		return fmt.Errorf("Error creating LVM logical volume: %w", err)
	}

	revert.Add(func() { _ = d.DeleteVolume(vol, op) })

	// For VMs, also create the filesystem volume.
	if vol.IsVMBlock() {
		fsVol := vol.NewVMBlockFilesystemVolume()
		err := d.CreateVolume(fsVol, nil, op)
		if err != nil {
			return err
		}

		revert.Add(func() { _ = d.DeleteVolume(fsVol, op) })
	}

	err = vol.MountTask(func(mountPath string, op *operations.Operation) error {
		// Run the volume filler function if supplied.
		if filler != nil && filler.Fill != nil {
			var err error
			var devPath string

			if IsContentBlock(vol.contentType) {
				// Get the device path.
				devPath, err = d.GetVolumeDiskPath(vol)
				if err != nil {
					return err
				}
			}

			allowUnsafeResize := false
			if vol.volType == VolumeTypeImage || !d.usesThinpool() {
				// Allow filler to resize initial image and non-thin volumes as needed.
				// Some storage drivers don't normally allow image volumes to be resized due to
				// them having read-only snapshots that cannot be resized. However when creating
				// the initial volume and filling it unsafe resizing can be allowed and is required
				// in order to support unpacking images larger than the default volume size.
				// The filler function is still expected to obey any volume size restrictions
				// configured on the pool.
				// Unsafe resize is also needed to disable filesystem resize safety checks.
				// This is safe because if for some reason an error occurs the volume will be
				// discarded rather than leaving a corrupt filesystem.
				allowUnsafeResize = true
			}

			// Run the filler.
			err = d.runFiller(vol, devPath, filler, allowUnsafeResize)
			if err != nil {
				return err
			}

			// Move the GPT alt header to end of disk if needed.
			if vol.IsVMBlock() {
				err = d.moveGPTAltHeader(devPath)
				if err != nil {
					return err
				}
			}
		}

		if vol.contentType == ContentTypeFS {
			// Run EnsureMountPath again after mounting and filling to ensure the mount directory has
			// the correct permissions set.
			err = vol.EnsureMountPath()
			if err != nil {
				return err
			}
		}

		return nil
	}, op)
	if err != nil {
		return err
	}

	revert.Success()
	return nil
}

// CreateVolumeFromBackup restores a backup tarball onto the storage device.
func (d *lvm) CreateVolumeFromBackup(vol VolumeCopy, srcBackup backup.Info, srcData io.ReadSeeker, op *operations.Operation) (VolumePostHook, revert.Hook, error) {
	return genericVFSBackupUnpack(d, d.state, vol, srcBackup.Snapshots, srcData, op)
}

// CreateVolumeFromCopy provides same-pool volume copying functionality.
func (d *lvm) CreateVolumeFromCopy(vol VolumeCopy, srcVol VolumeCopy, allowInconsistent bool, op *operations.Operation) error {
	var err error
	var srcSnapshots []string

	if len(vol.Snapshots) > 0 && !srcVol.IsSnapshot() {
		// Get the list of snapshots from the source.
		allSrcSnapshots, err := srcVol.Volume.Snapshots(op)
		if err != nil {
			return err
		}

		for _, srcSnapshot := range allSrcSnapshots {
			_, snapshotName, _ := api.GetParentAndSnapshotName(srcSnapshot.name)
			srcSnapshots = append(srcSnapshots, snapshotName)
		}
	}

	// We can use optimised copying when the pool is backed by an LVM thinpool.
	if d.usesThinpool() {
		err = d.copyThinpoolVolume(vol.Volume, srcVol.Volume, srcSnapshots, false)
		if err != nil {
			return err
		}

		// For VMs, also copy the filesystem volume.
		if vol.IsVMBlock() {
			srcFSVol := srcVol.NewVMBlockFilesystemVolume()
			fsVol := vol.NewVMBlockFilesystemVolume()
			return d.copyThinpoolVolume(fsVol, srcFSVol, srcSnapshots, false)
		}

		return nil
	}

	// Otherwise run the generic copy.
	_, err = genericVFSCopyVolume(d, nil, vol, srcVol, srcSnapshots, false, allowInconsistent, op)
	return err
}

// CreateVolumeFromMigration creates a volume being sent via a migration.
func (d *lvm) CreateVolumeFromMigration(vol VolumeCopy, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, preFiller *VolumeFiller, op *operations.Operation) error {
	_, err := genericVFSCreateVolumeFromMigration(d, nil, vol, conn, volTargetArgs, preFiller, op)
	return err
}

// RefreshVolume provides same-pool volume and specific snapshots syncing functionality.
func (d *lvm) RefreshVolume(vol VolumeCopy, srcVol VolumeCopy, refreshSnapshots []string, allowInconsistent bool, op *operations.Operation) error {
	// We can use optimised copying when the pool is backed by an LVM thinpool.
	if d.usesThinpool() {
		return d.copyThinpoolVolume(vol.Volume, srcVol.Volume, refreshSnapshots, true)
	}

	// Otherwise run the generic copy.
	_, err := genericVFSCopyVolume(d, nil, vol, srcVol, refreshSnapshots, true, allowInconsistent, op)
	return err
}

// 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 {
	snapshots, err := d.VolumeSnapshots(vol, op)
	if err != nil {
		return err
	}

	if len(snapshots) > 0 {
		return errors.New("Cannot remove a volume that has snapshots")
	}

	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
	lvExists, err := d.logicalVolumeExists(volDevPath)
	if err != nil {
		return err
	}

	if lvExists {
		// Only call UnmountVolume if mounted to avoid breaking deactivaion ref counts.
		if vol.contentType == ContentTypeFS && filesystem.IsMountPoint(vol.MountPath()) {
			_, err = d.UnmountVolume(vol, false, op)
			if err != nil {
				return fmt.Errorf("Error unmounting LVM logical volume: %w", err)
			}
		}

		err = d.removeLogicalVolume(d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name))
		if err != nil {
			return fmt.Errorf("Error removing LVM logical volume: %w", err)
		}
	}

	if vol.contentType == ContentTypeFS {
		// Remove the volume from the storage device.
		mountPath := vol.MountPath()
		err = os.RemoveAll(mountPath)
		if err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("Error removing LVM logical volume mount path %q: %w", mountPath, err)
		}

		// Although the volume snapshot directory should already be removed, lets remove it here to just in
		// case the top-level directory is left.
		err = deleteParentSnapshotDirIfEmpty(d.name, vol.volType, vol.name)
		if err != nil {
			return err
		}
	}

	// For VMs, also delete the filesystem volume.
	if vol.IsVMBlock() {
		fsVol := vol.NewVMBlockFilesystemVolume()
		err := d.DeleteVolume(fsVol, op)
		if err != nil {
			return err
		}
	}

	return nil
}

// HasVolume indicates whether a specific volume exists on the storage pool.
func (d *lvm) HasVolume(vol Volume) (bool, error) {
	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
	return d.logicalVolumeExists(volDevPath)
}

// FillVolumeConfig populate volume with default config.
func (d *lvm) FillVolumeConfig(vol Volume) error {
	// Copy volume.* configuration options from pool.
	// Exclude "block.filesystem" and "block.mount_options" as they depend on volume type (handled below).
	// Exclude "lvm.stripes", "lvm.stripes.size" as they only work on non-thin storage pools (handled below).
	err := d.fillVolumeConfig(&vol, "block.filesystem", "block.mount_options", "lvm.stripes", "lvm.stripes.size")
	if err != nil {
		return err
	}

	// Only validate filesystem config keys for filesystem volumes or VM block volumes (which have an
	// associated filesystem volume).
	if vol.ContentType() == ContentTypeFS || vol.IsVMBlock() {
		// VM volumes will always use the default filesystem.
		if vol.IsVMBlock() {
			vol.config["block.filesystem"] = DefaultFilesystem
		} else {
			// Inherit filesystem from pool if not set.
			if vol.config["block.filesystem"] == "" {
				vol.config["block.filesystem"] = d.config["volume.block.filesystem"]
			}

			// Default filesystem if neither volume nor pool specify an override.
			if vol.config["block.filesystem"] == "" {
				// Unchangeable volume property: Set unconditionally.
				vol.config["block.filesystem"] = DefaultFilesystem
			}
		}

		// Inherit filesystem mount options from pool if not set.
		if vol.config["block.mount_options"] == "" {
			vol.config["block.mount_options"] = d.config["volume.block.mount_options"]
		}

		// Default filesystem mount options if neither volume nor pool specify an override.
		if vol.config["block.mount_options"] == "" {
			// Unchangeable volume property: Set unconditionally.
			vol.config["block.mount_options"] = "discard"
		}
	}

	// Inherit stripe settings from pool if not set and not using thin pool.
	if !d.usesThinpool() {
		if vol.config["lvm.stripes"] == "" {
			vol.config["lvm.stripes"] = d.config["volume.lvm.stripes"]
		}

		if vol.config["lvm.stripes.size"] == "" {
			vol.config["lvm.stripes.size"] = d.config["lvm.stripes.size"]
		}
	}

	return nil
}

// commonVolumeRules returns validation rules which are common for pool and volume.
func (d *lvm) commonVolumeRules() map[string]func(value string) error {
	return map[string]func(value string) error{
		"block.mount_options": validate.IsAny,
		"block.filesystem":    validate.Optional(validate.IsOneOf(blockBackedAllowedFilesystems...)),
		// lxdmeta:generate(entities=storage-lvm; group=volume-conf; key=lvm.stripes)
		//
		// ---
		//  type: string
		//  defaultdesc: same as `volume.lvm.stripes`
		//  shortdesc: Number of stripes to use for new volumes (or thin pool volume)
		//  scope: global
		"lvm.stripes": validate.Optional(validate.IsUint32),
		// lxdmeta:generate(entities=storage-lvm; group=volume-conf; key=lvm.stripes.size)
		// The size must be at least 4096 bytes, and a multiple of 512 bytes.
		// ---
		//  type: string
		//  defaultdesc: same as `volume.lvm.stripes.size`
		//  shortdesc: Size of stripes to use
		//  scope: global
		"lvm.stripes.size": validate.Optional(validate.IsSize),
	}
}

// ValidateVolume validates the supplied volume config.
func (d *lvm) ValidateVolume(vol Volume, removeUnknownKeys bool) error {
	commonRules := d.commonVolumeRules()

	// Disallow block.* settings for regular custom block volumes. These settings only make sense
	// when using custom filesystem volumes. LXD will create the filesystem
	// for these volumes, and use the mount options. When attaching a regular block volume to a VM,
	// these are not mounted by LXD and therefore don't need these config keys.
	if vol.volType == VolumeTypeCustom && vol.contentType == ContentTypeBlock {
		delete(commonRules, "block.filesystem")
		delete(commonRules, "block.mount_options")
	}

	err := d.validateVolume(vol, commonRules, removeUnknownKeys)
	if err != nil {
		return err
	}

	if d.usesThinpool() && vol.config["lvm.stripes"] != "" {
		return errors.New("lvm.stripes cannot be used with thin pool volumes")
	}

	if d.usesThinpool() && vol.config["lvm.stripes.size"] != "" {
		return errors.New("lvm.stripes.size cannot be used with thin pool volumes")
	}

	return nil
}

// UpdateVolume applies config changes to the volume.
func (d *lvm) UpdateVolume(vol Volume, changedConfig map[string]string) error {
	newSize, sizeChanged := changedConfig["size"]
	if sizeChanged {
		err := d.SetVolumeQuota(vol, newSize, false, nil)
		if err != nil {
			return err
		}
	}

	_, changed := changedConfig["lvm.stripes"]
	if changed {
		return errors.New("lvm.stripes cannot be changed")
	}

	_, changed = changedConfig["lvm.stripes.size"]
	if changed {
		return errors.New("lvm.stripes.size cannot be changed")
	}

	return nil
}

// GetVolumeUsage returns the disk space used by the volume (this is not currently supported).
func (d *lvm) GetVolumeUsage(vol Volume) (int64, error) {
	// Snapshot usage not supported for LVM.
	if vol.IsSnapshot() {
		return -1, ErrNotSupported
	}

	// For non-snapshot filesystem volumes, we only return usage when the volume is mounted.
	// This is because to get an accurate value we cannot use blocks allocated, as the filesystem will likely
	// consume blocks and not free them when files are deleted in the volume. This avoids returning different
	// values depending on whether the volume is mounted or not.
	if vol.contentType == ContentTypeFS && filesystem.IsMountPoint(vol.MountPath()) {
		var stat unix.Statfs_t
		err := unix.Statfs(vol.MountPath(), &stat)
		if err != nil {
			return -1, err
		}

		return int64(stat.Blocks-stat.Bfree) * int64(stat.Bsize), nil
	} else if vol.contentType == ContentTypeBlock && d.usesThinpool() {
		// For non-snapshot thin pool block volumes we can calculate an approximate usage using the space
		// allocated to the volume from the thin pool.
		volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
		_, usedSize, err := d.thinPoolVolumeUsage(volDevPath)
		if err != nil {
			return -1, err
		}

		return int64(usedSize), nil
	}

	return -1, ErrNotSupported
}

// SetVolumeQuota applies a size limit on volume.
// Does nothing if supplied with an empty/zero size.
func (d *lvm) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, op *operations.Operation) error {
	// Do nothing if size isn't specified.
	if size == "" || size == "0" {
		return nil
	}

	sizeBytes, err := d.roundedSizeBytesString(size)
	if err != nil {
		return err
	}

	// Read actual size of current volume.
	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
	oldSizeBytes, err := d.logicalVolumeSize(volDevPath)
	if err != nil {
		return err
	}

	// Get the volume group's physical extent size, as we use this to figure out if the new and old sizes are
	// going to change beyond 1 extent size, otherwise there is no point in trying to resize as LVM do it.
	vgExtentSize, err := d.volumeGroupExtentSize(d.config["lvm.vg_name"])
	if err != nil {
		return err
	}

	// Round up the number of extents required for new quota size, as this is what the lvresize tool will do.
	newNumExtents := math.Ceil(float64(sizeBytes) / float64(vgExtentSize))
	oldNumExtents := math.Ceil(float64(oldSizeBytes) / float64(vgExtentSize))
	extentDiff := int(newNumExtents - oldNumExtents)

	// If old and new extents required are the same, nothing to do, as LVM won't resize them.
	if extentDiff == 0 {
		return nil
	}

	l := d.logger.AddContext(logger.Ctx{"dev": volDevPath, "size": strconv.FormatInt(sizeBytes, 10) + "b"})

	inUse := vol.MountInUse()

	// Resize filesystem if needed.
	if vol.contentType == ContentTypeFS {
		fsType := vol.ConfigBlockFilesystem()

		if sizeBytes < oldSizeBytes {
			if !filesystemTypeCanBeShrunk(fsType) {
				return fmt.Errorf("Filesystem %q cannot be shrunk: %w", fsType, ErrCannotBeShrunk)
			}

			if inUse {
				return ErrInUse // We don't allow online shrinking of filesytem volumes.
			}

			// Activate volume if needed.
			activated, err := d.activateVolume(vol)
			if err != nil {
				return err
			}

			if !activated {
				defer func() {
					_, _ = d.activateVolume(vol)
				}()
			}

			// Shrink filesystem first.
			// Pass allowUnsafeResize to allow disabling of filesystem resize safety checks.
			// We do this as a separate step rather than passing -r to lvresize in resizeLogicalVolume
			// so that we can have more control over when we trigger unsafe filesystem resize mode,
			// otherwise by passing -f to lvresize (required for other reasons) this would then pass
			// -f onto resize2fs as well.
			err = shrinkFileSystem(fsType, volDevPath, vol, sizeBytes, allowUnsafeResize)
			if err != nil {
				_, _ = d.deactivateVolume(vol)
				return err
			}

			// Deactivate the volume for resizing.
			_, err = d.deactivateVolume(vol)
			if err != nil {
				return err
			}

			l.Debug("Logical volume filesystem shrunk")

			// Shrink the block device.
			err = d.resizeLogicalVolume(volDevPath, sizeBytes)
			if err != nil {
				return err
			}
		} else if sizeBytes > oldSizeBytes {
			// Grow block device first.
			err = d.resizeLogicalVolume(volDevPath, sizeBytes)
			if err != nil {
				return err
			}

			// Activate the volume for resizing.
			activated, err := d.activateVolume(vol)
			if err != nil {
				return err
			}

			if activated {
				defer func() {
					_, _ = d.deactivateVolume(vol)
				}()
			}

			// Grow the filesystem to fill block device.
			err = growFileSystem(fsType, volDevPath, vol)
			if err != nil {
				return err
			}

			l.Debug("Logical volume filesystem grown")
		}
	} else {
		// Only perform pre-resize checks if we are not in "unsafe" mode.
		// In unsafe mode we expect the caller to know what they are doing and understand the risks.
		if !allowUnsafeResize {
			if sizeBytes < oldSizeBytes {
				return fmt.Errorf("Block volumes cannot be shrunk: %w", ErrCannotBeShrunk)
			}

			if inUse {
				return ErrInUse // We don't allow online resizing of block volumes.
			}
		}

		err = d.resizeLogicalVolume(volDevPath, sizeBytes)
		if err != nil {
			return err
		}

		// The new blocks in a grown volume will need clearing if using a thick pool.
		needsClearing := !d.usesThinpool() && (oldSizeBytes < sizeBytes)

		// VM block volumes need the GPT header moved on normal resize scenarios.
		needsGPTHeaderMove := vol.IsVMBlock() && !allowUnsafeResize

		// Need to activate the volume to clear it or to move the GPT header.
		needsActivating := needsClearing || needsGPTHeaderMove

		if needsActivating {
			// Activate the volume for clearing blocks and/or moving GPT header.
			activated, err := d.activateVolume(vol)
			if err != nil {
				return err
			}

			if activated {
				defer func() {
					_, _ = d.deactivateVolume(vol)
				}()
			}
		}

		// On thick pools, discard the blocks in the additional space when the volume is grown.
		if needsClearing {
			// Discard blocks from the end of the old volume's size.
			err := block.ClearBlock(volDevPath, oldSizeBytes)
			if err != nil {
				return err
			}
		}

		// Move the VM GPT alt header to end of disk if needed (not needed in unsafe resize mode as it is
		// expected the caller will do all necessary post resize actions themselves).
		// Do this after the new blocks have been cleared.
		if needsGPTHeaderMove {
			err = d.moveGPTAltHeader(volDevPath)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// GetVolumeDiskPath returns the location of a disk volume.
func (d *lvm) GetVolumeDiskPath(vol Volume) (string, error) {
	if vol.IsVMBlock() || (vol.volType == VolumeTypeCustom && IsContentBlock(vol.contentType)) {
		volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
		return volDevPath, nil
	}

	return "", ErrNotSupported
}

// ListVolumes returns a list of LXD volumes in storage pool.
func (d *lvm) ListVolumes() ([]Volume, error) {
	vols := make(map[string]Volume)

	cmd := exec.Command("lvs", "--noheadings", "-o", "lv_name", d.config["lvm.vg_name"])
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	err = cmd.Start()
	if err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		rawName := strings.TrimSpace(scanner.Text())
		var volType VolumeType
		var volName string

		for _, volumeType := range d.Info().VolumeTypes {
			prefix := string(volumeType) + "_"
			if strings.HasPrefix(rawName, prefix) {
				volType = volumeType
				volName = strings.TrimPrefix(rawName, prefix)
			}
		}

		if volType == "" {
			d.logger.Debug("Ignoring unrecognised volume type", logger.Ctx{"name": rawName})
			continue // Ignore unrecognised volume.
		}

		lvSnapSepCount := strings.Count(volName, lvmSnapshotSeparator)
		if lvSnapSepCount%2 != 0 {
			// If snapshot separator count is odd, then this means we have a lone lvmSnapshotSeparator
			// that is not part of the lvmEscapedHyphen pair, which means this volume is a snapshot.
			d.logger.Debug("Ignoring snapshot volume", logger.Ctx{"name": rawName})
			continue // Ignore snapshot volumes.
		}

		isBlock := strings.HasSuffix(volName, lvmBlockVolSuffix)

		if volType == VolumeTypeVM && !isBlock {
			continue // Ignore VM filesystem volumes as we will just return the VM's block volume.
		}

		// Unescape raw LVM name to LXD storage volume name. Safe to do now we know we are not dealing
		// with snapshot volumes.
		volName = strings.ReplaceAll(volName, lvmEscapedHyphen, "-")

		contentType := ContentTypeFS
		if volType == VolumeTypeCustom && strings.HasSuffix(volName, lvmISOVolSuffix) {
			contentType = ContentTypeISO
			volName = strings.TrimSuffix(volName, lvmISOVolSuffix)
		} else if volType == VolumeTypeVM || isBlock {
			contentType = ContentTypeBlock
			volName = strings.TrimSuffix(volName, lvmBlockVolSuffix)
		}

		// If a new volume has been found, or the volume will replace an existing image filesystem volume
		// then proceed to add the volume to the map. We allow image volumes to overwrite existing
		// filesystem volumes of the same name so that for VM images we only return the block content type
		// volume (so that only the single "logical" volume is returned).
		existingVol, foundExisting := vols[volName]
		if !foundExisting || (existingVol.Type() == VolumeTypeImage && existingVol.ContentType() == ContentTypeFS) {
			v := NewVolume(d, d.name, volType, contentType, volName, make(map[string]string), d.config)

			if contentType == ContentTypeFS {
				v.SetMountFilesystemProbe(true)
			}

			vols[volName] = v
			continue
		}

		return nil, fmt.Errorf("Unexpected duplicate volume %q found", volName)
	}

	errMsg, err := io.ReadAll(stderr)
	if err != nil {
		return nil, err
	}

	err = cmd.Wait()
	if err != nil {
		return nil, fmt.Errorf("Failed getting volume list: %v: %w", strings.TrimSpace(string(errMsg)), err)
	}

	volList := make([]Volume, 0, len(vols))
	for _, v := range vols {
		volList = append(volList, v)
	}

	return volList, nil
}

// mountCommon includes the logic for mounting either a volume or a volume snapshot as they follow very similar procedures.
func (d *lvm) mountCommon(vol Volume, op *operations.Operation) error {
	unlock, err := vol.MountLock()
	if err != nil {
		return err
	}

	defer unlock()

	revert := revert.New()
	defer revert.Fail()

	mountPath := vol.MountPath()

	var activated bool

	// Check if already mounted.
	if vol.contentType == ContentTypeFS && !filesystem.IsMountPoint(mountPath) {
		err = vol.EnsureMountPath()
		if err != nil {
			return err
		}

		// Default to mounting the original snapshot directly. This may be changed below if a temporary
		// snapshot needs to be taken.
		mountVol := vol
		volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], mountVol.volType, mountVol.contentType, mountVol.name)
		mountFlags, mountOptions := filesystem.ResolveMountOptions(strings.Split(mountVol.ConfigBlockMountOptions(), ","))
		isSnapshot := vol.IsSnapshot()

		// Snapshots should be mounted as readonly.
		if isSnapshot {
			mountFlags |= unix.MS_RDONLY
		}

		// Regenerate filesystem UUID if needed. This is because some filesystems do not allow mounting
		// multiple volumes that share the same UUID. As snapshotting a volume will copy its UUID we need
		// to potentially regenerate the UUID of the snapshot now that we are trying to mount it.
		// This is done at mount time rather than snapshot time for 2 reasons; firstly snapshots need to be
		// as fast as possible, and on some filesystems regenerating the UUID is a slow process, secondly
		// we do not want to modify a snapshot in case it is corrupted for some reason, so at mount time
		// we take another snapshot of the snapshot, regenerate the temporary snapshot's UUID and then
		// mount that.
		regenerateFSUUID := renegerateFilesystemUUIDNeeded(vol.ConfigBlockFilesystem())
		if isSnapshot && regenerateFSUUID {
			// Instantiate a new volume to be the temporary writable snapshot.
			tmpVolName := vol.name + tmpVolSuffix
			tmpVol := NewVolume(d, d.name, vol.volType, vol.contentType, tmpVolName, vol.config, vol.poolConfig)

			// Create writable snapshot from source snapshot named with a tmpVolSuffix suffix.
			_, err = d.createLogicalVolumeSnapshot(d.config["lvm.vg_name"], vol, tmpVol, false, d.usesThinpool())
			if err != nil {
				return fmt.Errorf("Error creating temporary LVM logical volume snapshot: %w", err)
			}

			revert.Add(func() {
				_ = d.removeLogicalVolume(d.lvmDevPath(d.config["lvm.vg_name"], tmpVol.volType, tmpVol.contentType, tmpVol.name))
			})

			// We are going to mount the temporary volume instead.
			mountVol = tmpVol
			volDevPath = d.lvmDevPath(d.config["lvm.vg_name"], mountVol.volType, mountVol.contentType, mountVol.name)

			tmpVolFsType := mountVol.ConfigBlockFilesystem()
			mountOptions = addNoRecoveryMountOption(mountOptions, tmpVolFsType)

			// When mounting XFS filesystems temporarily we can use the nouuid option rather than fully
			// regenerating the filesystem UUID.
			if tmpVolFsType == "xfs" {
				idx := strings.Index(mountOptions, "nouuid")
				if idx < 0 {
					mountOptions += ",nouuid"
				}
			} else {
				d.logger.Debug("Regenerating filesystem UUID", logger.Ctx{"dev": volDevPath, "fs": tmpVolFsType})
				err = regenerateFilesystemUUID(mountVol.ConfigBlockFilesystem(), volDevPath)
				if err != nil {
					return err
				}
			}
		}

		// Activate LVM volume if needed.
		activated, err = d.activateVolume(mountVol)
		if err != nil {
			return err
		}

		// Finally attempt to mount the volume that needs mounting.
		err = TryMount(context.TODO(), volDevPath, mountPath, mountVol.ConfigBlockFilesystem(), mountFlags, mountOptions)
		if err != nil {
			return fmt.Errorf("Failed to mount LVM snapshot volume: %w", err)
		}

		d.logger.Debug("Mounted logical volume", logger.Ctx{"dev": volDevPath, "path": mountPath, "options": mountOptions})
	} else {
		// Activate LVM volume if needed.
		activated, err = d.activateVolume(vol)
		if err != nil {
			return err
		}
	}

	if activated {
		revert.Add(func() { _, _ = d.deactivateVolume(vol) })
	}

	if vol.IsVMBlock() {
		// For VMs, mount the filesystem volume.
		fsVol := vol.NewVMBlockFilesystemVolume()
		err = d.MountVolumeSnapshot(fsVol, op)
		if err != nil {
			return err
		}
	}

	vol.MountRefCountIncrement() // From here on it is up to caller to call UnmountVolumeSnapshot() when done.
	revert.Success()
	return nil
}

// MountVolume mounts a volume and increments ref counter. Please call UnmountVolume() when done with the volume.
func (d *lvm) MountVolume(vol Volume, op *operations.Operation) error {
	return d.mountCommon(vol, op)
}

// unmountCommon includes the logic for unmounting either a volume or a volume snapshot as they follow very similar procedures.
func (d *lvm) unmountCommon(vol Volume, keepBlockDev bool, op *operations.Operation) (bool, error) {
	unlock, err := vol.MountLock()
	if err != nil {
		return false, err
	}

	defer unlock()

	ourUnmount := false
	mountPath := vol.MountPath()

	refCount := vol.MountRefCountDecrement()

	// For VMs, unmount the filesystem volume.
	if vol.IsVMBlock() {
		fsVol := vol.NewVMBlockFilesystemVolume()
		ourUnmount, err = d.UnmountVolume(fsVol, false, op)

		// If the VMBlockFilesystem volume is still in use, we use the refCount
		// of the block volume instead.
		if err != nil && !errors.Is(err, ErrInUse) {
			return false, err
		}
	}

	if refCount > 0 {
		// The LVM driver keeps track of activations separately from mounts, see
		// d.activationRefCountName.
		// Ensure that the activation refcount is also updated when deactivation
		// would normally be skipped.
		d.activationRefCountDecrement(vol)
		d.logger.Debug("Skipping unmount as in use", logger.Ctx{"volName": vol.name, "refCount": refCount})
		return false, ErrInUse
	}

	// Check if already mounted.
	if vol.contentType == ContentTypeFS && filesystem.IsMountPoint(mountPath) {
		if vol.IsSnapshot() {
			// Check if a temporary snapshot exists, and if so remove it.
			tmpVolName := vol.name + tmpVolSuffix
			tmpVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, tmpVolName)
			exists, err := d.logicalVolumeExists(tmpVolDevPath)
			if err != nil {
				return true, fmt.Errorf("Failed to check existence of temporary LVM snapshot volume %q: %w", tmpVolDevPath, err)
			}

			if exists {
				err = d.removeLogicalVolume(tmpVolDevPath)
				if err != nil {
					return true, fmt.Errorf("Failed to remove temporary LVM snapshot volume %q: %w", tmpVolDevPath, err)
				}
			}
		}

		err = TryUnmount(mountPath, 0)
		if err != nil {
			return false, fmt.Errorf("Failed to unmount LVM logical volume: %w", err)
		}

		d.logger.Debug("Unmounted logical volume", logger.Ctx{"volName": vol.name, "path": mountPath, "keepBlockDev": keepBlockDev})

		ourUnmount = true
	} else if IsContentBlock(vol.contentType) {
		volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)
		keepBlockDev = keepBlockDev || !shared.PathExists(volDevPath)
	}

	// We only deactivate filesystem volumes if an unmount was needed to better align with our
	// unmount return value indicator.
	if ourUnmount && !keepBlockDev {
		_, err = d.deactivateVolume(vol)
		if err != nil {
			return false, err
		}
	} else {
		// Since activation of the LV on mount is unconditional, the activation
		// refcount needs to be updated regardless of whether we actually
		// deactivated the volume or not; the refcount represents the number of
		// deactivations that are expected based on the number of times activate
		// is called; it doesn't have anything to do with the real state of the LV.
		d.activationRefCountDecrement(vol)
	}

	return ourUnmount, nil
}

// UnmountVolume unmounts volume if mounted and not in use. Returns true if this unmounted the volume.
// keepBlockDev indicates if backing block device should not be deactivated when volume is unmounted.
func (d *lvm) UnmountVolume(vol Volume, keepBlockDev bool, op *operations.Operation) (bool, error) {
	return d.unmountCommon(vol, keepBlockDev, op)
}

// RenameVolume renames a volume and its snapshots.
func (d *lvm) RenameVolume(vol Volume, newVolName string, op *operations.Operation) error {
	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name)

	return vol.UnmountTask(func(op *operations.Operation) error {
		snapNames, err := d.VolumeSnapshots(vol, op)
		if err != nil {
			return err
		}

		revert := revert.New()
		defer revert.Fail()

		// Rename snapshots (change volume prefix to use new parent volume name).
		for _, snapName := range snapNames {
			snapVolName := GetSnapshotVolumeName(vol.name, snapName)
			snapVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, snapVolName)
			newSnapVolName := GetSnapshotVolumeName(newVolName, snapName)
			newSnapVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, newSnapVolName)
			err = d.renameLogicalVolume(snapVolDevPath, newSnapVolDevPath)
			if err != nil {
				return err
			}

			revert.Add(func() { _ = d.renameLogicalVolume(newSnapVolDevPath, snapVolDevPath) })
		}

		// Rename snapshots dir if present.
		if vol.contentType == ContentTypeFS {
			srcSnapshotDir := GetVolumeSnapshotDir(d.name, vol.volType, vol.name)
			dstSnapshotDir := GetVolumeSnapshotDir(d.name, vol.volType, newVolName)
			if shared.PathExists(srcSnapshotDir) {
				err = os.Rename(srcSnapshotDir, dstSnapshotDir)
				if err != nil {
					return fmt.Errorf("Error renaming LVM logical volume snapshot directory from %q to %q: %w", srcSnapshotDir, dstSnapshotDir, err)
				}

				revert.Add(func() { _ = os.Rename(dstSnapshotDir, srcSnapshotDir) })
			}
		}

		// Rename actual volume.
		newVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, newVolName)
		err = d.renameLogicalVolume(volDevPath, newVolDevPath)
		if err != nil {
			return err
		}

		revert.Add(func() { _ = d.renameLogicalVolume(newVolDevPath, volDevPath) })

		// Rename volume dir.
		if vol.contentType == ContentTypeFS {
			srcVolumePath := GetVolumeMountPath(d.name, vol.volType, vol.name)
			dstVolumePath := GetVolumeMountPath(d.name, vol.volType, newVolName)
			err = os.Rename(srcVolumePath, dstVolumePath)
			if err != nil {
				return fmt.Errorf("Error renaming LVM logical volume mount path from %q to %q: %w", srcVolumePath, dstVolumePath, err)
			}

			revert.Add(func() { _ = os.Rename(dstVolumePath, srcVolumePath) })
		}

		// For VMs, also rename the filesystem volume.
		if vol.IsVMBlock() {
			fsVol := vol.NewVMBlockFilesystemVolume()
			err = d.RenameVolume(fsVol, newVolName, op)
			if err != nil {
				return err
			}
		}

		revert.Success()
		return nil
	}, false, op)
}

// MigrateVolume sends a volume for migration.
func (d *lvm) MigrateVolume(vol VolumeCopy, conn io.ReadWriteCloser, volSrcArgs *migration.VolumeSourceArgs, op *operations.Operation) error {
	return genericVFSMigrateVolume(d, d.state, vol, conn, volSrcArgs, op)
}

// 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 VolumeCopy, projectName string, tarWriter *instancewriter.InstanceTarWriter, _ bool, snapshots []string, op *operations.Operation) error {
	return genericVFSBackupVolume(d, vol, tarWriter, snapshots, op)
}

// CreateVolumeSnapshot creates a snapshot of a volume.
func (d *lvm) CreateVolumeSnapshot(snapVol Volume, op *operations.Operation) error {
	parentName, _, _ := api.GetParentAndSnapshotName(snapVol.name)
	parentVol := NewVolume(d, d.name, snapVol.volType, snapVol.contentType, parentName, snapVol.config, snapVol.poolConfig)
	snapPath := snapVol.MountPath()

	// Create the parent directory.
	err := createParentSnapshotDirIfMissing(d.name, snapVol.volType, parentName)
	if err != nil {
		return err
	}

	revert := revert.New()
	defer revert.Fail()

	// Create snapshot directory.
	err = snapVol.EnsureMountPath()
	if err != nil {
		return err
	}

	revert.Add(func() { _ = os.RemoveAll(snapPath) })

	_, err = d.createLogicalVolumeSnapshot(d.config["lvm.vg_name"], parentVol, snapVol, true, d.usesThinpool())
	if err != nil {
		return fmt.Errorf("Error creating LVM logical volume snapshot: %w", err)
	}

	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, snapVol.contentType, snapVol.name)

	revert.Add(func() {
		_ = d.removeLogicalVolume(volDevPath)
	})

	// For VMs, also snapshot the filesystem.
	if snapVol.IsVMBlock() {
		parentFSVol := parentVol.NewVMBlockFilesystemVolume()
		fsVol := snapVol.NewVMBlockFilesystemVolume()
		_, err = d.createLogicalVolumeSnapshot(d.config["lvm.vg_name"], parentFSVol, fsVol, true, d.usesThinpool())
		if err != nil {
			return fmt.Errorf("Error creating LVM logical volume snapshot: %w", err)
		}
	}

	revert.Success()
	return nil
}

// 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 {
	// Remove the snapshot from the storage device.
	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, snapVol.contentType, snapVol.name)
	lvExists, err := d.logicalVolumeExists(volDevPath)
	if err != nil {
		return err
	}

	if lvExists {
		// Only call UnmountVolumeSnapshot if mounted to avoid breaking deactivaion ref counts.
		if snapVol.contentType == ContentTypeFS && filesystem.IsMountPoint(snapVol.MountPath()) {
			_, err = d.UnmountVolumeSnapshot(snapVol, op)
			if err != nil {
				return fmt.Errorf("Error unmounting LVM logical volume: %w", err)
			}
		}

		err = d.removeLogicalVolume(d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, snapVol.contentType, snapVol.name))
		if err != nil {
			return fmt.Errorf("Error removing LVM logical volume: %w", err)
		}
	}

	// For VMs, also remove the snapshot filesystem volume.
	if snapVol.IsVMBlock() {
		fsVol := snapVol.NewVMBlockFilesystemVolume()
		err = d.DeleteVolumeSnapshot(fsVol, op)
		if err != nil {
			return err
		}
	}

	// Remove the snapshot mount path from the storage device.
	snapPath := snapVol.MountPath()
	err = os.RemoveAll(snapPath)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("Error removing LVM snapshot mount path %q: %w", snapPath, err)
	}

	// Remove the parent snapshot directory if this is the last snapshot being removed.
	parentName, _, _ := api.GetParentAndSnapshotName(snapVol.name)
	err = deleteParentSnapshotDirIfEmpty(d.name, snapVol.volType, parentName)
	if err != nil {
		return err
	}

	return nil
}

// 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) error {
	return d.mountCommon(snapVol, op)
}

// UnmountVolumeSnapshot removes the read-only mount placed on top of a snapshot.
// If a temporary snapshot volume exists then it will attempt to remove it.
func (d *lvm) UnmountVolumeSnapshot(snapVol Volume, op *operations.Operation) (bool, error) {
	return d.unmountCommon(snapVol, false, op)
}

// VolumeSnapshots returns a list of snapshots for the volume (in no particular order).
func (d *lvm) VolumeSnapshots(vol Volume, op *operations.Operation) ([]string, error) {
	// We use the volume list rather than inspecting the logical volumes themselves because the origin
	// property of an LVM snapshot can be removed/changed when restoring snapshots, such that they are no
	// marked as origin of the parent volume. Instead we use prefix matching on the volume names to find the
	// snapshot volumes.
	cmd := exec.Command("lvs", "--noheadings", "-o", "lv_name", d.config["lvm.vg_name"])
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	err = cmd.Start()
	if err != nil {
		return nil, err
	}

	snapshots := []string{}
	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		snapName := d.parseLogicalVolumeSnapshot(vol, strings.TrimSpace(scanner.Text()))
		if snapName == "" {
			continue // Skip logical volumes that are not recognised as a snapshot of our parent vol.
		}

		snapshots = append(snapshots, snapName)
	}

	errMsg, err := io.ReadAll(stderr)
	if err != nil {
		return nil, err
	}

	err = cmd.Wait()
	if err != nil {
		return nil, fmt.Errorf("Failed to get snapshot list for volume %q: %v: %w", vol.name, strings.TrimSpace(string(errMsg)), err)
	}

	return snapshots, nil
}

// RestoreVolume restores a volume from a snapshot.
func (d *lvm) RestoreVolume(vol Volume, snapVol Volume, op *operations.Operation) error {
	_, snapshotName, _ := api.GetParentAndSnapshotName(snapVol.name)

	restoreThinPoolVolume := func(restoreVol Volume) (revert.Hook, error) {
		// Instantiate snapshot volume from snapshot name.
		snapVol, err := restoreVol.NewSnapshot(snapshotName)
		if err != nil {
			return nil, err
		}

		_, err = d.UnmountVolume(restoreVol, false, op)
		if err != nil {
			return nil, fmt.Errorf("Error unmounting LVM logical volume: %w", err)
		}

		originalVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], restoreVol.volType, restoreVol.contentType, restoreVol.name)
		tmpVolName := restoreVol.name + tmpVolSuffix
		tmpVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], restoreVol.volType, restoreVol.contentType, tmpVolName)

		reverter := revert.New()
		defer reverter.Fail()

		// Rename original logical volume to temporary new name so we can revert if needed.
		err = d.renameLogicalVolume(originalVolDevPath, tmpVolDevPath)
		if err != nil {
			return nil, fmt.Errorf("Error temporarily renaming original LVM logical volume: %w", err)
		}

		reverter.Add(func() {
			// Rename the original volume back to the original name.
			_ = d.renameLogicalVolume(tmpVolDevPath, originalVolDevPath)
		})

		// Create writable snapshot from source snapshot named as target volume.
		_, err = d.createLogicalVolumeSnapshot(d.config["lvm.vg_name"], snapVol, restoreVol, false, true)
		if err != nil {
			return nil, fmt.Errorf("Error restoring LVM logical volume snapshot: %w", err)
		}

		volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], restoreVol.volType, restoreVol.contentType, restoreVol.name)

		reverter.Add(func() {
			_ = d.removeLogicalVolume(volDevPath)
		})

		// If the volume's filesystem needs to have its UUID regenerated to allow mount then do so now.
		if restoreVol.contentType == ContentTypeFS && renegerateFilesystemUUIDNeeded(restoreVol.ConfigBlockFilesystem()) {
			_, err = d.activateVolume(restoreVol)
			if err != nil {
				return nil, err
			}

			d.logger.Debug("Regenerating filesystem UUID", logger.Ctx{"dev": volDevPath, "fs": restoreVol.ConfigBlockFilesystem()})
			err = regenerateFilesystemUUID(restoreVol.ConfigBlockFilesystem(), volDevPath)
			if err != nil {
				return nil, err
			}
		}

		// Finally remove the original logical volume. Should always be the last step to allow revert.
		err = d.removeLogicalVolume(d.lvmDevPath(d.config["lvm.vg_name"], restoreVol.volType, restoreVol.contentType, tmpVolName))
		if err != nil {
			return nil, fmt.Errorf("Error removing original LVM logical volume: %w", err)
		}

		cleanup := reverter.Clone().Fail
		reverter.Success()
		return cleanup, nil
	}

	reverter := revert.New()
	defer reverter.Fail()

	// If the pool uses thinpools, then the process for restoring a snapshot is as follows:
	// 1. Rename the original volume to a temporary name (so we can revert later if needed).
	// 2. Create a writable snapshot with the original name from the snapshot being restored.
	// 3. Delete the renamed original volume.
	if d.usesThinpool() {
		cleanup, err := restoreThinPoolVolume(vol)
		if err != nil {
			return err
		}

		reverter.Add(cleanup)

		// For VMs, restore the filesystem volume.
		if vol.IsVMBlock() {
			fsVol := vol.NewVMBlockFilesystemVolume()
			cleanup, err := restoreThinPoolVolume(fsVol)
			if err != nil {
				return err
			}

			reverter.Add(cleanup)
		}

		reverter.Success()
		return nil
	}

	// Instantiate snapshot volume from snapshot name.
	snapVol, err := vol.NewSnapshot(snapshotName)
	if err != nil {
		return err
	}

	// If the pool uses classic logical volumes, then the process for restoring a snapshot is as follows:
	// 1. Ensure snapshot volumes have sufficient CoW capacity to allow restoration.
	// 2. Mount source and target.
	// 3. Copy (rsync or dd) source to target.
	// 4. Unmount source and target.

	// Ensure that the snapshot volumes have sufficient CoW capacity to allow restoration.
	// In the past we set snapshot sizes by specifying the same size as the origin volume. Unfortunately due to
	// the way that LVM extents work, this means that the snapshot CoW capacity can be just a little bit too
	// small to allow the entire snapshot to be restored to the origin. If this happens then we can end up
	// invalidating the snapshot meaning it cannot be used anymore!
	// Nowadays we use the "100%ORIGIN" size when creating snapshots, which lets LVM figure out what the number
	// of extents is required to restore the whole snapshot, but we need to support resizing older snapshots
	// taken before this change. So we use lvresize here to grow the snapshot volume to the size of the origin.
	// The use of "+100%ORIGIN" here rather than just "100%ORIGIN" like we use when taking new snapshots, is
	// rather counter intuitive. However there seems to be a bug in lvresize/lvextend so that when specifying
	// "100%ORIGIN", it fails to extend sufficiently, saying that the number of extents in the snapshot matches
	// that of the origin (which they do). However if we take take that at face value then the restore will
	// end up invalidating the snapshot. Instead if we specify a much larger value (such as adding 100% of
	// the origin to the snapshot size) then LVM is able to extend the snapshot a little bit more, and LVM
	// limits the new size to the maximum CoW size that the snapshot can be (which happens to be the same size
	// as newer snapshots are taken at using the "100%ORIGIN" size). Confusing isn't it.
	if snapVol.IsVMBlock() || snapVol.contentType == ContentTypeFS {
		snapLVPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, ContentTypeFS, snapVol.name)
		_, err = shared.TryRunCommand("lvresize", "-l", "+100%ORIGIN", "-f", snapLVPath)
		if err != nil {
			return fmt.Errorf("Error resizing LV snapshot named %q: %w", snapLVPath, err)
		}
	}

	if snapVol.IsVMBlock() || (snapVol.contentType == ContentTypeBlock && snapVol.volType == VolumeTypeCustom) {
		snapLVPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, ContentTypeBlock, snapVol.name)
		_, err = shared.TryRunCommand("lvresize", "-l", "+100%ORIGIN", "-f", snapLVPath)
		if err != nil {
			return fmt.Errorf("Error resizing LV snapshot named %q: %w", snapLVPath, err)
		}
	}

	// Mount source and target, copy, then unmount.
	err = vol.MountTask(func(mountPath string, op *operations.Operation) error {
		// Copy source to destination (mounting each volume if needed).
		err = snapVol.MountTask(func(srcMountPath string, op *operations.Operation) error {
			if snapVol.IsVMBlock() || snapVol.contentType == ContentTypeFS {
				bwlimit := d.config["rsync.bwlimit"]
				d.Logger().Debug("Copying fileystem volume", logger.Ctx{"sourcePath": srcMountPath, "targetPath": mountPath, "bwlimit": bwlimit})
				_, err := rsync.LocalCopy(srcMountPath, mountPath, bwlimit, true)
				if err != nil {
					return err
				}
			}

			if snapVol.IsVMBlock() || (snapVol.contentType == ContentTypeBlock && snapVol.volType == VolumeTypeCustom) {
				srcDevPath, err := d.GetVolumeDiskPath(snapVol)
				if err != nil {
					return err
				}

				targetDevPath, err := d.GetVolumeDiskPath(vol)
				if err != nil {
					return err
				}

				d.Logger().Debug("Copying block volume", logger.Ctx{"srcDevPath": srcDevPath, "targetPath": targetDevPath})
				err = copyDevice(srcDevPath, targetDevPath)
				if err != nil {
					return err
				}
			}

			return nil
		}, op)
		if err != nil {
			return err
		}

		// Run EnsureMountPath after mounting and syncing to ensure the mounted directory has the
		// correct permissions set.
		err = vol.EnsureMountPath()
		if err != nil {
			return err
		}

		return nil
	}, op)
	if err != nil {
		return fmt.Errorf("Error restoring LVM logical volume snapshot: %w", err)
	}

	reverter.Success()
	return nil
}

// RenameVolumeSnapshot renames a volume snapshot.
func (d *lvm) RenameVolumeSnapshot(snapVol Volume, newSnapshotName string, op *operations.Operation) error {
	volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, snapVol.contentType, snapVol.name)

	parentName, _, _ := api.GetParentAndSnapshotName(snapVol.name)
	newSnapVolName := GetSnapshotVolumeName(parentName, newSnapshotName)
	newVolDevPath := d.lvmDevPath(d.config["lvm.vg_name"], snapVol.volType, snapVol.contentType, newSnapVolName)
	err := d.renameLogicalVolume(volDevPath, newVolDevPath)
	if err != nil {
		return fmt.Errorf("Error renaming LVM logical volume: %w", err)
	}

	oldPath := snapVol.MountPath()
	newPath := GetVolumeMountPath(d.name, snapVol.volType, newSnapVolName)
	err = os.Rename(oldPath, newPath)
	if err != nil {
		return fmt.Errorf("Error renaming snapshot mount path from %q to %q: %w", oldPath, newPath, err)
	}

	return nil
}
