{"sha":"06637bfd3158e2457128703afcf60e286a6d6a61","node_id":"C_kwDOAY-o9doAKDA2NjM3YmZkMzE1OGUyNDU3MTI4NzAzYWZjZjYwZTI4NmE2ZDZhNjE","commit":{"author":{"name":"Tom Parrott","email":"thomas.parrott@canonical.com","date":"2026-03-24T15:38:48Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-24T15:38:48Z"},"message":"Import: Create backup config from index (#17921)\n\nWhen importing a backup, out-rule inconsistencies by creating a backup's\nconfig from its index.\nFurthermore don't anymore include the backup config in non-optimized\nexports, as it can always be re-created from the index (it's a sub type\nof it).","tree":{"sha":"8d6079df8e47708da45000e74a2e91a52891f761","url":"https://api.github.com/repos/canonical/lxd/git/trees/8d6079df8e47708da45000e74a2e91a52891f761"},"url":"https://api.github.com/repos/canonical/lxd/git/commits/06637bfd3158e2457128703afcf60e286a6d6a61","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpwrAICRC1aQ7uu5UhlAAAW8UQAJ4SZSyiMgDHa+L1U4eEiAR4\n+mxivPrzgYIeVQdn/CBhkYcmAeN/0UucNlLhSiXzFLjfX8Hr0LsVTXcAMt5R0tsZ\nEms6Dgp3enWpdOTCOLNG6ikmCuFa1Gu+90PwfidDjIxn0VH7MB1Mb9U4ex10Qfdj\n5dr40avTiwHIu9KGTyKF5z05FkJNs7220ntuJ4AOZT+lvXkHA241ODHRm0l2PeRp\nii7xqbmfbKM6jhxIBd22mnthXCxS6kXHNrNeA6bRL1KdIOASzIcGsBUtsHZiyx1+\nq0nLQyYG73QFTuvOp6dgpxlzF2CPSZWmkVXKXuv3EMReZ9kpgETNyuSqnq1PL6oJ\ntpgOOcaVvwEbAa/WTVrDJnuQaIEPucilXMgv3nE/mPznPmbyagsbJmTydrPOJtt+\nONtqyzPObi2xttBhyHSNNMR+5G7azGfxU4AcgxYlvY0kYUyfDk26IUeBfy2bTByi\nEdTz+fsIgEkyiJB2fktWfLSRQjshr1T41r3VX7UjAQw/TSeNsJ+mdch4WY1j+nKM\nKdm5Vz8R2BRU5VwF1sG1N59YaSKfokDnF3+ClS18dQAZsNgiemAvwKJ3M2jsnLYv\nYaRuKzoCS3rZagggR+6e92MRN6C5CeTZwnXQ8TIWScav561vmQrssPqlhCVzMVmb\n079WbaRkR5mtLvVzcQOH\n=R6iS\n-----END PGP SIGNATURE-----\n","payload":"tree 8d6079df8e47708da45000e74a2e91a52891f761\nparent 69bb194eba58558db8951cbf790d57bbfba99aa8\nparent 0fe9b1fdb4578c5c27e10b45bfc5ea4d23d0832c\nauthor Tom Parrott <thomas.parrott@canonical.com> 1774366728 +0000\ncommitter GitHub <noreply@github.com> 1774366728 +0000\n\nImport: Create backup config from index (#17921)\n\nWhen importing a backup, out-rule inconsistencies by creating a backup's\nconfig from its index.\nFurthermore don't anymore include the backup config in non-optimized\nexports, as it can always be re-created from the index (it's a sub type\nof it).","verified_at":"2026-03-24T15:38:49Z"}},"url":"https://api.github.com/repos/canonical/lxd/commits/06637bfd3158e2457128703afcf60e286a6d6a61","html_url":"https://github.com/canonical/lxd/commit/06637bfd3158e2457128703afcf60e286a6d6a61","comments_url":"https://api.github.com/repos/canonical/lxd/commits/06637bfd3158e2457128703afcf60e286a6d6a61/comments","author":{"login":"tomponline","id":280181,"node_id":"MDQ6VXNlcjI4MDE4MQ==","avatar_url":"https://avatars.githubusercontent.com/u/280181?v=4","gravatar_id":"","url":"https://api.github.com/users/tomponline","html_url":"https://github.com/tomponline","followers_url":"https://api.github.com/users/tomponline/followers","following_url":"https://api.github.com/users/tomponline/following{/other_user}","gists_url":"https://api.github.com/users/tomponline/gists{/gist_id}","starred_url":"https://api.github.com/users/tomponline/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tomponline/subscriptions","organizations_url":"https://api.github.com/users/tomponline/orgs","repos_url":"https://api.github.com/users/tomponline/repos","events_url":"https://api.github.com/users/tomponline/events{/privacy}","received_events_url":"https://api.github.com/users/tomponline/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"69bb194eba58558db8951cbf790d57bbfba99aa8","url":"https://api.github.com/repos/canonical/lxd/commits/69bb194eba58558db8951cbf790d57bbfba99aa8","html_url":"https://github.com/canonical/lxd/commit/69bb194eba58558db8951cbf790d57bbfba99aa8"},{"sha":"0fe9b1fdb4578c5c27e10b45bfc5ea4d23d0832c","url":"https://api.github.com/repos/canonical/lxd/commits/0fe9b1fdb4578c5c27e10b45bfc5ea4d23d0832c","html_url":"https://github.com/canonical/lxd/commit/0fe9b1fdb4578c5c27e10b45bfc5ea4d23d0832c"}],"stats":{"total":315,"additions":199,"deletions":116},"files":[{"sha":"55f991a24f43fcb8c432c369e55cb6c969684a43","filename":"lxd/api_internal.go","status":"modified","additions":9,"deletions":19,"changes":28,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fapi_internal.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fapi_internal.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Fapi_internal.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -729,9 +729,12 @@ func internalSQLExec(tx *sql.Tx, query string, result *internalSQLResult) error\n }\n \n // internalImportFromBackup creates instance, storage pool and volume DB records from an instance's backup file.\n-// It expects the instance volume to be mounted so that the backup.yaml file is readable.\n+// It expects the backup's index file to determine the instance's config.\n // Also accepts an optional map of device overrides.\n-func internalImportFromBackup(ctx context.Context, s *state.State, projectName string, instName string, allowNameOverride bool, deviceOverrides map[string]map[string]string) error {\n+func internalImportFromBackup(ctx context.Context, s *state.State, bInfo *backup.Info, allowNameOverride bool, deviceOverrides map[string]map[string]string) error {\n+\tprojectName := bInfo.Project\n+\tinstName := bInfo.Name\n+\n \tif instName == \"\" {\n \t\treturn errors.New(\"The name of the instance is required\")\n \t}\n@@ -786,23 +789,9 @@ func internalImportFromBackup(ctx context.Context, s *state.State, projectName s\n \t\treturn fmt.Errorf(`The instance %q does not seem to exist on any storage pool`, instName)\n \t}\n \n-\t// User needs to make sure that we can access the directory where backup.yaml lives.\n-\tinstanceMountPoint := instanceMountPoints[0]\n-\tisEmpty, err := shared.PathIsEmpty(instanceMountPoint)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n-\tif isEmpty {\n-\t\treturn fmt.Errorf(`The instance's directory %q appears to be empty. Please ensure that the instance's storage volume is mounted`, instanceMountPoint)\n-\t}\n-\n-\t// Read in the backup.yaml file.\n-\tbackupYamlPath := filepath.Join(instanceMountPoint, \"backup.yaml\")\n-\tbackupConf, err := backup.ParseConfigYamlFile(backupYamlPath)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n+\t// Use the information from the backup index.\n+\t// The backup config later gets persisted to disk too.\n+\tbackupConf := bInfo.Config\n \n \tif backupConf.Instance == nil {\n \t\treturn errors.New(\"Instance definition in backup config is missing\")\n@@ -972,6 +961,7 @@ func internalImportFromBackup(ctx context.Context, s *state.State, projectName s\n \tinstancePath := storagePools.InstancePath(instanceType, projectName, backupConf.Instance.Name, false)\n \tisPrivileged := backupConf.Instance.Config[\"security.privileged\"] == \"\"\n \n+\tinstanceMountPoint := instanceMountPoints[0]\n \terr = storagePools.CreateContainerMountpoint(instanceMountPoint, instancePath, isPrivileged)\n \tif err != nil {\n \t\treturn err"},{"sha":"eb11251272c88727de42fc12f39ce91c645aff76","filename":"lxd/backup.go","status":"modified","additions":8,"deletions":2,"changes":10,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fbackup.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fbackup.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Fbackup.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -255,8 +255,14 @@ func backupWriteIndex(sourceInst instance.Instance, pool storagePools.Pool, opti\n \t\treturn os.ErrNotExist\n \t}\n \n-\t// Do not include any custom storage volumes (and their pools) in the backup config.\n-\tconfig, err := pool.GenerateInstanceBackupConfig(sourceInst, snapshots, nil, nil)\n+\t// Try to include as much information as possible in the backup's index.\n+\t// The index is used during import to re-create the backup's config.\n+\tvolBackupConf, err := pool.GenerateInstanceCustomVolumeBackupConfig(sourceInst, nil, true, nil)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"Failed generating instance custom volume config: %w\", err)\n+\t}\n+\n+\tconfig, err := pool.GenerateInstanceBackupConfig(sourceInst, snapshots, volBackupConf, nil)\n \tif err != nil {\n \t\treturn fmt.Errorf(\"Failed generating instance backup config: %w\", err)\n \t}"},{"sha":"813f778066c6f0e8e71917eb113d2e3d1b582690","filename":"lxd/backup/backup_config_utils.go","status":"modified","additions":11,"deletions":65,"changes":76,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fbackup%2Fbackup_config_utils.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fbackup%2Fbackup_config_utils.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Fbackup%2Fbackup_config_utils.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -5,7 +5,6 @@ import (\n \t\"errors\"\n \t\"fmt\"\n \t\"os\"\n-\t\"path/filepath\"\n \n \t\"go.yaml.in/yaml/v2\"\n \n@@ -201,49 +200,15 @@ func updateRootDevicePool(devices map[string]map[string]string, poolName string)\n \treturn false\n }\n \n-// UpdateInstanceConfig updates the instance's backup.yaml configuration file.\n-func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error {\n-\tbackupFilePath := filepath.Join(mountPath, \"backup.yaml\")\n-\n-\t// Read in the backup.yaml file.\n-\tbackup, err := ParseConfigYamlFile(backupFilePath)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n-\t// Update volume information in the backup.yaml.\n-\tif backup.Volumes != nil {\n-\t\trootVol, err := backup.RootVolume()\n-\t\tif err != nil {\n-\t\t\treturn fmt.Errorf(\"Failed getting the root volume: %w\", err)\n-\t\t}\n-\n-\t\trootVol.Name = b.Name\n-\t\trootVol.Project = b.Project\n-\n-\t\tupdateRootVol, err := b.Config.RootVolume()\n-\t\tif err != nil {\n-\t\t\treturn fmt.Errorf(\"Failed getting the root volume: %w\", err)\n-\t\t}\n-\n-\t\t// Ensure the most recent volume UUIDs get updated.\n-\t\trootVol.Config = updateRootVol.Config\n-\t\trootVol.Snapshots = updateRootVol.Snapshots\n-\t}\n-\n-\t// Update instance information in the backup.yaml.\n-\t// Perform this after fetching the root vol as it's picked by the instance's name from the list of vols.\n-\tif backup.Instance != nil {\n-\t\tbackup.Instance.Name = b.Name\n-\t\tbackup.Instance.Project = b.Project\n-\t}\n-\n+// UpdateInstanceConfigInPlace updates the instance's backup index in place.\n+func UpdateInstanceConfigInPlace(c *db.Cluster, b *Info) error {\n \tvar pool *api.StoragePool\n \n-\terr = c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {\n+\terr := c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {\n+\t\tvar err error\n+\n \t\t// Load the storage pool.\n \t\t_, pool, _, err = tx.GetStoragePool(ctx, b.Pool)\n-\n \t\treturn err\n \t})\n \tif err != nil {\n@@ -252,21 +217,21 @@ func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error {\n \n \trootDiskDeviceFound := false\n \n-\t// Change the pool in the backup.yaml.\n-\terr = backup.UpdateRootVolumePool(pool)\n+\t// Change the pool in case it doesn't match the one of the original instance.\n+\terr = b.Config.UpdateRootVolumePool(pool)\n \tif err != nil {\n \t\treturn fmt.Errorf(\"Failed updating the root volume's pool: %w\", err)\n \t}\n \n-\tif updateRootDevicePool(backup.Instance.Devices, pool.Name) {\n+\tif updateRootDevicePool(b.Config.Instance.Devices, pool.Name) {\n \t\trootDiskDeviceFound = true\n \t}\n \n-\tif updateRootDevicePool(backup.Instance.ExpandedDevices, pool.Name) {\n+\tif updateRootDevicePool(b.Config.Instance.ExpandedDevices, pool.Name) {\n \t\trootDiskDeviceFound = true\n \t}\n \n-\tfor _, snapshot := range backup.Snapshots {\n+\tfor _, snapshot := range b.Config.Snapshots {\n \t\tupdateRootDevicePool(snapshot.Devices, pool.Name)\n \t\tupdateRootDevicePool(snapshot.ExpandedDevices, pool.Name)\n \t}\n@@ -275,24 +240,5 @@ func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error {\n \t\treturn errors.New(\"No root device could be found\")\n \t}\n \n-\t// Write updated backup.yaml file.\n-\n-\tfile, err := os.Create(backupFilePath)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n-\tdefer func() { _ = file.Close() }()\n-\n-\tdata, err := yaml.Marshal(&backup)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n-\t_, err = file.Write(data)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n-\treturn file.Close()\n+\treturn nil\n }"},{"sha":"5ca04ae7e2320bb846c2e50be30c66003573e805","filename":"lxd/instances_post.go","status":"modified","additions":15,"deletions":6,"changes":21,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Finstances_post.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Finstances_post.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Finstances_post.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -871,16 +871,17 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data\n \t\tbInfo.Pool = pool\n \t}\n \n-\t// Override instance name.\n-\tif instanceName != \"\" {\n-\t\tbInfo.Name = instanceName\n-\t}\n-\n \trootVol, err := bInfo.Config.RootVolume()\n \tif err != nil {\n \t\treturn response.SmartError(fmt.Errorf(\"Failed getting the root volume: %w\", err))\n \t}\n \n+\t// Override instance name.\n+\tif instanceName != \"\" {\n+\t\tbInfo.Name = instanceName\n+\t\trootVol.Name = instanceName\n+\t}\n+\n \t// Override the volume's UUID.\n \t// Normally a volume (and its snapshots) gets a new UUID if their config doesn't already have\n \t// a `volatile.uuid` field during creation of the volume's record in the DB.\n@@ -940,6 +941,13 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data\n \t\treturn response.InternalError(err)\n \t}\n \n+\t// Ensure the backup's config included in the index reflects the current state.\n+\t// It is used later to create the actual backup's config.\n+\terr = backup.UpdateInstanceConfigInPlace(s.DB.Cluster, bInfo)\n+\tif err != nil {\n+\t\treturn response.SmartError(fmt.Errorf(\"Failed updating backup index file in place: %w\", err))\n+\t}\n+\n \t// Copy reverter so far so we can use it inside run after this function has finished.\n \trunRevert := revert.Clone()\n \n@@ -969,7 +977,7 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data\n \n \t\trunRevert.Add(revertHook)\n \n-\t\terr = internalImportFromBackup(ctx, s, bInfo.Project, bInfo.Name, instanceName != \"\", devices)\n+\t\terr = internalImportFromBackup(ctx, s, bInfo, instanceName != \"\", devices)\n \t\tif err != nil {\n \t\t\treturn fmt.Errorf(\"Failed importing backup: %w\", err)\n \t\t}\n@@ -984,6 +992,7 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data\n \n \t\t// Run the storage post hook to perform any final actions now that the instance has been created\n \t\t// in the database (this normally includes unmounting volumes that were mounted).\n+\t\t// This also writes the backup config to disk.\n \t\tif postHook != nil {\n \t\t\terr = postHook(inst)\n \t\t\tif err != nil {"},{"sha":"b2c48573186f4d20828ce2a04bafdbab5edc2867","filename":"lxd/storage/backend_lxd.go","status":"modified","additions":0,"deletions":20,"changes":20,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fstorage%2Fbackend_lxd.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fstorage%2Fbackend_lxd.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Fstorage%2Fbackend_lxd.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -942,14 +942,6 @@ func (b *lxdBackend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.\n \t\t})\n \t}\n \n-\t// Update information in the backup.yaml file.\n-\terr = vol.MountTask(func(mountPath string, _ *operations.Operation) error {\n-\t\treturn backup.UpdateInstanceConfig(b.state.DB.Cluster, srcBackup, mountPath)\n-\t}, op)\n-\tif err != nil {\n-\t\treturn nil, nil, fmt.Errorf(\"Error updating backup file: %w\", err)\n-\t}\n-\n \t// Create a post hook function that will use the instance (that will be created) to setup a new volume\n \t// containing the instance's root disk device's config so that the driver's post hook function can access\n \t// that config to perform any post instance creation setup.\n@@ -3423,18 +3415,6 @@ func (b *lxdBackend) BackupInstance(inst instance.Instance, tarWriter *instancew\n \t\treturn err\n \t}\n \n-\tvolBackupConf, err := b.GenerateInstanceCustomVolumeBackupConfig(inst, nil, snapshots, op)\n-\tif err != nil {\n-\t\treturn fmt.Errorf(\"Failed generating instance custom volume config: %w\", err)\n-\t}\n-\n-\t// Ensure the backup file reflects current config.\n-\t// Use the version requested by the caller to write the correct backup file format.\n-\terr = b.UpdateInstanceBackupFile(inst, snapshots, volBackupConf, version, op)\n-\tif err != nil {\n-\t\treturn err\n-\t}\n-\n \tvar snapNames []string\n \tvar sourceSnapshots []drivers.Volume\n \tif snapshots {"},{"sha":"6b39df43ed15dbde1fea12a6ea66d9a853d32b55","filename":"lxd/storage/drivers/generic_vfs.go","status":"modified","additions":14,"deletions":2,"changes":16,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fstorage%2Fdrivers%2Fgeneric_vfs.go","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/lxd%2Fstorage%2Fdrivers%2Fgeneric_vfs.go","contents_url":"https://api.github.com/repos/canonical/lxd/contents/lxd%2Fstorage%2Fdrivers%2Fgeneric_vfs.go?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -519,6 +519,11 @@ func genericVFSBackupVolume(d Driver, vol VolumeCopy, tarWriter *instancewriter.\n \t\t\t// Reset hard link cache as we are copying a new volume (instance or snapshot).\n \t\t\ttarWriter.ResetHardLinkMap()\n \n+\t\t\t// Never include the backup config in the tarball.\n+\t\t\talwaysExcludedPaths := []string{\n+\t\t\t\tfilepath.Join(mountPath, \"backup.yaml\"),\n+\t\t\t}\n+\n \t\t\tif v.contentType != ContentTypeBlock {\n \t\t\t\tlogMsg := \"Copying container filesystem volume\"\n \t\t\t\tif vol.volType == VolumeTypeCustom {\n@@ -548,6 +553,11 @@ func genericVFSBackupVolume(d Driver, vol VolumeCopy, tarWriter *instancewriter.\n \t\t\t\t\t\treturn fmt.Errorf(\"Error walking file during export: %q: %w\", srcPath, err)\n \t\t\t\t\t}\n \n+\t\t\t\t\t// Skip any excluded files.\n+\t\t\t\t\tif shared.StringHasPrefix(srcPath, alwaysExcludedPaths...) {\n+\t\t\t\t\t\treturn nil\n+\t\t\t\t\t}\n+\n \t\t\t\t\tname := filepath.Join(prefix, strings.TrimPrefix(srcPath, mountPath))\n \n \t\t\t\t\t// Write the file to the tarball with ignoreGrowth enabled so that if the\n@@ -588,14 +598,16 @@ func genericVFSBackupVolume(d Driver, vol VolumeCopy, tarWriter *instancewriter.\n \t\t\tif v.IsVMBlock() {\n \t\t\t\tlogMsg := \"Copying virtual machine config volume\"\n \n+\t\t\t\tcombinedExcludedPaths := append(exclude, alwaysExcludedPaths...)\n+\n \t\t\t\td.Logger().Debug(logMsg, logger.Ctx{\"sourcePath\": mountPath, \"prefix\": prefix})\n \t\t\t\terr = filepath.Walk(mountPath, func(srcPath string, fi os.FileInfo, err error) error {\n \t\t\t\t\tif err != nil {\n \t\t\t\t\t\treturn err\n \t\t\t\t\t}\n \n-\t\t\t\t\t// Skip any exluded files.\n-\t\t\t\t\tif shared.StringHasPrefix(srcPath, exclude...) {\n+\t\t\t\t\t// Skip any excluded files.\n+\t\t\t\t\tif shared.StringHasPrefix(srcPath, combinedExcludedPaths...) {\n \t\t\t\t\t\treturn nil\n \t\t\t\t\t}\n "},{"sha":"4353ba59405fefc3e019fe59e775e0364dd56caf","filename":"test/includes/test-groups.sh","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/test%2Fincludes%2Ftest-groups.sh","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/test%2Fincludes%2Ftest-groups.sh","contents_url":"https://api.github.com/repos/canonical/lxd/contents/test%2Fincludes%2Ftest-groups.sh?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -200,6 +200,7 @@ readonly test_group_standalone_storage=(\n     \"backup_instance_uuid\"\n     \"backup_volume_expiry\"\n     \"backup_export_import_recover\"\n+    \"backup_inconsistent_config\"\n     \"container_copy_incremental\"\n     \"container_copy_start\"\n     \"container_devices_disk\""},{"sha":"fff392a672edfc34d6c106ac74c5d44637968d37","filename":"test/suites/backup.sh","status":"modified","additions":141,"deletions":2,"changes":143,"blob_url":"https://github.com/canonical/lxd/blob/06637bfd3158e2457128703afcf60e286a6d6a61/test%2Fsuites%2Fbackup.sh","raw_url":"https://github.com/canonical/lxd/raw/06637bfd3158e2457128703afcf60e286a6d6a61/test%2Fsuites%2Fbackup.sh","contents_url":"https://api.github.com/repos/canonical/lxd/contents/test%2Fsuites%2Fbackup.sh?ref=06637bfd3158e2457128703afcf60e286a6d6a61","patch":"@@ -1242,9 +1242,9 @@ test_backup_metadata() {\n   cat \"${tmpDir}/backup/index.yaml\"\n   [ \"$(yq '.snapshots | length' < \"${tmpDir}/backup/index.yaml\")\" = \"1\" ]\n   [ \"$(yq .config.version < \"${tmpDir}/backup/index.yaml\")\" = \"${highest_version}\" ]\n-  [ \"$(yq '.config.volumes | length' < \"${tmpDir}/backup/index.yaml\")\" = \"1\" ]\n+  [ \"$(yq '.config.volumes | length' < \"${tmpDir}/backup/index.yaml\")\" = \"2\" ]\n   [ \"$(yq '.config.volumes.[0].snapshots | length' < \"${tmpDir}/backup/index.yaml\")\" = \"1\" ]\n-  [ \"$(yq '.config.pools | length' < \"${tmpDir}/backup/index.yaml\")\" = \"1\" ]\n+  [ \"$(yq '.config.pools | length' < \"${tmpDir}/backup/index.yaml\")\" = \"2\" ]\n \n   rm -rf \"${tmpDir}/backup\" \"${tmpDir}/c1.tar.gz\"\n \n@@ -1303,3 +1303,142 @@ test_backup_metadata() {\n \n   rmdir \"${tmpDir}\"\n }\n+\n+test_backup_inconsistent_config() {\n+  local poolName\n+  poolName=\"lxdtest-$(basename \"${LXD_DIR}\")\"\n+\n+  # Create a restricted project and switch to it.\n+  lxc project create restricted \\\n+    -c restricted=true\n+  lxc profile device add default root disk path=/ pool=\"${poolName}\" --project restricted\n+\n+  # Switch to restricted project to test imports.\n+  lxc project switch restricted\n+\n+  tmpDir=$(mktemp -d -p \"${TEST_DIR}\" backups-XXX)\n+\n+  # Create a new empty instance with a clean index and inconsistent backup config.\n+  mkdir -p \"${tmpDir}/backup/container\"\n+  cat > \"${tmpDir}/backup/index.yaml\" <<EOF\n+version: 2\n+name: inconsistent-instance\n+backend: dir\n+pool: ${poolName}\n+type: container\n+optimized: false\n+config:\n+  instance:\n+    name: inconsistent-instance\n+    architecture: x86_64\n+    type: container\n+    config: {}\n+    devices: {}\n+    expanded_config: {}\n+    expanded_devices:\n+      root:\n+        path: /\n+        pool: ${poolName}\n+        type: disk\n+    profiles:\n+      - default\n+    stateful: false\n+  pools:\n+    - name: ${poolName}\n+      driver: dir\n+  volumes:\n+    - name: inconsistent-instance\n+      type: container\n+      pool: ${poolName}\n+      content_type: filesystem\n+      config:\n+        volatile.uuid: \"96c0c029-e0f4-4d36-8a2a-49eaa4b1572f\"\n+EOF\n+  cat > \"${tmpDir}/backup/container/backup.yaml\" <<EOF\n+instance:\n+  name: inconsistent-instance\n+  architecture: x86_64\n+  type: container\n+  config:\n+    security.privileged: \"true\"\n+  devices: {}\n+  expanded_config:\n+    security.privileged: \"true\"\n+  expanded_devices:\n+    root:\n+      path: /\n+      pool: ${poolName}\n+      type: disk\n+  profiles:\n+    - default\n+  stateful: false\n+pools:\n+  - name: ${poolName}\n+    driver: dir\n+volumes:\n+  - name: inconsistent-instance\n+    type: container\n+    pool: ${poolName}\n+    content_type: filesystem\n+    config:\n+      volatile.uuid: \"96c0c029-e0f4-4d36-8a2a-49eaa4b1572f\"\n+EOF\n+\n+  # Re-package the inconsistent archive.\n+  tar -cf \"${tmpDir}/inconsistent-backup.tar\" -C \"${tmpDir}\" \"backup/\"\n+\n+  # Importing the instance from tarball succeeds.\n+  lxc import \"${tmpDir}/inconsistent-backup.tar\"\n+\n+  # Check the inconsistent backup config got overwritten by the index during import\n+  [ \"$(lxc config get inconsistent-instance security.privileged || echo fail)\" = \"\" ]\n+\n+  lxc delete -f inconsistent-instance\n+\n+  # Fix the backup's config by taking the contents from the index.\n+  # As we are using yq from the snap, it doesn't have permission to read inside ${tmpDir}.\n+  # Instead we have to juggle a bit to read and overwrite the original file as we cannot use yq -i for inplace updates.\n+  yq '.instance.config = {}' < \"${tmpDir}/backup/container/backup.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/container/backup.yaml\"\n+  yq '.instance.expanded_config = {}' < \"${tmpDir}/backup/container/backup.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/container/backup.yaml\"\n+\n+  # Re-package the fixed archive.\n+  tar -cf \"${tmpDir}/fixed-backup.tar\" -C \"${tmpDir}\" \"backup/\"\n+\n+  # Importing the instance from tarball succeeds.\n+  lxc import \"${tmpDir}/fixed-backup.tar\"\n+\n+  lxc delete -f inconsistent-instance\n+\n+  # Now make the index inconsistent.\n+  yq '.config.instance.config += {\"security.privileged\": \"true\"}' < \"${tmpDir}/backup/index.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/index.yaml\"\n+  yq '.config.instance.expanded_config += {\"security.privileged\": \"true\"}' < \"${tmpDir}/backup/index.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/index.yaml\"\n+\n+  # Re-package the inconsistent archive.\n+  tar -cf \"${tmpDir}/inconsistent-backup.tar\" -C \"${tmpDir}\" \"backup/\"\n+\n+  # Importing the instance from tarball fails.\n+  ! lxc import \"${tmpDir}/inconsistent-backup.tar\" || false\n+\n+  # Fix the index.\n+  yq '.config.instance.config = {}' < \"${tmpDir}/backup/index.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/index.yaml\"\n+  yq '.config.instance.expanded_config = {}' < \"${tmpDir}/backup/index.yaml\" > temp.yaml && mv temp.yaml \"${tmpDir}/backup/index.yaml\"\n+\n+  # Re-package the fixed archive.\n+  tar -cf \"${tmpDir}/fixed-backup.tar\" -C \"${tmpDir}\" \"backup/\"\n+\n+  # Importing the instance from tarball succeeds.\n+  lxc import \"${tmpDir}/fixed-backup.tar\"\n+\n+  lxc delete -f inconsistent-instance\n+  lxc project delete restricted\n+  rm -rf \"${tmpDir}/backup\"\n+\n+  # Check an exported instance doesn't contain a backup config.\n+  lxc init --empty c1 -d \"${SMALL_ROOT_DISK}\"\n+  lxc export c1 \"${tmpDir}/c1.tar.gz\"\n+  tar -xzf \"${tmpDir}/c1.tar.gz\" -C \"${tmpDir}\"\n+  [ ! -f \"${tmpDir}/backup/container/backup.yaml\" ]\n+\n+  lxc delete -f c1\n+  rm -r \"${tmpDir}\"\n+}"}]}