The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/4463
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This feature allows importing and exporting containers (with snapshots). ## Status What has been done so far: - [x] Database - [ ] API endpoints - [x] /1.0/containers/NAME/backups (GET) - [x] /1.0/containers/NAME/backups (POST) - [x] /1.0/containers/NAME/backups/NAME (DELETE) - [x] /1.0/containers/NAME/backups/NAME (GET) - [x] /1.0/containers/NAME/backups/NAME (POST) - [ ] /1.0/containers/NAME/backups/NAME/export (GET) - [ ] /1.0/containers (extend POST) - [ ] Storage - [x] dir - [x] LVM - [ ] Btrfs - [ ] ZFS - [ ] Ceph - [ ] Commands - [ ] `lxc export [<remote>:]<container> [target] [--container-only] [--optimized-storage]` - [ ] `lxc import [<remote>:] <backup file>` - [ ] Documentation - [ ] Tests
From 81e84e9b3165d340b36ea03b72c8cd7437c43446 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Wed, 4 Apr 2018 17:22:59 +0200 Subject: [PATCH 1/4] db: Create containers_backups table Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/db/cluster/schema.go | 13 ++++++++++++- lxd/db/cluster/update.go | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lxd/db/cluster/schema.go b/lxd/db/cluster/schema.go index e1371b1b9..89ea9a921 100644 --- a/lxd/db/cluster/schema.go +++ b/lxd/db/cluster/schema.go @@ -34,6 +34,17 @@ CREATE TABLE containers ( UNIQUE (name), FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE ); +CREATE TABLE containers_backups ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + container_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + creation_date DATETIME, + expiry_date DATETIME, + container_only INTEGER NOT NULL default 0, + optimized_storage INTEGER NOT NULL default 0, + FOREIGN KEY (container_id) REFERENCES containers (id) ON DELETE CASCADE, + UNIQUE (container_id, name) +); CREATE TABLE containers_config ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, container_id INTEGER NOT NULL, @@ -235,5 +246,5 @@ CREATE TABLE storage_volumes_config ( FOREIGN KEY (storage_volume_id) REFERENCES storage_volumes (id) ON DELETE CASCADE ); -INSERT INTO schema (version, updated_at) VALUES (7, strftime("%s")) +INSERT INTO schema (version, updated_at) VALUES (8, strftime("%s")) ` diff --git a/lxd/db/cluster/update.go b/lxd/db/cluster/update.go index 8b40b628e..e782ee1ad 100644 --- a/lxd/db/cluster/update.go +++ b/lxd/db/cluster/update.go @@ -32,6 +32,25 @@ var updates = map[int]schema.Update{ 5: updateFromV4, 6: updateFromV5, 7: updateFromV6, + 8: updateFromV7, +} + +func updateFromV7(tx *sql.Tx) error { + stmts := ` +CREATE TABLE containers_backups ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + container_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + creation_date DATETIME, + expiry_date DATETIME, + container_only INTEGER NOT NULL default 0, + optimized_storage INTEGER NOT NULL default 0, + FOREIGN KEY (container_id) REFERENCES containers (id) ON DELETE CASCADE, + UNIQUE (container_id, name) +); +` + _, err := tx.Exec(stmts) + return err } // The zfs.pool_name config key is node-specific, and needs to be linked to From 946be28114f54e547d28eed4ccbb2eac0ee1ea2a Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Mon, 9 Apr 2018 16:13:06 +0200 Subject: [PATCH 2/4] lxd: Add container backups Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/api_1.0.go | 2 + lxd/backup.go | 106 ++++++++++++++++++ lxd/container.go | 52 ++++++++- lxd/container_backup.go | 243 +++++++++++++++++++++++++++++++++++++++++ lxd/container_lxc.go | 21 ++++ lxd/containers.go | 20 ++++ lxd/db/containers.go | 158 +++++++++++++++++++++++++++ lxd/storage.go | 56 ++++++++++ lxd/storage_btrfs.go | 12 ++ lxd/storage_ceph.go | 12 ++ lxd/storage_dir.go | 12 ++ lxd/storage_lvm.go | 12 ++ lxd/storage_mock.go | 12 ++ lxd/storage_zfs.go | 12 ++ lxd/sys/fs.go | 1 + shared/api/container_backup.go | 26 +++++ shared/version/api.go | 1 + 17 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 lxd/backup.go create mode 100644 lxd/container_backup.go create mode 100644 shared/api/container_backup.go diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index 9bc06cad3..171d0b7b6 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -33,6 +33,8 @@ var api10 = []Command{ containerExecCmd, containerMetadataCmd, containerMetadataTemplatesCmd, + containerBackupsCmd, + containerBackupCmd, aliasCmd, aliasesCmd, eventsCmd, diff --git a/lxd/backup.go b/lxd/backup.go new file mode 100644 index 000000000..e6da46be2 --- /dev/null +++ b/lxd/backup.go @@ -0,0 +1,106 @@ +package main + +import ( + "time" + + "github.com/lxc/lxd/lxd/state" + "github.com/lxc/lxd/shared/api" +) + +// backup represents a container backup. +type backup struct { + state *state.State + container container + + // Properties + id int + name string + creationDate time.Time + expiryDate time.Time + containerOnly bool + optimizedStorage bool +} + +// Rename renames a container backup. +func (b *backup) Rename(newName string) error { + ourStart, err := b.container.StorageStart() + if err != nil { + return err + } + if ourStart { + defer b.container.StorageStop() + } + + // Rename the database entry + err = b.state.Cluster.ContainerBackupRename(b.Name(), newName) + if err != nil { + return err + } + + // Rename the directories and files + err = b.container.Storage().ContainerBackupRename(*b, newName) + if err != nil { + return err + } + + return nil +} + +// Delete removes a container backup. +func (b *backup) Delete() error { + ourStart, err := b.container.StorageStart() + if err != nil { + return err + } + if ourStart { + defer b.container.StorageStop() + } + + // Remove the database record + err = b.state.Cluster.ContainerBackupRemove(b.Name()) + if err != nil { + return err + } + + // Delete backup from storage + err = b.container.Storage().ContainerBackupDelete(b.Name()) + if err != nil { + return err + } + + return nil +} + +func (b *backup) Render() interface{} { + return &api.ContainerBackup{ + Name: b.name, + CreationDate: b.creationDate, + ExpiryDate: b.expiryDate, + ContainerOnly: b.containerOnly, + OptimizedStorage: b.optimizedStorage, + } +} + +func (b *backup) Id() int { + return b.id +} + +func (b *backup) Name() string { + return b.name +} + +func (b *backup) CreationDate() time.Time { + return b.creationDate +} + +func (b *backup) ExpiryDate() time.Time { + return b.expiryDate +} + +func (b *backup) ContainerOnly() bool { + return b.containerOnly +} + +func (b *backup) OptimizedStorage() bool { + return b.optimizedStorage +} diff --git a/lxd/container.go b/lxd/container.go index 6134ffd60..7f66e8e34 100644 --- a/lxd/container.go +++ b/lxd/container.go @@ -435,13 +435,14 @@ type container interface { Stop(stateful bool) error Unfreeze() error - // Snapshots & migration + // Snapshots & migration & backups Restore(sourceContainer container, stateful bool) error /* actionScript here is a script called action.sh in the stateDir, to * be passed to CRIU as --action-script */ Migrate(args *CriuMigrationArgs) error Snapshots() ([]container, error) + Backups() ([]backup, error) // Config handling Rename(newName string) error @@ -977,3 +978,52 @@ func containerLoadByName(s *state.State, name string) (container, error) { return containerLXCLoad(s, args) } + +func containerBackupLoadByName(s *state.State, name string) (*backup, error) { + // Get the DB record + args, err := s.Cluster.ContainerGetBackup(name) + if err != nil { + return nil, err + } + + c, err := containerLoadById(s, args.ContainerID) + if err != nil { + return nil, err + } + + return &backup{ + state: s, + container: c, + id: args.ID, + name: name, + creationDate: args.CreationDate, + expiryDate: args.ExpiryDate, + containerOnly: args.ContainerOnly, + optimizedStorage: args.OptimizedStorage, + }, nil +} + +func containerBackupCreate(s *state.State, args db.ContainerBackupArgs, + sourceContainer container) error { + err := s.Cluster.ContainerBackupCreate(args) + if err != nil { + if err == db.ErrAlreadyDefined { + return fmt.Errorf("backup '%s' already exists", args.Name) + } + return err + } + + b, err := containerBackupLoadByName(s, args.Name) + if err != nil { + return err + } + + // Now create the empty snapshot + err = sourceContainer.Storage().ContainerBackupCreate(*b, sourceContainer) + if err != nil { + s.Cluster.ContainerBackupRemove(args.Name) + return err + } + + return nil +} diff --git a/lxd/container_backup.go b/lxd/container_backup.go new file mode 100644 index 000000000..e4df43828 --- /dev/null +++ b/lxd/container_backup.go @@ -0,0 +1,243 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/lxc/lxd/lxd/db" + "github.com/lxc/lxd/lxd/util" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/api" + "github.com/lxc/lxd/shared/version" +) + +func containerBackupsGet(d *Daemon, r *http.Request) Response { + cname := mux.Vars(r)["name"] + + // Handle requests targeted to a container on a different node + response, err := ForwardedResponseIfContainerIsRemote(d, r, cname) + if err != nil { + return SmartError(err) + } + if response != nil { + return response + } + + recursion := util.IsRecursionRequest(r) + + c, err := containerLoadByName(d.State(), cname) + if err != nil { + return SmartError(err) + } + + backups, err := c.Backups() + if err != nil { + return SmartError(err) + } + + resultString := []string{} + resultMap := []*api.ContainerBackup{} + + for _, backup := range backups { + if !recursion { + url := fmt.Sprintf("/%s/containers/%s/backups/%s", + version.APIVersion, cname, backup.Name()) + resultString = append(resultString, url) + } else { + render := backup.Render() + resultMap = append(resultMap, render.(*api.ContainerBackup)) + } + } + + if !recursion { + return SyncResponse(true, resultString) + } + + return SyncResponse(true, resultMap) +} + +func containerBackupsPost(d *Daemon, r *http.Request) Response { + name := mux.Vars(r)["name"] + + // Handle requests targeted to a container on a different node + response, err := ForwardedResponseIfContainerIsRemote(d, r, name) + if err != nil { + return SmartError(err) + } + if response != nil { + return response + } + + c, err := containerLoadByName(d.State(), name) + if err != nil { + return SmartError(err) + } + + ourStart, err := c.StorageStart() + if err != nil { + return InternalError(err) + } + if ourStart { + defer c.StorageStop() + } + + req := api.ContainerBackupsPost{} + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return BadRequest(err) + } + + // Validate the name + if strings.Contains(req.Name, "/") { + return BadRequest(fmt.Errorf("Backup names may not contain slashes")) + } + + fullName := name + shared.SnapshotDelimiter + req.Name + + backup := func(op *operation) error { + args := db.ContainerBackupArgs{ + Name: fullName, + ContainerID: c.Id(), + CreationDate: time.Now(), + ExpiryDate: time.Now().Add(time.Duration(req.ExpiryDate) * time.Second), + ContainerOnly: req.ContainerOnly, + OptimizedStorage: req.OptimizedStorage, + } + + err := containerBackupCreate(d.State(), args, c) + if err != nil { + return err + } + + return nil + } + + resources := map[string][]string{} + resources["containers"] = []string{name} + + op, err := operationCreate(d.cluster, operationClassTask, + "Backing up container", resources, nil, backup, nil, nil) + if err != nil { + return InternalError(err) + } + + return OperationResponse(op) +} + +func containerBackupGet(d *Daemon, r *http.Request) Response { + name := mux.Vars(r)["name"] + backupName := mux.Vars(r)["backupName"] + + // Handle requests targeted to a container on a different node + response, err := ForwardedResponseIfContainerIsRemote(d, r, name) + if err != nil { + return SmartError(err) + } + if response != nil { + return response + } + + backup, err := containerBackupLoadByName(d.State(), backupName) + if err != nil { + return SmartError(err) + } + + return SyncResponse(true, backup.Render()) +} + +func containerBackupPost(d *Daemon, r *http.Request) Response { + name := mux.Vars(r)["name"] + backupName := mux.Vars(r)["backupName"] + + // Handle requests targeted to a container on a different node + response, err := ForwardedResponseIfContainerIsRemote(d, r, name) + if err != nil { + return SmartError(err) + } + if response != nil { + return response + } + + req := api.ContainerBackupPost{} + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return BadRequest(err) + } + + // Validate the name + if strings.Contains(req.Name, "/") { + return BadRequest(fmt.Errorf("Backup names may not contain slashes")) + } + + oldName := name + shared.SnapshotDelimiter + backupName + backup, err := containerBackupLoadByName(d.State(), oldName) + if err != nil { + SmartError(err) + } + + newName := name + shared.SnapshotDelimiter + req.Name + + rename := func(op *operation) error { + err := backup.Rename(newName) + if err != nil { + return err + } + + return nil + } + + resources := map[string][]string{} + resources["containers"] = []string{name} + + op, err := operationCreate(d.cluster, operationClassTask, + "Renaming container backup", resources, nil, rename, nil, nil) + if err != nil { + return InternalError(err) + } + + return OperationResponse(op) +} + +func containerBackupDelete(d *Daemon, r *http.Request) Response { + name := mux.Vars(r)["name"] + backupName := mux.Vars(r)["backupName"] + + // Handle requests targeted to a container on a different node + response, err := ForwardedResponseIfContainerIsRemote(d, r, name) + if err != nil { + return SmartError(err) + } + if response != nil { + return response + } + + fullName := name + shared.SnapshotDelimiter + backupName + backup, err := containerBackupLoadByName(d.State(), fullName) + if err != nil { + return SmartError(err) + } + + remove := func(op *operation) error { + err := backup.Delete() + if err != nil { + return err + } + + return nil + } + + resources := map[string][]string{} + resources["container"] = []string{name} + + op, err := operationCreate(d.cluster, operationClassTask, + "Removing container backup", resources, nil, remove, nil, nil) + if err != nil { + return InternalError(err) + } + + return OperationResponse(op) +} diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index b40eb99f3..35e642a77 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -3000,6 +3000,27 @@ func (c *containerLXC) Snapshots() ([]container, error) { return containers, nil } +func (c *containerLXC) Backups() ([]backup, error) { + // Get all the backups + backupNames, err := c.state.Cluster.ContainerGetBackups(c.name) + if err != nil { + return nil, err + } + + // Build the backup list + backups := []backup{} + for _, backupName := range backupNames { + backup, err := containerBackupLoadByName(c.state, backupName) + if err != nil { + return nil, err + } + + backups = append(backups, *backup) + } + + return backups, nil +} + func (c *containerLXC) Restore(sourceContainer container, stateful bool) error { var ctxMap log.Ctx diff --git a/lxd/containers.go b/lxd/containers.go index e28357adc..1764cf99e 100644 --- a/lxd/containers.go +++ b/lxd/containers.go @@ -81,6 +81,26 @@ var containerMetadataTemplatesCmd = Command{ delete: containerMetadataTemplatesDelete, } +var containerBackupsCmd = Command{ + name: "containers/{name}/backups", + get: containerBackupsGet, + post: containerBackupsPost, +} + +var containerBackupCmd = Command{ + name: "containers/{name}/backups/{backupName}", + get: containerBackupGet, + post: containerBackupPost, + delete: containerBackupDelete, +} + +/* +var containerBackupExportCmd = Command{ + name: "containers/{name}/backups/{backupName}/export", + get: containerBackupExportGet, +} +*/ + type containerAutostartList []container func (slice containerAutostartList) Len() int { diff --git a/lxd/db/containers.go b/lxd/db/containers.go index 3b93a0d51..5b30c7013 100644 --- a/lxd/db/containers.go +++ b/lxd/db/containers.go @@ -36,6 +36,18 @@ type ContainerArgs struct { Stateful bool } +type ContainerBackupArgs struct { + // Don't set manually + ID int + + ContainerID int + Name string + CreationDate time.Time + ExpiryDate time.Time + ContainerOnly bool + OptimizedStorage bool +} + // ContainerType encodes the type of container (either regular or snapshot). type ContainerType int @@ -872,3 +884,149 @@ WHERE storage_volumes.node_id=? AND storage_volumes.name=? AND storage_volumes.t return poolName, nil } + +// ContainerBackupID returns the ID of the container backup with the given name. +func (c *Cluster) ContainerBackupID(name string) (int, error) { + q := "SELECT id FROM containers_backups WHERE name=?" + id := -1 + arg1 := []interface{}{name} + arg2 := []interface{}{&id} + err := dbQueryRowScan(c.db, q, arg1, arg2) + return id, err +} + +// ContainerGetBackup returns the backup with the given name. +func (c *Cluster) ContainerGetBackup(name string) (ContainerBackupArgs, error) { + args := ContainerBackupArgs{} + args.Name = name + + containerOnlyInt := -1 + optimizedStorageInt := -1 + q := ` +SELECT id, container_id, creation_date, expiry_date, container_only, optimized_storage + FROM containers_backups + WHERE name=? +` + arg1 := []interface{}{name} + arg2 := []interface{}{&args.ID, &args.ContainerID, &args.CreationDate, + &args.ExpiryDate, &containerOnlyInt, &optimizedStorageInt} + err := dbQueryRowScan(c.db, q, arg1, arg2) + if err != nil { + return args, err + } + + if containerOnlyInt == 1 { + args.ContainerOnly = true + } + + if optimizedStorageInt == 1 { + args.OptimizedStorage = true + } + + return args, nil +} + +// ContainerGetBackups returns the names of all backups of the container +// with the given name. +func (c *Cluster) ContainerGetBackups(name string) ([]string, error) { + var result []string + + q := `SELECT containers_backups.name FROM containers_backups +JOIN containers ON containers_backups.container_id=containers.id +WHERE containers.name=?` + inargs := []interface{}{name} + outfmt := []interface{}{name} + dbResults, err := queryScan(c.db, q, inargs, outfmt) + if err != nil { + return nil, err + } + + for _, r := range dbResults { + result = append(result, r[0].(string)) + } + + return result, nil +} + +func (c *Cluster) ContainerBackupCreate(args ContainerBackupArgs) error { + _, err := c.ContainerBackupID(args.Name) + if err == nil { + return ErrAlreadyDefined + } + + err = c.Transaction(func(tx *ClusterTx) error { + containerOnlyInt := 0 + if args.ContainerOnly { + containerOnlyInt = 1 + } + + optimizedStorageInt := 0 + if args.OptimizedStorage { + optimizedStorageInt = 1 + } + + str := fmt.Sprintf("INSERT INTO containers_backups (container_id, name, creation_date, expiry_date, container_only, optimized_storage) VALUES (?, ?, ?, ?, ?, ?)") + stmt, err := tx.tx.Prepare(str) + if err != nil { + return err + } + defer stmt.Close() + result, err := stmt.Exec(args.ContainerID, args.Name, + args.CreationDate.Unix(), args.ExpiryDate.Unix(), containerOnlyInt, + optimizedStorageInt) + if err != nil { + return err + } + + _, err = result.LastInsertId() + if err != nil { + return fmt.Errorf("Error inserting %s into database", args.Name) + } + + return nil + }) + + return err +} + +// ContainerBackupRemove removes the container backup with the given name from +// the database. +func (c *Cluster) ContainerBackupRemove(name string) error { + id, err := c.ContainerBackupID(name) + if err != nil { + return err + } + + err = exec(c.db, "DELETE FROM containers_backups WHERE id=?", id) + if err != nil { + return err + } + + return nil +} + +// ContainerBackupRename renames a container backup from the given current name +// to the new one. +func (c *Cluster) ContainerBackupRename(oldName, newName string) error { + err := c.Transaction(func(tx *ClusterTx) error { + str := fmt.Sprintf("UPDATE containers_backups SET name = ? WHERE name = ?") + stmt, err := tx.tx.Prepare(str) + if err != nil { + return err + } + defer stmt.Close() + + logger.Debug( + "Calling SQL Query", + log.Ctx{ + "query": "UPDATE containers_backups SET name = ? WHERE name = ?", + "oldName": oldName, + "newName": newName}) + if _, err := stmt.Exec(newName, oldName); err != nil { + return err + } + + return nil + }) + return err +} diff --git a/lxd/storage.go b/lxd/storage.go index f718027e0..a060f52b4 100644 --- a/lxd/storage.go +++ b/lxd/storage.go @@ -187,6 +187,10 @@ type storage interface { ContainerSnapshotStart(c container) (bool, error) ContainerSnapshotStop(c container) (bool, error) + ContainerBackupCreate(backup backup, sourceContainer container) error + ContainerBackupDelete(name string) error + ContainerBackupRename(backup backup, newName string) error + // For use in migrating snapshots. ContainerSnapshotCreateEmpty(c container) error @@ -580,6 +584,11 @@ func getStoragePoolVolumeMountPoint(poolName string, volumeName string) string { return shared.VarPath("storage-pools", poolName, "custom", volumeName) } +// ${LXD_DIR}/storage-pools/<pool>/backups/<backup_name> +func getBackupMountPoint(poolName string, backupName string) string { + return shared.VarPath("storage-pools", poolName, "backups", backupName) +} + func createContainerMountpoint(mountPoint string, mountPointSymlink string, privileged bool) error { var mode os.FileMode if privileged { @@ -718,6 +727,53 @@ func deleteSnapshotMountpoint(snapshotMountpoint string, snapshotsSymlinkTarget return nil } +func createBackupMountpoint(backupMountpoint string, backupsSymlinkTarget string, backupsSymlink string) error { + backupMntPointExists := shared.PathExists(backupMountpoint) + mntPointSymlinkExist := shared.PathExists(backupsSymlink) + + if !backupMntPointExists { + err := os.MkdirAll(backupMountpoint, 0711) + if err != nil { + return err + } + } + + if !mntPointSymlinkExist { + err := os.Symlink(backupsSymlinkTarget, backupsSymlink) + if err != nil { + return err + } + } + + return nil +} + +func deleteBackupMountpoint(backupMountpoint string, backupsSymlinkTarget string, backupsSymlink string) error { + if shared.PathExists(backupMountpoint) { + err := os.Remove(backupMountpoint) + if err != nil { + return err + } + } + + couldRemove := false + if shared.PathExists(backupsSymlinkTarget) { + err := os.Remove(backupsSymlinkTarget) + if err == nil { + couldRemove = true + } + } + + if couldRemove && shared.PathExists(backupsSymlink) { + err := os.Remove(backupsSymlink) + if err != nil { + return err + } + } + + return nil +} + // ShiftIfNecessary sets the volatile.last_state.idmap key to the idmap last // used by the container. func ShiftIfNecessary(container container, srcIdmap *idmap.IdmapSet) error { diff --git a/lxd/storage_btrfs.go b/lxd/storage_btrfs.go index 6c2e02f4f..4e09b3298 100644 --- a/lxd/storage_btrfs.go +++ b/lxd/storage_btrfs.go @@ -1383,6 +1383,18 @@ func (s *storageBtrfs) ContainerSnapshotCreateEmpty(snapshotContainer container) return nil } +func (s *storageBtrfs) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageBtrfs) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageBtrfs) ContainerBackupRename(backup backup, newName string) error { + return nil +} + func (s *storageBtrfs) ImageCreate(fingerprint string) error { logger.Debugf("Creating BTRFS storage volume for image \"%s\" on storage pool \"%s\".", fingerprint, s.pool.Name) diff --git a/lxd/storage_ceph.go b/lxd/storage_ceph.go index 1839c0867..c9f89d570 100644 --- a/lxd/storage_ceph.go +++ b/lxd/storage_ceph.go @@ -2226,6 +2226,18 @@ func (s *storageCeph) ContainerSnapshotCreateEmpty(c container) error { return nil } +func (s *storageCeph) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageCeph) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageCeph) ContainerBackupRename(backup backup, newName string) error { + return nil +} + func (s *storageCeph) ImageCreate(fingerprint string) error { logger.Debugf(`Creating RBD storage volume for image "%s" on storage `+ `pool "%s"`, fingerprint, s.pool.Name) diff --git a/lxd/storage_dir.go b/lxd/storage_dir.go index d555596d7..cfc4be2d6 100644 --- a/lxd/storage_dir.go +++ b/lxd/storage_dir.go @@ -1011,6 +1011,18 @@ func (s *storageDir) ContainerSnapshotStop(container container) (bool, error) { return true, nil } +func (s *storageDir) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageDir) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageDir) ContainerBackupRename(backup backup, newName string) error { + return nil +} + func (s *storageDir) ImageCreate(fingerprint string) error { return nil } diff --git a/lxd/storage_lvm.go b/lxd/storage_lvm.go index df5c7cb30..214ee3bc8 100644 --- a/lxd/storage_lvm.go +++ b/lxd/storage_lvm.go @@ -1561,6 +1561,18 @@ func (s *storageLvm) ContainerSnapshotCreateEmpty(snapshotContainer container) e return nil } +func (s *storageLvm) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageLvm) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageLvm) ContainerBackupRename(backup backup, newName string) error { + return nil +} + func (s *storageLvm) ImageCreate(fingerprint string) error { logger.Debugf("Creating LVM storage volume for image \"%s\" on storage pool \"%s\".", fingerprint, s.pool.Name) diff --git a/lxd/storage_mock.go b/lxd/storage_mock.go index 673fea4a6..7bae39168 100644 --- a/lxd/storage_mock.go +++ b/lxd/storage_mock.go @@ -190,6 +190,18 @@ func (s *storageMock) ContainerSnapshotCreateEmpty(snapshotContainer container) return nil } +func (s *storageMock) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageMock) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageMock) ContainerBackupRename(backup backup, newName string) error { + return nil +} + func (s *storageMock) ImageCreate(fingerprint string) error { return nil } diff --git a/lxd/storage_zfs.go b/lxd/storage_zfs.go index e3a0594d6..66f7b2d1b 100644 --- a/lxd/storage_zfs.go +++ b/lxd/storage_zfs.go @@ -1853,6 +1853,18 @@ func (s *storageZfs) ContainerSnapshotCreateEmpty(snapshotContainer container) e return nil } +func (s *storageZfs) ContainerBackupCreate(backup backup, sourceContainer container) error { + return nil +} + +func (s *storageZfs) ContainerBackupDelete(name string) error { + return nil +} + +func (s *storageZfs) ContainerBackupRename(backup backup, newName string) error { + return nil +} + // - create temporary directory ${LXD_DIR}/images/lxd_images_ // - create new zfs volume images/<fingerprint> // - mount the zfs volume on ${LXD_DIR}/images/lxd_images_ diff --git a/lxd/sys/fs.go b/lxd/sys/fs.go index 8370838eb..61dc47298 100644 --- a/lxd/sys/fs.go +++ b/lxd/sys/fs.go @@ -25,6 +25,7 @@ func (s *OS) initDirs() error { {filepath.Join(s.VarDir, "networks"), 0711}, {filepath.Join(s.VarDir, "disks"), 0700}, {filepath.Join(s.VarDir, "storage-pools"), 0711}, + {filepath.Join(s.VarDir, "backups"), 0711}, } for _, dir := range dirs { diff --git a/shared/api/container_backup.go b/shared/api/container_backup.go new file mode 100644 index 000000000..b6c8c0545 --- /dev/null +++ b/shared/api/container_backup.go @@ -0,0 +1,26 @@ +package api + +import "time" + +// ContainerBackupsPost represents the fields available for a new LXD container backup +type ContainerBackupsPost struct { + Name string `json:"name" yaml:"name"` + ExpiryDate int64 `json:"expiry" yaml:"expiry"` + ContainerOnly bool `json:"container_only" yaml:"container_only"` + OptimizedStorage bool `json:"optimized_storage" yaml:"optimized_storage"` +} + +// ContainerBackup represents a LXD conainer backup +type ContainerBackup struct { + Name string `json:"name" yaml:"name"` + CreationDate time.Time `json:"creation_date" yaml:"creation_date"` + ExpiryDate time.Time `json:"expiry_date" yaml:"expiry_date"` + ContainerOnly bool `json:"container_only" yaml:"container_only"` + OptimizedStorage bool `json:"optimized_storage" yaml:"optimized_storage"` +} + +// ContainerBackupPost represents the fields available for the renaming of a +// container backup +type ContainerBackupPost struct { + Name string `json:"name" yaml:"name"` +} diff --git a/shared/version/api.go b/shared/version/api.go index bec41352f..dcaac1d61 100644 --- a/shared/version/api.go +++ b/shared/version/api.go @@ -102,6 +102,7 @@ var APIExtensions = []string{ "event_lifecycle", "storage_api_remote_volume_handling", "nvidia_runtime", + "backup", } // APIExtensionsCount returns the number of available API extensions. From 9d9e83f11a4f10a8f2c5cc5f525bd306634a1699 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Thu, 12 Apr 2018 15:36:42 +0200 Subject: [PATCH 3/4] lxd: Implement backups for storage_dir Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage_dir.go | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/lxd/storage_dir.go b/lxd/storage_dir.go index cfc4be2d6..d1df8b5ef 100644 --- a/lxd/storage_dir.go +++ b/lxd/storage_dir.go @@ -590,6 +590,25 @@ func (s *storageDir) ContainerDelete(container container) error { } } + // Delete potential leftover backup mountpoints. + backupMntPoint := getBackupMountPoint(s.pool.Name, container.Name()) + if shared.PathExists(backupMntPoint) { + err := os.RemoveAll(backupMntPoint) + if err != nil { + return err + } + } + + // Delete potential leftover backup symlinks: + // ${LXD_DIR}/backups/<container_name> -> ${POOL}/backups/<container_name> + backupSymlink := shared.VarPath("backups", container.Name()) + if shared.PathExists(backupSymlink) { + err := os.Remove(backupSymlink) + if err != nil { + return err + } + } + logger.Debugf("Deleted DIR storage volume for container \"%s\" on storage pool \"%s\".", s.volume.Name, s.pool.Name) return nil } @@ -773,6 +792,35 @@ func (s *storageDir) ContainerRename(container container, newName string) error } } + // Rename the backup mountpoint for the container if existing: + // ${POOL}/backups/<old_container_name> to ${POOL}/backups/<new_container_name> + oldBackupsMntPoint := getBackupMountPoint(s.pool.Name, container.Name()) + newBackupsMntPoint := getBackupMountPoint(s.pool.Name, newName) + if shared.PathExists(oldBackupsMntPoint) { + err = os.Rename(oldBackupsMntPoint, newBackupsMntPoint) + if err != nil { + return err + } + } + + // Remove the old backup symlink: + // ${LXD_DIR}/backups/<old_container_name> + oldBackupSymlink := shared.VarPath("backups", container.Name()) + newBackupSymlink := shared.VarPath("backups", newName) + if shared.PathExists(oldBackupSymlink) { + err := os.Remove(oldBackupSymlink) + if err != nil { + return err + } + + // Create the new backup symlink: + // ${LXD_DIR}/backups/<new_container_name> -> ${POOL}/backups/<new_container_name> + err = os.Symlink(newBackupsMntPoint, newBackupSymlink) + if err != nil { + return err + } + } + logger.Debugf("Renamed DIR storage volume for container \"%s\" from %s -> %s.", s.volume.Name, s.volume.Name, newName) return nil } @@ -1012,14 +1060,194 @@ func (s *storageDir) ContainerSnapshotStop(container container) (bool, error) { } func (s *storageDir) ContainerBackupCreate(backup backup, sourceContainer container) error { + logger.Debugf("Creating DIR storage volume for backup \"%s\" on storage pool \"%s\".", + backup.Name(), s.pool.Name) + + _, err := s.StoragePoolMount() + if err != nil { + return err + } + + // Create the path for the backup. + baseMntPoint := getBackupMountPoint(s.pool.Name, backup.Name()) + targetBackupContainerMntPoint := fmt.Sprintf("%s/container", + baseMntPoint) + targetBackupSnapshotsMntPoint := fmt.Sprintf("%s/snapshots", + baseMntPoint) + + err = os.MkdirAll(targetBackupContainerMntPoint, 0711) + if err != nil { + return err + } + + if !backup.ContainerOnly() { + // Create path for snapshots as well. + err = os.MkdirAll(targetBackupSnapshotsMntPoint, 0711) + if err != nil { + return err + } + } + + rsync := func(oldPath string, newPath string, bwlimit string) error { + output, err := rsyncLocalCopy(oldPath, newPath, bwlimit) + if err != nil { + s.ContainerBackupDelete(backup.Name()) + return fmt.Errorf("failed to rsync: %s: %s", string(output), err) + } + return nil + } + + ourStart, err := sourceContainer.StorageStart() + if err != nil { + return err + } + if ourStart { + defer sourceContainer.StorageStop() + } + + _, sourcePool, _ := sourceContainer.Storage().GetContainerPoolInfo() + sourceContainerMntPoint := getContainerMountPoint(sourcePool, + sourceContainer.Name()) + bwlimit := s.pool.Config["rsync.bwlimit"] + err = rsync(sourceContainerMntPoint, targetBackupContainerMntPoint, bwlimit) + if err != nil { + return err + } + + if sourceContainer.IsRunning() { + // This is done to ensure consistency when snapshotting. But we + // probably shouldn't fail just because of that. + logger.Debugf("Trying to freeze and rsync again to ensure consistency.") + + err := sourceContainer.Freeze() + if err != nil { + logger.Errorf("Trying to freeze and rsync again failed.") + goto onSuccess + } + defer sourceContainer.Unfreeze() + + err = rsync(sourceContainerMntPoint, targetBackupContainerMntPoint, bwlimit) + if err != nil { + return err + } + } + + if !backup.ContainerOnly() { + // Backup snapshots as well. + snaps, err := sourceContainer.Snapshots() + if err != nil { + return nil + } + + for _, ct := range snaps { + snapshotMntPoint := getSnapshotMountPoint(sourcePool, + ct.Name()) + err = rsync(snapshotMntPoint, targetBackupSnapshotsMntPoint, bwlimit) + if err != nil { + return err + } + } + } + +onSuccess: + // Check if the symlink + // ${LXD_DIR}/backups/<backup_name> -> ${POOL_PATH}/backups/<backup_name> + // exists and if not create it. + backupSymlink := shared.VarPath("backups", sourceContainer.Name()) + backupSymlinkTarget := getBackupMountPoint(sourcePool, sourceContainer.Name()) + if !shared.PathExists(backupSymlink) { + err = os.Symlink(backupSymlinkTarget, backupSymlink) + if err != nil { + return err + } + } + + logger.Debugf("Created DIR storage volume for backup \"%s\" on storage pool \"%s\".", + backup.Name(), s.pool.Name) return nil } func (s *storageDir) ContainerBackupDelete(name string) error { + logger.Debugf("Deleting DIR storage volume for backup \"%s\" on storage pool \"%s\".", + name, s.pool.Name) + + _, err := s.StoragePoolMount() + if err != nil { + return err + } + + source := s.pool.Config["source"] + if source == "" { + return fmt.Errorf("no \"source\" property found for the storage pool") + } + + err = dirBackupDeleteInternal(s.pool.Name, name) + if err != nil { + return err + } + + logger.Debugf("Deleted DIR storage volume for backup \"%s\" on storage pool \"%s\".", + name, s.pool.Name) + return nil +} + +func dirBackupDeleteInternal(poolName string, backupName string) error { + backupContainerMntPoint := getBackupMountPoint(poolName, backupName) + if shared.PathExists(backupContainerMntPoint) { + err := os.RemoveAll(backupContainerMntPoint) + if err != nil { + return err + } + } + + sourceContainerName, _, _ := containerGetParentAndSnapshotName(backupName) + backupContainerPath := getBackupMountPoint(poolName, sourceContainerName) + empty, _ := shared.PathIsEmpty(backupContainerPath) + if empty == true { + err := os.Remove(backupContainerPath) + if err != nil { + return err + } + + backupSymlink := shared.VarPath("backups", sourceContainerName) + if shared.PathExists(backupSymlink) { + err := os.Remove(backupSymlink) + if err != nil { + return err + } + } + } + return nil } func (s *storageDir) ContainerBackupRename(backup backup, newName string) error { + logger.Debugf("Renaming DIR storage volume for backup \"%s\" from %s -> %s.", + backup.Name(), backup.Name(), newName) + + _, err := s.StoragePoolMount() + if err != nil { + return err + } + + source := s.pool.Config["source"] + if source == "" { + return fmt.Errorf("no \"source\" property found for the storage pool") + } + + oldBackupMntPoint := getBackupMountPoint(s.pool.Name, backup.Name()) + newBackupMntPoint := getBackupMountPoint(s.pool.Name, newName) + + // Rename directory + if shared.PathExists(oldBackupMntPoint) { + err := os.Rename(oldBackupMntPoint, newBackupMntPoint) + if err != nil { + return err + } + } + + logger.Debugf("Renamed DIR storage volume for backup \"%s\" from %s -> %s.", + backup.Name(), backup.Name(), newName) return nil } From d210ea2ed33acf909cbb398e4107d5063454746b Mon Sep 17 00:00:00 2001 From: Thomas Hipp <thomas.h...@canonical.com> Date: Mon, 16 Apr 2018 18:11:44 +0200 Subject: [PATCH 4/4] lxd: Implement backups for LVM storage Signed-off-by: Thomas Hipp <thomas.h...@canonical.com> --- lxd/storage_lvm.go | 268 +++++++++++++++++++++++++++++++++++++++++++ lxd/storage_lvm_utils.go | 32 ++++++ lxd/storage_volumes_utils.go | 1 + 3 files changed, 301 insertions(+) diff --git a/lxd/storage_lvm.go b/lxd/storage_lvm.go index 214ee3bc8..4748cf1a0 100644 --- a/lxd/storage_lvm.go +++ b/lxd/storage_lvm.go @@ -1068,6 +1068,67 @@ func (s *storageLvm) ContainerDelete(container container) error { return err } + if container.IsSnapshot() { + // Snapshots will return a empty list when calling Backups(). We need to + // find the correct backup by iterating over the container's backups. + ctName, snapshotName, _ := containerGetParentAndSnapshotName(container.Name()) + ct, err := containerLoadByName(s.s, ctName) + if err != nil { + return err + } + + backups, err := ct.Backups() + if err != nil { + return err + } + + for _, backup := range backups { + if backup.ContainerOnly() { + // Skip container-only backups since they don't include + // snapshots + continue + } + + parts := strings.Split(backup.Name(), "/") + err := s.ContainerBackupDelete(fmt.Sprintf("%s/%s/%s", ctName, + snapshotName, parts[1])) + if err != nil { + return err + } + } + } else { + backups, err := container.Backups() + if err != nil { + return err + } + + for _, backup := range backups { + err := s.ContainerBackupDelete(backup.Name()) + if err != nil { + return err + } + continue + + if !backup.ContainerOnly() { + // Remove the snapshots + snapshots, err := container.Snapshots() + if err != nil { + return err + } + + for _, snap := range snapshots { + ctName, snapshotName, _ := containerGetParentAndSnapshotName(snap.Name()) + parts := strings.Split(backup.Name(), "/") + err := s.ContainerBackupDelete(fmt.Sprintf("%s/%s/%s", ctName, + snapshotName, parts[1])) + if err != nil { + return err + } + } + } + } + } + logger.Debugf("Deleted LVM storage volume for container \"%s\" on storage pool \"%s\".", s.volume.Name, s.pool.Name) return nil } @@ -1308,6 +1369,43 @@ func (s *storageLvm) ContainerRename(container container, newContainerName strin } } + // Rename backups + if !container.IsSnapshot() { + oldBackupPath := getBackupMountPoint(s.pool.Name, oldName) + newBackupPath := getBackupMountPoint(s.pool.Name, newContainerName) + if shared.PathExists(oldBackupPath) { + err = os.Rename(oldBackupPath, newBackupPath) + if err != nil { + return err + } + } + + oldBackupSymlink := shared.VarPath("backups", oldName) + newBackupSymlink := shared.VarPath("backups", newContainerName) + if shared.PathExists(oldBackupSymlink) { + err := os.Remove(oldBackupSymlink) + if err != nil { + return err + } + + err = os.Symlink(newBackupPath, newBackupSymlink) + if err != nil { + return err + } + } + } + + backups, err := container.Backups() + if err != nil { + return err + } + + for _, backup := range backups { + backupName := strings.Split(backup.Name(), "/")[1] + newName := fmt.Sprintf("%s/%s", newContainerName, backupName) + s.ContainerBackupRename(backup, newName) + } + tryUndo = false logger.Debugf("Renamed LVM storage volume for container \"%s\" from %s -> %s.", s.volume.Name, s.volume.Name, newContainerName) @@ -1562,14 +1660,184 @@ func (s *storageLvm) ContainerSnapshotCreateEmpty(snapshotContainer container) e } func (s *storageLvm) ContainerBackupCreate(backup backup, sourceContainer container) error { + logger.Debugf("Creating LVM storage volume for backup \"%s\" on storage pool \"%s\".", s.volume.Name, s.pool.Name) + + // Create the path for the backup. + baseMntPoint := getBackupMountPoint(s.pool.Name, backup.Name()) + targetBackupContainerMntPoint := fmt.Sprintf("%s/container", + baseMntPoint) + targetBackupSnapshotsMntPoint := fmt.Sprintf("%s/snapshots", + baseMntPoint) + + err := os.MkdirAll(targetBackupContainerMntPoint, 0711) + if err != nil { + return err + } + + if !backup.ContainerOnly() { + // Create path for snapshots as well. + err = os.MkdirAll(targetBackupSnapshotsMntPoint, 0711) + if err != nil { + return err + } + } + + err = s.createContainerBackup(backup, sourceContainer, true) + if err != nil { + return err + } + + if !backup.ContainerOnly() { + // Backup snapshots + snaps, err := sourceContainer.Snapshots() + if err != nil { + return nil + } + + for _, ct := range snaps { + err = s.createContainerBackup(backup, ct, true) + if err != nil { + return err + } + + // Create path to snapshot + _, snapName, _ := containerGetParentAndSnapshotName(ct.Name()) + err = os.MkdirAll(fmt.Sprintf("%s/%s", targetBackupSnapshotsMntPoint, + snapName), 0711) + if err != nil { + return err + } + } + } + + backupSymlink := shared.VarPath("backups", sourceContainer.Name()) + backupSymlinkTarget := getBackupMountPoint(s.pool.Name, sourceContainer.Name()) + if !shared.PathExists(backupSymlink) { + err = os.Symlink(backupSymlinkTarget, backupSymlink) + if err != nil { + return err + } + } + + logger.Debugf("Created LVM storage volume for backup \"%s\" on storage pool \"%s\".", s.volume.Name, s.pool.Name) return nil } func (s *storageLvm) ContainerBackupDelete(name string) error { + lvName := containerNameToLVName(name) + poolName := s.getOnDiskPoolName() + + containerLvmDevPath := getLvmDevPath(poolName, + storagePoolVolumeAPIEndpointBackups, lvName) + + lvExists, _ := storageLVExists(containerLvmDevPath) + if lvExists { + err := removeLV(poolName, storagePoolVolumeAPIEndpointBackups, lvName) + if err != nil { + return err + } + } + + lvmBackupDeleteInternal(poolName, name) + return nil } +func lvmBackupDeleteInternal(poolName string, backupName string) error { + backupContainerMntPoint := getBackupMountPoint(poolName, backupName) + if shared.PathExists(backupContainerMntPoint) { + err := os.RemoveAll(backupContainerMntPoint) + if err != nil { + return err + } + } + + sourceContainerName, _, _ := containerGetParentAndSnapshotName(backupName) + backupContainerPath := getBackupMountPoint(poolName, sourceContainerName) + empty, _ := shared.PathIsEmpty(backupContainerPath) + if empty == true { + err := os.Remove(backupContainerPath) + if err != nil { + return err + } + + backupSymlink := shared.VarPath("backups", sourceContainerName) + if shared.PathExists(backupSymlink) { + err := os.Remove(backupSymlink) + if err != nil { + return err + } + } + } + + return nil +} func (s *storageLvm) ContainerBackupRename(backup backup, newName string) error { + logger.Debugf("Renaming LVM storage volume for backup \"%s\" from %s -> %s.", + s.volume.Name, s.volume.Name, newName) + + tryUndo := true + + oldName := backup.Name() + oldLvmName := containerNameToLVName(oldName) + newLvmName := containerNameToLVName(newName) + + err := s.renameLVByPath(oldLvmName, newLvmName, storagePoolVolumeAPIEndpointBackups) + if err != nil { + return fmt.Errorf("Failed to rename a backup LV, oldName='%s', newName='%s', err='%s'", + oldLvmName, newLvmName, err) + } + defer func() { + if tryUndo { + s.renameLVByPath(newLvmName, oldLvmName, storagePoolVolumeAPIEndpointBackups) + } + }() + + if !backup.ContainerOnly() { + // Rename snapshot backups if they exist + snaps, err := backup.container.Snapshots() + if err != nil { + return err + } + + for _, snap := range snaps { + ctName, snapshotName, _ := containerGetParentAndSnapshotName(snap.Name()) + oldName := strings.Split(backup.Name(), "/")[1] + newName := strings.Split(newName, "/")[1] + oldLvmName := containerNameToLVName(fmt.Sprintf("%s/%s/%s", ctName, + snapshotName, oldName)) + newLvmName := containerNameToLVName(fmt.Sprintf("%s/%s/%s", ctName, + snapshotName, newName)) + + exists, err := storageLVExists(oldLvmName) + if err != nil { + return err + } + + if exists { + err := s.renameLVByPath(oldLvmName, newLvmName, + storagePoolVolumeAPIEndpointBackups) + if err != nil { + return err + } + } + } + } + + oldBackupMntPoint := getBackupMountPoint(s.pool.Name, oldName) + newBackupMntPoint := getBackupMountPoint(s.pool.Name, newName) + + if shared.PathExists(oldBackupMntPoint) { + err = os.Rename(oldBackupMntPoint, newBackupMntPoint) + if err != nil { + return err + } + } + + tryUndo = false + + logger.Debugf("Renamed LVM storage volume for backup \"%s\" from %s -> %s.", + s.volume.Name, s.volume.Name, newName) return nil } diff --git a/lxd/storage_lvm_utils.go b/lxd/storage_lvm_utils.go index 1bd244042..97e5929e3 100644 --- a/lxd/storage_lvm_utils.go +++ b/lxd/storage_lvm_utils.go @@ -280,6 +280,38 @@ func (s *storageLvm) createSnapshotContainer(snapshotContainer container, source return nil } +func (s *storageLvm) createContainerBackup(backup backup, sourceContainer container, + readonly bool) error { + tryUndo := true + + sourceContainerName := sourceContainer.Name() + sourceContainerLvmName := containerNameToLVName(sourceContainerName) + + // Always add the backup name as suffix, e.g. container-backup0 or + // container-snap1-backup1. + names := strings.Split(backup.Name(), "/") + targetContainerLvmName := fmt.Sprintf("%s-%s", sourceContainerLvmName, + names[len(names)-1]) + + poolName := s.getOnDiskPoolName() + _, err := s.createSnapshotLV(poolName, sourceContainerLvmName, + storagePoolVolumeAPIEndpointContainers, targetContainerLvmName, + storagePoolVolumeAPIEndpointBackups, readonly, s.useThinpool) + if err != nil { + return fmt.Errorf("Error creating snapshot LV: %s", err) + } + + defer func() { + if tryUndo { + s.ContainerBackupDelete(backup.Name()) + } + }() + + tryUndo = false + + return nil +} + // Copy a container on a storage pool that does use a thinpool. func (s *storageLvm) copyContainerThinpool(target container, source container, readonly bool) error { err := s.createSnapshotContainer(target, source, readonly) diff --git a/lxd/storage_volumes_utils.go b/lxd/storage_volumes_utils.go index d7e4a9ef6..861dc5442 100644 --- a/lxd/storage_volumes_utils.go +++ b/lxd/storage_volumes_utils.go @@ -33,6 +33,7 @@ const ( storagePoolVolumeAPIEndpointContainers string = "containers" storagePoolVolumeAPIEndpointImages string = "images" storagePoolVolumeAPIEndpointCustom string = "custom" + storagePoolVolumeAPIEndpointBackups string = "backups" ) var supportedVolumeTypes = []int{storagePoolVolumeTypeContainer, storagePoolVolumeTypeImage, storagePoolVolumeTypeCustom}
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel