[lxc-devel] [lxd/master] Storage: BTRFS backup subvolume support

tomponline on Github lxc-bot at linuxcontainers.org
Wed May 13 13:42:36 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 356 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200513/88a1fd6e/attachment.bin>
-------------- next part --------------
From 1b15e36f20e46a399e28647e2839d1d6da4a3c43 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:23:56 +0100
Subject: [PATCH 1/9] lxd/device/device/utils/generic: Removes deviceNameEncode
 and deviceNameDecode

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/device_utils_generic.go | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/lxd/device/device_utils_generic.go b/lxd/device/device_utils_generic.go
index 8828403a68..3b1c051f6a 100644
--- a/lxd/device/device_utils_generic.go
+++ b/lxd/device/device_utils_generic.go
@@ -4,20 +4,6 @@ import (
 	"strings"
 )
 
-// deviceNameEncode encodes a string to be used as part of a file name in the LXD devices path.
-// The encoding scheme replaces "-" with "--" and then "/" with "-".
-func deviceNameEncode(text string) string {
-	return strings.Replace(strings.Replace(text, "-", "--", -1), "/", "-", -1)
-}
-
-// deviceNameDecode decodes a string used in the LXD devices path back to its original form.
-// The decoding scheme converts "-" back to "/" and "--" back to "-".
-func deviceNameDecode(text string) string {
-	// This converts "--" to the null character "\0" first, to allow remaining "-" chars to be
-	// converted back to "/" before making a final pass to convert "\0" back to original "-".
-	return strings.Replace(strings.Replace(strings.Replace(text, "--", "\000", -1), "-", "/", -1), "\000", "-", -1)
-}
-
 // deviceJoinPath joins together prefix and text delimited by a "." for device path generation.
 func deviceJoinPath(parts ...string) string {
 	return strings.Join(parts, ".")

From b0f658ca41406ab74b3615f2099baa9f371bd559 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:24:26 +0100
Subject: [PATCH 2/9] lxd/storage/drivers/utils: Adds PathNameEncode and
 PathNameDecode

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

diff --git a/lxd/storage/drivers/utils.go b/lxd/storage/drivers/utils.go
index 8093fa1d7a..a2cf4dfa7f 100644
--- a/lxd/storage/drivers/utils.go
+++ b/lxd/storage/drivers/utils.go
@@ -753,3 +753,17 @@ func BlockDevSizeBytes(blockDevPath string) (int64, error) {
 
 	return int64(res), nil
 }
+
+// PathNameEncode encodes a path string to be used as part of a file name.
+// The encoding scheme replaces "-" with "--" and then "/" with "-".
+func PathNameEncode(text string) string {
+	return strings.Replace(strings.Replace(text, "-", "--", -1), "/", "-", -1)
+}
+
+// PathNameDecode decodes a string containing an encoded path back to its original form.
+// The decoding scheme converts "-" back to "/" and "--" back to "-".
+func PathNameDecode(text string) string {
+	// This converts "--" to the null character "\0" first, to allow remaining "-" chars to be
+	// converted back to "/" before making a final pass to convert "\0" back to original "-".
+	return strings.Replace(strings.Replace(strings.Replace(text, "--", "\000", -1), "-", "/", -1), "\000", "-", -1)
+}

From 66c0bd5ee9d93389102862dadf42c570c60f89d0 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:27:29 +0100
Subject: [PATCH 3/9] lxd/device/device: PathNameEncode and PathNameDecode
 usage

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/device_utils_unix.go | 17 +++++++++--------
 lxd/device/disk.go              |  4 ++--
 lxd/device/unix_common.go       |  3 ++-
 3 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/lxd/device/device_utils_unix.go b/lxd/device/device_utils_unix.go
index da711e6f01..810e77dc99 100644
--- a/lxd/device/device_utils_unix.go
+++ b/lxd/device/device_utils_unix.go
@@ -12,6 +12,7 @@ import (
 
 	deviceConfig "github.com/lxc/lxd/lxd/device/config"
 	"github.com/lxc/lxd/lxd/state"
+	storageDrivers "github.com/lxc/lxd/lxd/storage/drivers"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/idmap"
 	"github.com/lxc/lxd/shared/logger"
@@ -190,7 +191,7 @@ func UnixDeviceCreate(s *state.State, idmapSet *idmap.IdmapSet, devicesPath stri
 
 	destPath := unixDeviceDestPath(m)
 	relativeDestPath := strings.TrimPrefix(destPath, "/")
-	devName := deviceNameEncode(deviceJoinPath(prefix, relativeDestPath))
+	devName := storageDrivers.PathNameEncode(deviceJoinPath(prefix, relativeDestPath))
 	devPath := filepath.Join(devicesPath, devName)
 
 	// Create the new entry.
@@ -253,7 +254,7 @@ func unixDeviceSetup(s *state.State, devicesPath string, typePrefix string, devi
 
 	// Convert the requested dest path inside the instance to an encoded relative one.
 	ourDestPath := unixDeviceDestPath(m)
-	ourEncRelDestFile := deviceNameEncode(strings.TrimPrefix(ourDestPath, "/"))
+	ourEncRelDestFile := storageDrivers.PathNameEncode(strings.TrimPrefix(ourDestPath, "/"))
 
 	// Load all existing host devices.
 	dents, err := ioutil.ReadDir(devicesPath)
@@ -359,7 +360,7 @@ func unixDeviceSetupBlockNum(s *state.State, devicesPath string, typePrefix stri
 // UnixDeviceExists checks if the unix device already exists in devices path.
 func UnixDeviceExists(devicesPath string, prefix string, path string) bool {
 	relativeDestPath := strings.TrimPrefix(path, "/")
-	devName := fmt.Sprintf("%s.%s", deviceNameEncode(prefix), deviceNameEncode(relativeDestPath))
+	devName := fmt.Sprintf("%s.%s", storageDrivers.PathNameEncode(prefix), storageDrivers.PathNameEncode(relativeDestPath))
 	devPath := filepath.Join(devicesPath, devName)
 
 	return shared.PathExists(devPath)
@@ -384,9 +385,9 @@ func unixDeviceRemove(devicesPath string, typePrefix string, deviceName string,
 	var ourPrefix string
 	// If a prefix override has been supplied, use that for filtering the devices to remove.
 	if optPrefix != "" {
-		ourPrefix = deviceNameEncode(deviceJoinPath(typePrefix, deviceName, optPrefix))
+		ourPrefix = storageDrivers.PathNameEncode(deviceJoinPath(typePrefix, deviceName, optPrefix))
 	} else {
-		ourPrefix = deviceNameEncode(deviceJoinPath(typePrefix, deviceName))
+		ourPrefix = storageDrivers.PathNameEncode(deviceJoinPath(typePrefix, deviceName))
 	}
 
 	ourDevs := []string{}
@@ -448,7 +449,7 @@ func unixDeviceRemove(devicesPath string, typePrefix string, deviceName string,
 
 		// Append this device to the mount rules (these will be unmounted).
 		runConf.Mounts = append(runConf.Mounts, deviceConfig.MountEntryItem{
-			TargetPath: deviceNameDecode(ourEncRelDestFile),
+			TargetPath: storageDrivers.PathNameDecode(ourEncRelDestFile),
 		})
 
 		absDevPath := filepath.Join(devicesPath, ourDev)
@@ -474,9 +475,9 @@ func unixDeviceDeleteFiles(s *state.State, devicesPath string, typePrefix string
 	var ourPrefix string
 	// If a prefix override has been supplied, use that for filtering the devices to remove.
 	if optPrefix != "" {
-		ourPrefix = deviceNameEncode(deviceJoinPath(typePrefix, deviceName, optPrefix))
+		ourPrefix = storageDrivers.PathNameEncode(deviceJoinPath(typePrefix, deviceName, optPrefix))
 	} else {
-		ourPrefix = deviceNameEncode(deviceJoinPath(typePrefix, deviceName))
+		ourPrefix = storageDrivers.PathNameEncode(deviceJoinPath(typePrefix, deviceName))
 	}
 
 	// Load all devices.
diff --git a/lxd/device/disk.go b/lxd/device/disk.go
index a0104be82d..10ab81fde0 100644
--- a/lxd/device/disk.go
+++ b/lxd/device/disk.go
@@ -189,7 +189,7 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
 // getDevicePath returns the absolute path on the host for this instance and supplied device config.
 func (d *disk) getDevicePath(devName string, devConfig deviceConfig.Device) string {
 	relativeDestPath := strings.TrimPrefix(devConfig["path"], "/")
-	devPath := deviceNameEncode(deviceJoinPath("disk", devName, relativeDestPath))
+	devPath := storageDrivers.PathNameEncode(deviceJoinPath("disk", devName, relativeDestPath))
 	return filepath.Join(d.inst.DevicesPath(), devPath)
 }
 
@@ -1514,7 +1514,7 @@ func (d *disk) getParentBlocks(path string) ([]string, error) {
 // generateVMConfigDrive generates an ISO containing the cloud init config for a VM.
 // Returns the path to the ISO.
 func (d *disk) generateVMConfigDrive() (string, error) {
-	scratchDir := filepath.Join(d.inst.DevicesPath(), deviceNameEncode(d.name))
+	scratchDir := filepath.Join(d.inst.DevicesPath(), storageDrivers.PathNameEncode(d.name))
 
 	// Check we have the mkisofs tool available.
 	mkisofsPath, err := exec.LookPath("mkisofs")
diff --git a/lxd/device/unix_common.go b/lxd/device/unix_common.go
index 87de76f724..21e273afe1 100644
--- a/lxd/device/unix_common.go
+++ b/lxd/device/unix_common.go
@@ -8,6 +8,7 @@ import (
 	deviceConfig "github.com/lxc/lxd/lxd/device/config"
 	"github.com/lxc/lxd/lxd/instance"
 	"github.com/lxc/lxd/lxd/instance/instancetype"
+	storageDrivers "github.com/lxc/lxd/lxd/storage/drivers"
 	"github.com/lxc/lxd/shared"
 )
 
@@ -91,7 +92,7 @@ func (d *unixCommon) Register() error {
 		// Derive the host side path for the instance device file.
 		ourPrefix := deviceJoinPath("unix", deviceName)
 		relativeDestPath := strings.TrimPrefix(unixDeviceDestPath(devConfig), "/")
-		devName := deviceNameEncode(deviceJoinPath(ourPrefix, relativeDestPath))
+		devName := storageDrivers.PathNameEncode(deviceJoinPath(ourPrefix, relativeDestPath))
 		devPath := filepath.Join(devicesPath, devName)
 
 		runConf := deviceConfig.RunConfig{}

From 36c681e2814af736d0719d258d5a3ca19719b12c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:28:44 +0100
Subject: [PATCH 4/9] lxd/storage/drivers/driver/types: Adds
 OptimizedBackupHeader field to Info

Used to indicate if driver will generate a backup header file when generating an optimized backup file.

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

diff --git a/lxd/storage/drivers/driver_types.go b/lxd/storage/drivers/driver_types.go
index c7547211ed..700eca1cd6 100644
--- a/lxd/storage/drivers/driver_types.go
+++ b/lxd/storage/drivers/driver_types.go
@@ -8,6 +8,7 @@ type Info struct {
 	Remote                bool         // Whether the driver uses a remote backing store.
 	OptimizedImages       bool         // Whether driver stores images as separate volume.
 	OptimizedBackups      bool         // Whether driver supports optimized volume backups.
+	OptimizedBackupHeader bool         // Whether driver generates an optimised backup header file in backup.
 	PreservesInodes       bool         // Whether driver preserves inodes when volumes are moved hosts.
 	BlockBacking          bool         // Whether driver uses block devices as backing store.
 	RunningQuotaResize    bool         // Whether quota resize is supported whilst instance running.

From ca9ad71f6984235fd34ca1e258d77ba3e673e538 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:29:56 +0100
Subject: [PATCH 5/9] lxd/backup/backup: Adds OptimizedHeader field to Info
 struct

Used to indicate if an optimized backup file will contain a driver specific optimized header file.

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

diff --git a/lxd/backup/backup.go b/lxd/backup/backup.go
index 3648f2d375..5b9b2b571a 100644
--- a/lxd/backup/backup.go
+++ b/lxd/backup/backup.go
@@ -31,7 +31,8 @@ type Info struct {
 	Backend          string           `json:"backend" yaml:"backend"`
 	Pool             string           `json:"pool" yaml:"pool"`
 	Snapshots        []string         `json:"snapshots,omitempty" yaml:"snapshots,omitempty"`
-	OptimizedStorage *bool            `json:"optimized,omitempty" yaml:"optimized,omitempty"` // Optional field to handle older optimized backups that don't have this field.
+	OptimizedStorage *bool            `json:"optimized,omitempty" yaml:"optimized,omitempty"`               // Optional field to handle older optimized backups that don't have this field.
+	OptimizedHeader  *bool            `json:"optimized_header,omitempty" yaml:"optimized_header,omitempty"` // Optional field to handle older optimized backups that don't have this field.
 	Type             api.InstanceType `json:"type" yaml:"type"`
 }
 

From 44da0769461216ff792e4e5cc86acab1aa4e556c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:30:35 +0100
Subject: [PATCH 6/9] lxd/backup: Updates backupWriteIndex to populate the
 OptimizedHeader field

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/backup.go | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/lxd/backup.go b/lxd/backup.go
index a1bef75103..f61c119920 100644
--- a/lxd/backup.go
+++ b/lxd/backup.go
@@ -167,6 +167,12 @@ func backupCreate(s *state.State, args db.InstanceBackupArgs, sourceInst instanc
 
 // backupWriteIndex generates an index.yaml file and then writes it to the root of the backup tarball.
 func backupWriteIndex(sourceInst instance.Instance, pool storagePools.Pool, optimized bool, snapshots bool, tarWriter *instancewriter.InstanceTarWriter) error {
+	// Indicate whether the driver will include a driver-specific optimized header.
+	poolDriverOptimizedHeader := false
+	if optimized {
+		poolDriverOptimizedHeader = pool.Driver().Info().OptimizedBackupHeader
+	}
+
 	indexInfo := backup.Info{
 		Name:             sourceInst.Name(),
 		Pool:             pool.Name(),
@@ -174,6 +180,7 @@ func backupWriteIndex(sourceInst instance.Instance, pool storagePools.Pool, opti
 		Backend:          pool.Driver().Info().Name,
 		Type:             api.InstanceType(sourceInst.Type().String()),
 		OptimizedStorage: &optimized,
+		OptimizedHeader:  &poolDriverOptimizedHeader,
 	}
 
 	if snapshots {
@@ -188,7 +195,7 @@ func backupWriteIndex(sourceInst instance.Instance, pool storagePools.Pool, opti
 		}
 	}
 
-	// Convert to JSON.
+	// Convert to YAML.
 	indexData, err := yaml.Marshal(&indexInfo)
 	if err != nil {
 		return err

From da3f45b2f656bdb41eb30f70c956100bd05a5316 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:31:06 +0100
Subject: [PATCH 7/9] lxd/storage/drivers/driver/btrfs: Sets
 OptimizedBackupHeader to true in Info struct response

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

diff --git a/lxd/storage/drivers/driver_btrfs.go b/lxd/storage/drivers/driver_btrfs.go
index e69d0ff18b..2ec544339f 100644
--- a/lxd/storage/drivers/driver_btrfs.go
+++ b/lxd/storage/drivers/driver_btrfs.go
@@ -73,6 +73,7 @@ func (d *btrfs) Info() Info {
 		Version:               btrfsVersion,
 		OptimizedImages:       true,
 		OptimizedBackups:      true,
+		OptimizedBackupHeader: true,
 		PreservesInodes:       !d.state.OS.RunningInUserNS,
 		Remote:                false,
 		VolumeTypes:           []VolumeType{VolumeTypeCustom, VolumeTypeImage, VolumeTypeContainer, VolumeTypeVM},

From eec84caf71e1c3c86a2a8af5a150203e53905c3e Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:31:36 +0100
Subject: [PATCH 8/9] lxd/storage/drivers/driver/btrfs/utils: Adds warning to
 BTRFSSubVolume and BTRFSMetaDataHeader about shared usage

Modifying these structs in the future will impact both migration and backup subsystems which may be unwanted.

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

diff --git a/lxd/storage/drivers/driver_btrfs_utils.go b/lxd/storage/drivers/driver_btrfs_utils.go
index ef1aa554a0..6f3e795212 100644
--- a/lxd/storage/drivers/driver_btrfs_utils.go
+++ b/lxd/storage/drivers/driver_btrfs_utils.go
@@ -423,6 +423,7 @@ func (d *btrfs) setSubvolumeReadonlyProperty(path string, readonly bool) error {
 }
 
 // BTRFSSubVolume is the structure used to store information about a subvolume.
+// Note: This is used by both migration and backup subsystems so do not modify without considering both!
 type BTRFSSubVolume struct {
 	Path     string `json:"path" yaml:"path"`         // Path inside the volume where the subvolume belongs (so / is the top of the volume tree).
 	Snapshot string `json:"snapshot" yaml:"snapshot"` // Snapshot name the subvolume belongs to.
@@ -466,6 +467,7 @@ func (d *btrfs) getSubvolumesMetaData(vol Volume) ([]BTRFSSubVolume, error) {
 }
 
 // BTRFSMetaDataHeader is the meta data header about the volumes being sent/stored.
+// Note: This is used by both migration and backup subsystems so do not modify without considering both!
 type BTRFSMetaDataHeader struct {
 	Subvolumes []BTRFSSubVolume `json:"subvolumes" yaml:"subvolumes"` // Sub volumes inside the volume (including the top level ones).
 }

From 34a1cd6fc2a206a63a849816e8eb378a40f08253 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Wed, 13 May 2020 14:33:16 +0100
Subject: [PATCH 9/9] lxd/storage/drivers/driver/btrfs/volumes: Updates
 BackupVolume to add subvolumes to optimized backup file

 Adds an optimized_header.yaml file containing subvolume metadata.
 Adds each subvolume as a separate .bin file, the name of which encodes the relative path of the subvolume so it can be found from the optimized_header.yaml file data.

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

diff --git a/lxd/storage/drivers/driver_btrfs_volumes.go b/lxd/storage/drivers/driver_btrfs_volumes.go
index fcd3c8bd15..398b0f24d4 100644
--- a/lxd/storage/drivers/driver_btrfs_volumes.go
+++ b/lxd/storage/drivers/driver_btrfs_volumes.go
@@ -1,6 +1,7 @@
 package drivers
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -9,8 +10,10 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"github.com/pkg/errors"
+	"gopkg.in/yaml.v2"
 
 	"github.com/lxc/lxd/lxd/migration"
 	"github.com/lxc/lxd/lxd/operations"
@@ -910,7 +913,45 @@ func (d *btrfs) BackupVolume(vol Volume, tarWriter *instancewriter.InstanceTarWr
 		return genericVFSBackupVolume(d, vol, tarWriter, snapshots, op)
 	}
 
-	// Handle the optimized tarballs.
+	// Optimized backup.
+	var err error
+	var volSnapshots []string
+
+	// Retrieve the snapshots if requested.
+	if snapshots {
+		volSnapshots, err = d.VolumeSnapshots(vol, op)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Generate driver restoration header.
+	optimizedHeader, err := d.restorationHeader(vol, volSnapshots)
+	if err != nil {
+		return err
+	}
+
+	// Convert to YAML.
+	optimizedHeaderYAML, err := yaml.Marshal(&optimizedHeader)
+	if err != nil {
+		return err
+	}
+	r := bytes.NewReader(optimizedHeaderYAML)
+
+	indexFileInfo := instancewriter.FileInfo{
+		FileName:    "backup/optimized_header.yaml",
+		FileSize:    int64(len(optimizedHeaderYAML)),
+		FileMode:    0644,
+		FileModTime: time.Now(),
+	}
+
+	// Write to tarball.
+	err = tarWriter.WriteFileFromReader(r, &indexFileInfo)
+	if err != nil {
+		return err
+	}
+
+	// sendToFile sends a subvolume to backup file.
 	sendToFile := func(path string, parent string, fileName string) error {
 		// Prepare btrfs send arguments.
 		args := []string{"send"}
@@ -929,7 +970,7 @@ func (d *btrfs) BackupVolume(vol Volume, tarWriter *instancewriter.InstanceTarWr
 		defer os.Remove(tmpFile.Name())
 
 		// Write the subvolume to the file.
-		d.logger.Debug("Generating optimized volume file", log.Ctx{"sourcePath": path, "file": tmpFile.Name(), "name": fileName})
+		d.logger.Debug("Generating optimized volume file", log.Ctx{"sourcePath": path, "parent": parent, "file": tmpFile.Name(), "name": fileName})
 		err = shared.RunCommandWithFds(nil, tmpFile, "btrfs", args...)
 		if err != nil {
 			return err
@@ -949,44 +990,104 @@ func (d *btrfs) BackupVolume(vol Volume, tarWriter *instancewriter.InstanceTarWr
 		return tmpFile.Close()
 	}
 
-	// Handle snapshots.
-	finalParent := ""
-	if snapshots {
-		// Retrieve the snapshots.
-		volSnapshots, err := d.VolumeSnapshots(vol, op)
-		if err != nil {
-			return err
+	// addVolume adds a volume and its subvolumes to backup file.
+	addVolume := func(v Volume, sourcePrefix string, parentPrefix string, fileNamePrefix string) error {
+		snapName := "" // Default to empty (sending main volume) from migrationHeader.Subvolumes.
+
+		// Detect if we are adding a snapshot by comparing to main volume name.
+		// We can't only use IsSnapshot() as the main vol may itself be a snapshot.
+		if v.IsSnapshot() && v.name != vol.name {
+			_, snapName, _ = shared.InstanceGetParentAndSnapshotName(v.name)
 		}
 
-		for i, snap := range volSnapshots {
-			fullSnapshotName := GetSnapshotVolumeName(vol.name, snap)
+		sentVols := 0
 
-			// Figure out parent and current subvolumes.
-			parent := ""
-			if i > 0 {
-				parent = GetVolumeMountPath(d.name, vol.volType, GetSnapshotVolumeName(vol.name, volSnapshots[i-1]))
+		// Add volume (and any subvolumes if supported) to backup file.
+		for _, subVolume := range optimizedHeader.Subvolumes {
+			if subVolume.Snapshot != snapName {
+				continue // Only add subvolumes related to snapshot name (empty for main vol).
 			}
 
-			cur := GetVolumeMountPath(d.name, vol.volType, fullSnapshotName)
+			// Detect if parent subvolume exists, and if so use it for differential.
+			parentPath := ""
+			if parentPrefix != "" && btrfsIsSubVolume(filepath.Join(parentPrefix, subVolume.Path)) {
+				parentPath = filepath.Join(parentPrefix, subVolume.Path)
+
+				// Set parent subvolume readonly if needed so we can add the subvolume.
+				if !BTRFSSubVolumeIsRo(parentPath) {
+					err = d.setSubvolumeReadonlyProperty(parentPath, true)
+					if err != nil {
+						return err
+					}
+					defer d.setSubvolumeReadonlyProperty(parentPath, false)
+				}
+			}
 
-			// Make a binary btrfs backup.
-			prefix := "snapshots"
-			fileName := fmt.Sprintf("%s.bin", snap)
-			if vol.volType == VolumeTypeVM {
-				prefix = "virtual-machine-snapshots"
-				if vol.contentType == ContentTypeFS {
-					fileName = fmt.Sprintf("%s-config.bin", snap)
+			// Set subvolume readonly if needed so we can add it.
+			sourcePath := filepath.Join(sourcePrefix, subVolume.Path)
+			if !BTRFSSubVolumeIsRo(sourcePath) {
+				err = d.setSubvolumeReadonlyProperty(sourcePath, true)
+				if err != nil {
+					return err
 				}
+				defer d.setSubvolumeReadonlyProperty(sourcePath, false)
+			}
+
+			// Default to no subvolume name for root subvolume to maintain backwards compatibility
+			// with earlier optimized dump format. Although restoring this backup file on an earlier
+			// system will not restore the subvolumes stored inside the backup.
+			subVolName := ""
+			if subVolume.Path != string(filepath.Separator) {
+				// Encode the path of the subvolume (without the leading /) into the filename so
+				// that we find the file from the optimized header's Path field on restore.
+				subVolName = fmt.Sprintf("_%s", PathNameEncode(strings.TrimPrefix(subVolume.Path, string(filepath.Separator))))
 			}
 
-			target := fmt.Sprintf("backup/%s/%s", prefix, fileName)
-			err := sendToFile(cur, parent, target)
+			fileName := fmt.Sprintf("%s%s.bin", fileNamePrefix, subVolName)
+			err = sendToFile(sourcePath, parentPath, filepath.Join("backup", fileName))
 			if err != nil {
-				return err
+				return errors.Wrapf(err, "Failed adding volume %v:%s", v.name, subVolume.Path)
+			}
+
+			sentVols++
+		}
+
+		// Ensure we found and sent at least root subvolume of the volume requested.
+		if sentVols < 1 {
+			return fmt.Errorf("No matching subvolume(s) for %q found in subvolumes list", v.name)
+		}
+
+		return nil
+	}
+
+	// Backup snapshots if populated.
+	finalParent := ""
+	for i, snapName := range volSnapshots {
+		snapVol, _ := vol.NewSnapshot(snapName)
+
+		// Locate the parent snapshot.
+		parentSnapshotPath := ""
+		if i > 0 {
+			parentSnapshotPath = GetVolumeMountPath(d.name, vol.volType, GetSnapshotVolumeName(vol.name, volSnapshots[i-1]))
+		}
+
+		// Make a binary btrfs backup.
+		snapDir := "snapshots"
+		fileName := snapName
+		if vol.volType == VolumeTypeVM {
+			snapDir = "virtual-machine-snapshots"
+			if vol.contentType == ContentTypeFS {
+				fileName = fmt.Sprintf("%s-config", snapName)
 			}
+		}
 
-			finalParent = cur
+		fileNamePrefix := filepath.Join(snapDir, fileName)
+		err := addVolume(snapVol, snapVol.MountPath(), parentSnapshotPath, fileNamePrefix)
+		if err != nil {
+			return err
 		}
+
+		finalParent = snapVol.MountPath()
 	}
 
 	// Make a temporary copy of the instance.
@@ -1017,18 +1118,17 @@ func (d *btrfs) BackupVolume(vol Volume, tarWriter *instancewriter.InstanceTarWr
 		return err
 	}
 
-	// Dump the container to a file.
-	fileName := "container.bin"
+	// Dump the instance to a file.
+	fileNamePrefix := "container"
 	if vol.volType == VolumeTypeVM {
 		if vol.contentType == ContentTypeFS {
-			fileName = "virtual-machine-config.bin"
+			fileNamePrefix = "virtual-machine-config"
 		} else {
-			fileName = "virtual-machine.bin"
+			fileNamePrefix = "virtual-machine"
 		}
 	}
 
-	// Dump the container to a file.
-	err = sendToFile(targetVolume, finalParent, fmt.Sprintf("backup/%s", fileName))
+	err = addVolume(vol, targetVolume, finalParent, fileNamePrefix)
 	if err != nil {
 		return err
 	}


More information about the lxc-devel mailing list