[lxc-devel] [lxd/master] Add support for on-pool backups/images files

stgraber on Github lxc-bot at linuxcontainers.org
Wed Aug 14 13:03:04 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 314 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190814/2c1cbe9a/attachment.bin>
-------------- next part --------------
From b101362e97c2e41b94a5517cea5d283dc7288516 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 12 Aug 2019 00:14:38 -0400
Subject: [PATCH 1/5] api: Add daemon_storage extension
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/api-extensions.md | 5 +++++
 shared/version/api.go | 1 +
 2 files changed, 6 insertions(+)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 06249be844..dc4ef31970 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -818,3 +818,8 @@ This makes use of shiftfs as an overlay filesystem.
 
 ## resources\_infiniband
 Export infiniband character device information (issm, umad, uverb) as part of the resources API.
+
+## daemon\_storage
+This introduces two new configuration keys `storage.images\_volume` and
+`storage.backups\_volume` to allow for a storage volume on an existing
+pool be used for storing the daemon-wide images and backups artifacts.
diff --git a/shared/version/api.go b/shared/version/api.go
index 0e3f9c892e..201e834828 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -163,6 +163,7 @@ var APIExtensions = []string{
 	"container_disk_shift",
 	"storage_shifted",
 	"resources_infiniband",
+	"daemon_storage",
 }
 
 // APIExtensionsCount returns the number of available API extensions.

From 4d37105c1a49315c82e01a351ffc9a2a56c19ff7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 12 Aug 2019 00:14:52 -0400
Subject: [PATCH 2/5] doc: Add daemon storage keys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/server.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/doc/server.md b/doc/server.md
index 18628dd7c8..68d238c05c 100644
--- a/doc/server.md
+++ b/doc/server.md
@@ -46,6 +46,8 @@ rbac.agent.private\_key             | string    | global    | -         | rbac
 rbac.api.expiry                     | integer   | global    | -         | rbac                              | RBAC macaroon expiry in seconds
 rbac.api.key                        | string    | global    | -         | rbac                              | Public key of the RBAC server (required for HTTP-only servers)
 rbac.api.url                        | string    | global    | -         | rbac                              | URL of the external RBAC server
+storage.backups\_volume             | string    | local     | -         | daemon\_storage                   | Volume to use to store the backup tarballs (syntax is POOL/VOLUME)
+storage.images\_volume              | string    | local     | -         | daemon\_storage                   | Volume to use to store the image tarballs (syntax is POOL/VOLUME)
 
 Those keys can be set using the lxc tool with:
 

