[lxc-devel] [lxd/master] [WIP] Container backups
monstermunchkin on Github
lxc-bot at linuxcontainers.org
Mon Apr 16 19:05:56 UTC 2018
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 1064 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180416/87ece2e2/attachment.bin>
-------------- next part --------------
From 81e84e9b3165d340b36ea03b72c8cd7437c43446 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at 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.hipp at 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.hipp at 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.hipp at 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.hipp at 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.hipp at 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.hipp at 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.hipp at 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}
More information about the lxc-devel
mailing list