[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