From 2366f593189df4802ac6bf30f5cba8167d195f86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 12 Aug 2019 00:15:32 -0400
Subject: [PATCH 3/5] bash: Add daemon storage keys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 scripts/bash/lxd-client | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/scripts/bash/lxd-client b/scripts/bash/lxd-client
index b8613c4e0c..6a78584688 100644
--- a/scripts/bash/lxd-client
+++ b/scripts/bash/lxd-client
@@ -75,7 +75,8 @@ _have lxc && {
       images.compression_algorithm images.remote_cache_expiry \
       maas.api.url maas.api.key maas.machine cluster.images_minimal_replica \
       rbac.agent.url rbac.agent.username rbac.agent.public_key \
-      rbac.agent.private_key rbac.api.expiry rbac.api.key rbac.api.url"
+      rbac.agent.private_key rbac.api.expiry rbac.api.key rbac.api.url \
+      storage.backups_volume storage.images_volume"
 
     container_keys="boot.autostart boot.autostart.delay \
       boot.autostart.priority boot.stop.priority \

From a50b8608f11bac5f6d72d17561c29eef78644ae6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 12 Aug 2019 00:27:28 -0400
Subject: [PATCH 4/5] lxd: Add daemon storage keys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/node/config.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/lxd/node/config.go b/lxd/node/config.go
index c67814c030..c478791faa 100644
--- a/lxd/node/config.go
+++ b/lxd/node/config.go
@@ -54,6 +54,16 @@ func (c *Config) MAASMachine() string {
 	return c.m.GetString("maas.machine")
 }
 
+// StorageBackupsVolume returns the name of the pool/volume to use for storing backup tarballs
+func (c *Config) StorageBackupsVolume() string {
+	return c.m.GetString("storage.backups_volume")
+}
+
+// StorageImagesVolume returns the name of the pool/volume to use for storing image tarballs
+func (c *Config) StorageImagesVolume() string {
+	return c.m.GetString("storage.images_volume")
+}
+
 // Dump current configuration keys and their values. Keys with values matching
 // their defaults are omitted.
 func (c *Config) Dump() map[string]interface{} {
@@ -150,4 +160,8 @@ var ConfigSchema = config.Schema{
 
 	// MAAS machine this LXD instance is associated with
 	"maas.machine": {},
+
+	// Storage volumes to store backups/images on
+	"storage.backups_volume": {},
+	"storage.images_volume":  {},
 }

From 29a065245b7529ba20f941d512c301101f879d13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 13 Aug 2019 22:45:57 -0400
Subject: [PATCH 5/5] lxd/daemon: Support storing images/backups on pool
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/api_1.0.go                  |  37 +++-
 lxd/daemon.go                   |   6 +
 lxd/daemon_storage.go           | 348 ++++++++++++++++++++++++++++++++
 lxd/storage_volumes.go          |  19 +-
 lxd/storage_volumes_snapshot.go |  14 +-
 lxd/storage_volumes_utils.go    |  10 +
 6 files changed, 426 insertions(+), 8 deletions(-)
 create mode 100644 lxd/daemon_storage.go

diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index c794c2ff6f..0b6e1a9db4 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -321,6 +321,8 @@ func api10Patch(d *Daemon, r *http.Request) Response {
 }
 
 func doApi10Update(d *Daemon, req api.ServerPut, patch bool) Response {
+	s := d.State()
+
 	// First deal with config specific to the local daemon
 	nodeValues := map[string]interface{}{}
 
@@ -350,6 +352,21 @@ func doApi10Update(d *Daemon, req api.ServerPut, patch bool) Response {
 			return fmt.Errorf("Changing cluster.https_address is currently not supported")
 		}
 
+		// Validate the storage volumes
+		if nodeValues["storage.backups_volume"] != nil && nodeValues["storage.backups_volume"] != newNodeConfig.StorageBackupsVolume() {
+			err := daemonStorageValidate(s, nodeValues["storage.backups_volume"].(string))
+			if err != nil {
+				return err
+			}
+		}
+
+		if nodeValues["storage.images_volume"] != nil && nodeValues["storage.images_volume"] != newNodeConfig.StorageImagesVolume() {
+			err := daemonStorageValidate(s, nodeValues["storage.images_volume"].(string))
+			if err != nil {
+				return err
+			}
+		}
+
 		if patch {
 			nodeChanged, err = newNodeConfig.Patch(nodeValues)
 		} else {
@@ -411,7 +428,7 @@ func doApi10Update(d *Daemon, req api.ServerPut, patch bool) Response {
 	}
 
 	// Notify the other nodes about changes
-	notifier, err := cluster.NewNotifier(d.State(), d.endpoints.NetworkCert(), cluster.NotifyAlive)
+	notifier, err := cluster.NewNotifier(s, d.endpoints.NetworkCert(), cluster.NotifyAlive)
 	if err != nil {
 		return SmartError(err)
 	}
@@ -442,6 +459,8 @@ func doApi10Update(d *Daemon, req api.ServerPut, patch bool) Response {
 }
 
 func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]string, nodeConfig *node.Config, clusterConfig *cluster.Config) error {
+	s := d.State()
+
 	maasChanged := false
 	candidChanged := false
 	rbacChanged := false
@@ -525,6 +544,22 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
 		}
 	}
 
+	value, ok = nodeChanged["storage.backups_volume"]
+	if ok {
+		err := daemonStorageMove(s, "backups", value)
+		if err != nil {
+			return err
+		}
+	}
+
+	value, ok = nodeChanged["storage.images_volume"]
+	if ok {
+		err := daemonStorageMove(s, "images", value)
+		if err != nil {
+			return err
+		}
+	}
+
 	if maasChanged {
 		url, key := clusterConfig.MAASController()
 		machine := nodeConfig.MAASMachine()
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 64b2abb3de..0aad1da9f9 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -777,6 +777,12 @@ func (d *Daemon) init() error {
 		return err
 	}
 
+	// Mount any daemon storage
+	err = daemonStorageMount(d.State())
+	if err != nil {
+		return err
+	}
+
 	/* Apply all patches */
 	err = patchesApplyAll(d)
 	if err != nil {
diff --git a/lxd/daemon_storage.go b/lxd/daemon_storage.go
new file mode 100644
index 0000000000..ef55306190
--- /dev/null
+++ b/lxd/daemon_storage.go
@@ -0,0 +1,348 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	"github.com/lxc/lxd/lxd/db"
+	"github.com/lxc/lxd/lxd/node"
+	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/logger"
+)
+
+func daemonStorageMount(s *state.State) error {
+	var storageBackups string
+	var storageImages string
+	err := s.Node.Transaction(func(tx *db.NodeTx) error {
+		nodeConfig, err := node.ConfigLoad(tx)
+		if err != nil {
+			return err
+		}
+
+		storageBackups = nodeConfig.StorageBackupsVolume()
+		storageImages = nodeConfig.StorageImagesVolume()
+
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	mount := func(storageType string, source string) error {
+		// Parse the source
+		fields := strings.Split(source, "/")
+		if len(fields) != 2 {
+			return fmt.Errorf("Invalid syntax for volume, must be <pool>/<volume>")
+		}
+
+		poolName := fields[0]
+		volumeName := fields[1]
+
+		// Mount volume
+		volume, err := storageInit(s, "default", poolName, volumeName, storagePoolVolumeTypeCustom)
+		if err != nil {
+			return errors.Wrapf(err, "Unable to load storage volume \"%s\"", source)
+		}
+
+		_, err = volume.StoragePoolVolumeMount()
+		if err != nil {
+			return errors.Wrapf(err, "Failed to mount storage volume \"%s\"", source)
+		}
+
+		return nil
+	}
+
+	if storageBackups != "" {
+		err := mount("backups", storageBackups)
+		if err != nil {
+			return errors.Wrap(err, "Failed to mount backups storage")
+		}
+	}
+
+	if storageImages != "" {
+		err := mount("images", storageImages)
+		if err != nil {
+			return errors.Wrap(err, "Failed to mount images storage")
+		}
+	}
+
+	return nil
+}
+
+func daemonStorageUsed(s *state.State, poolName string, volumeName string) (bool, error) {
+	var storageBackups string
+	var storageImages string
+	err := s.Node.Transaction(func(tx *db.NodeTx) error {
+		nodeConfig, err := node.ConfigLoad(tx)
+		if err != nil {
+			return err
+		}
+
+		storageBackups = nodeConfig.StorageBackupsVolume()
+		storageImages = nodeConfig.StorageImagesVolume()
+
+		return nil
+	})
+	if err != nil {
+		return false, err
+	}
+
+	fullName := fmt.Sprintf("%s/%s", poolName, volumeName)
+	if storageBackups == fullName || storageImages == fullName {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+func daemonStorageValidate(s *state.State, target string) error {
+	// Check syntax
+	if target == "" {
+		return nil
+	}
+
+	fields := strings.Split(target, "/")
+	if len(fields) != 2 {
+		return fmt.Errorf("Invalid syntax for volume, must be <pool>/<volume>")
+	}
+
+	poolName := fields[0]
+	volumeName := fields[1]
+
+	// Validate pool exists
+	poolID, pool, err := s.Cluster.StoragePoolGet(poolName)
+	if err != nil {
+		return errors.Wrapf(err, "Unable to load storage pool \"%s\"", poolName)
+	}
+
+	// Validate pool driver (can't be CEPH or CEPHFS)
+	if pool.Driver == "ceph" || pool.Driver == "cephfs" {
+		return fmt.Errorf("Server storage volumes cannot be stored on Ceph")
+	}
+
+	// Confirm volume exists
+	volume, err := storageInit(s, "default", poolName, volumeName, storagePoolVolumeTypeCustom)
+	if err != nil {
+		return errors.Wrapf(err, "Unable to load storage volume \"%s\"", target)
+	}
+
+	snapshots, err := s.Cluster.StoragePoolVolumeSnapshotsGetType(volumeName, storagePoolVolumeTypeCustom, poolID)
+	if err != nil {
+		return errors.Wrapf(err, "Unable to load storage volume snapshots \"%s\"", target)
+	}
+
+	if len(snapshots) != 0 {
+		return fmt.Errorf("Storage volumes for use by LXD itself cannot have snapshots")
+	}
+
+	// Mount volume
+	ourMount, err := volume.StoragePoolVolumeMount()
+	if err != nil {
+		return errors.Wrapf(err, "Failed to mount storage volume \"%s\"", target)
+	}
+	if ourMount {
+		defer volume.StoragePoolUmount()
+	}
+
+	// Validate volume is empty (ignore lost+found)
+	mountpoint := shared.VarPath("storage-pools", poolName, "custom", volumeName)
+
+	entries, err := ioutil.ReadDir(mountpoint)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to list \"%s\"", mountpoint)
+	}
+
+	for _, entry := range entries {
+		entryName := entry.Name()
+
+		if entryName == "lost+found" {
+			continue
+		}
+
+		return fmt.Errorf("Storage volume \"%s\" isn't empty", target)
+	}
+
+	return nil
+}
+
+func daemonStorageMove(s *state.State, storageType string, target string) error {
+	logger.Errorf("stgraber: daemonStorageMove for %s at %s", storageType, target)
+	destPath := shared.VarPath(storageType)
+
+	// Track down the current storage
+	var sourcePool string
+	var sourceVolume string
+
+	sourcePath, err := os.Readlink(destPath)
+	if err != nil {
+		sourcePath = destPath
+	} else {
+		fields := strings.Split(sourcePath, "/")
+		sourcePool = fields[len(fields)-3]
+		sourceVolume = fields[len(fields)-1]
+	}
+
+	moveContent := func(source string, target string) error {
+		// Copy the content
+		_, err := rsyncLocalCopy(source, target, "", false)
+		if err != nil {
+			return err
+		}
+
+		// Remove the source content
+		entries, err := ioutil.ReadDir(source)
+		if err != nil {
+			return err
+		}
+
+		for _, entry := range entries {
+			err := os.RemoveAll(filepath.Join(source, entry.Name()))
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	}
+
+	// Deal with unsetting
+	if target == "" {
+		// Things already look correct
+		if sourcePath == destPath {
+			return nil
+		}
+
+		// Remove the symlink
+		err = os.Remove(destPath)
+		if err != nil {
+			return errors.Wrapf(err, "Failed to delete storage symlink at \"%s\"", destPath)
+		}
+
+		// Re-create as a directory
+		err = os.MkdirAll(destPath, 0700)
+		if err != nil {
+			return errors.Wrapf(err, "Failed to create directory \"%s\"", destPath)
+		}
+
+		// Move the data across
+		err = moveContent(sourcePath, destPath)
+		if err != nil {
+			return errors.Wrapf(err, "Failed to move data over to directory \"%s\"", destPath)
+		}
+
+		// Unmount old volume
+		volume, err := storageInit(s, "default", sourcePool, sourceVolume, storagePoolVolumeTypeCustom)
+		if err != nil {
+			return errors.Wrapf(err, "Unable to load storage volume \"%s/%s\"", sourcePool, sourceVolume)
+		}
+
+		_, err = volume.StoragePoolVolumeUmount()
+		if err != nil {
+			return errors.Wrapf(err, "Failed to umount storage volume \"%s/%s\"", sourcePool, sourceVolume)
+		}
+
+		return nil
+	}
+
+	// Parse the target
+	fields := strings.Split(target, "/")
+	if len(fields) != 2 {
+		return fmt.Errorf("Invalid syntax for volume, must be <pool>/<volume>")
+	}
+
+	poolName := fields[0]
+	volumeName := fields[1]
+
+	// Mount volume
+	volume, err := storageInit(s, "default", poolName, volumeName, storagePoolVolumeTypeCustom)
+	if err != nil {
+		return errors.Wrapf(err, "Unable to load storage volume \"%s\"", target)
+	}
+
+	_, err = volume.StoragePoolVolumeMount()
+	if err != nil {
+		return errors.Wrapf(err, "Failed to mount storage volume \"%s\"", target)
+	}
+
+	// Set ownership & mode
+	mountpoint := shared.VarPath("storage-pools", poolName, "custom", volumeName)
+	destPath = mountpoint
+
+	err = os.Chmod(mountpoint, 0700)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to set permissions on \"%s\"", mountpoint)
+	}
+
+	err = os.Chown(mountpoint, 0, 0)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to set ownership on \"%s\"", mountpoint)
+	}
+
+	// Handle changes
+	if sourcePath != shared.VarPath(storageType) {
+		// Remove the symlink
+		err := os.Remove(shared.VarPath(storageType))
+		if err != nil {
+			return errors.Wrapf(err, "Failed to remove the new symlink at \"%s\"", shared.VarPath(storageType))
+		}
+
+		// Create the new symlink
+		err = os.Symlink(destPath, shared.VarPath(storageType))
+		if err != nil {
+			return errors.Wrapf(err, "Failed to create the new symlink at \"%s\"", shared.VarPath(storageType))
+		}
+
+		// Move the data across
+		err = moveContent(sourcePath, destPath)
+		if err != nil {
+			return errors.Wrapf(err, "Failed to move data over to directory \"%s\"", destPath)
+		}
+
+		// Unmount old volume
+		volume, err := storageInit(s, "default", sourcePool, sourceVolume, storagePoolVolumeTypeCustom)
+		if err != nil {
+			return errors.Wrapf(err, "Unable to load storage volume \"%s/%s\"", sourcePool, sourceVolume)
+		}
+
+		_, err = volume.StoragePoolVolumeUmount()
+		if err != nil {
+			return errors.Wrapf(err, "Failed to umount storage volume \"%s/%s\"", sourcePool, sourceVolume)
+		}
+
+		return nil
+	}
+
+	sourcePath = shared.VarPath(storageType) + ".temp"
+
+	// Rename the existing storage
+	err = os.Rename(shared.VarPath(storageType), sourcePath)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to rename existing storage \"%s\"", shared.VarPath(storageType))
+	}
+
+	// Create the new symlink
+	err = os.Symlink(destPath, shared.VarPath(storageType))
+	if err != nil {
+		return errors.Wrapf(err, "Failed to create the new symlink at \"%s\"", shared.VarPath(storageType))
+	}
+
+	// Move the data across
+	err = moveContent(sourcePath, destPath)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to move data over to directory \"%s\"", destPath)
+	}
+
+	// Remove the old data
+	err = os.RemoveAll(sourcePath)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to cleanup old directory \"%s\"", sourcePath)
+	}
+
+	return nil
+}
diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go
index 38250235d0..ce02541471 100644
--- a/lxd/storage_volumes.go
+++ b/lxd/storage_volumes.go
@@ -452,7 +452,7 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request, volumeTypeName string
 		// Handle volume
 		volumeName = fields[0]
 	} else {
-		return BadRequest(fmt.Errorf("invalid storage volume %s", mux.Vars(r)["name"]))
+		return BadRequest(fmt.Errorf("Invalid storage volume %s", mux.Vars(r)["name"]))
 	}
 
 	// Get the name of the storage pool the volume is supposed to be
@@ -560,8 +560,7 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request, volumeTypeName string
 	}
 
 	// Check that the name isn't already in use.
-	_, err = d.cluster.StoragePoolNodeVolumeGetTypeID(req.Name,
-		storagePoolVolumeTypeCustom, poolID)
+	_, err = d.cluster.StoragePoolNodeVolumeGetTypeID(req.Name, storagePoolVolumeTypeCustom, poolID)
 	if err != db.ErrNoSuchObject {
 		if err != nil {
 			return InternalError(err)
@@ -571,6 +570,17 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request, volumeTypeName string
 	}
 
 	doWork := func() error {
+		// Check if the daemon itself is using it
+		used, err := daemonStorageUsed(d.State(), poolName, volumeName)
+		if err != nil {
+			return err
+		}
+
+		if used {
+			return fmt.Errorf("Volume is used by LXD itself and cannot be renamed")
+		}
+
+		// Check if a running container is using it
 		ctsUsingVolume, err := storagePoolVolumeUsedByRunningContainersWithProfilesGet(d.State(), poolName, volumeName, storagePoolVolumeTypeNameCustom, true)
 		if err != nil {
 			return err
@@ -1007,8 +1017,7 @@ func storagePoolVolumeTypeDelete(d *Daemon, r *http.Request, volumeTypeName stri
 				"/%s/images/%s",
 				version.APIVersion,
 				volumeName) {
-			return BadRequest(fmt.Errorf(`The storage volume is ` +
-				`still in use by containers or profiles`))
+			return BadRequest(fmt.Errorf("The storage volume is still in use"))
 		}
 	}
 
diff --git a/lxd/storage_volumes_snapshot.go b/lxd/storage_volumes_snapshot.go
index 3b2b2cfa10..8e87a6e925 100644
--- a/lxd/storage_volumes_snapshot.go
+++ b/lxd/storage_volumes_snapshot.go
@@ -56,7 +56,7 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) Response {
 
 	// Check that the storage volume type is valid.
 	if !shared.IntInSlice(volumeType, supportedVolumeTypes) {
-		return BadRequest(fmt.Errorf("invalid storage volume type \"%d\"", volumeType))
+		return BadRequest(fmt.Errorf("Invalid storage volume type \"%d\"", volumeType))
 	}
 
 	// Get a snapshot name.
@@ -71,6 +71,16 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
+	// Check that this isn't a restricted volume
+	used, err := daemonStorageUsed(d.State(), poolName, volumeName)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	if used {
+		return BadRequest(fmt.Errorf("Volumes used by LXD itself cannot have snapshots"))
+	}
+
 	// Retrieve ID of the storage pool (and check if the storage pool
 	// exists).
 	poolID, err := d.cluster.StoragePoolGetID(poolName)
@@ -94,7 +104,7 @@ func storagePoolVolumeSnapshotsTypePost(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	// Ensure that it doens't already fucking exist
+	// Ensure that the snapshot doens't already exist
 	_, _, err = d.cluster.StoragePoolNodeVolumeGetType(fmt.Sprintf("%s/%s", volumeName, req.Name), volumeType, poolID)
 	if err != db.ErrNoSuchObject {
 		if err != nil {
diff --git a/lxd/storage_volumes_utils.go b/lxd/storage_volumes_utils.go
index 538b4704fe..f7d32012b0 100644
--- a/lxd/storage_volumes_utils.go
+++ b/lxd/storage_volumes_utils.go
@@ -447,6 +447,16 @@ func storagePoolVolumeUsedByGet(s *state.State, project, poolName string, volume
 		return []string{fmt.Sprintf("/%s/images/%s", version.APIVersion, volumeName)}, nil
 	}
 
+	// Check if the daemon itself is using it
+	used, err := daemonStorageUsed(s, poolName, volumeName)
+	if err != nil {
+		return []string{}, err
+	}
+
+	if used {
+		return []string{fmt.Sprintf("/%s", version.APIVersion)}, nil
+	}
+
 	// Look for containers using this volume
 	ctsUsingVolume, err := storagePoolVolumeUsedByContainersGet(s, project, poolName, volumeName)
 	if err != nil {


More information about the lxc-devel mailing list