[lxc-devel] [lxd/master] storage: remote {copy, move} custom storage volumes

brauner on Github lxc-bot at linuxcontainers.org
Fri Mar 23 20:57:59 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 364 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180323/099c34a0/attachment.bin>
-------------- next part --------------
From a9c1e9bd9a61be394cd2430a4d9e2e23b474bd79 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 20:21:40 +0100
Subject: [PATCH 1/6] api: remote storage volume migration

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 shared/api/storage_pool_volume.go | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/shared/api/storage_pool_volume.go b/shared/api/storage_pool_volume.go
index 72f9358d4..e407acd46 100644
--- a/shared/api/storage_pool_volume.go
+++ b/shared/api/storage_pool_volume.go
@@ -21,6 +21,21 @@ type StorageVolumePost struct {
 
 	// API extension: storage_api_local_volume_handling
 	Pool string `json:"pool,omitempty" yaml:"pool,omitempty"`
+
+	// API extension: storage_api_remote_volume_handling
+	Migration bool `json:"migration" yaml:"migration"`
+
+	// API extension: storage_api_remote_volume_handling
+	Target *StorageVolumePostTarget `json:"target" yaml:"target"`
+}
+
+// StorageVolumePostTarget represents the migration target host and operation
+//
+// API extension: storage_api_remote_volume_handling
+type StorageVolumePostTarget struct {
+	Certificate string            `json:"certificate" yaml:"certificate"`
+	Operation   string            `json:"operation,omitempty" yaml:"operation,omitempty"`
+	Websockets  map[string]string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
 }
 
 // StorageVolume represents the fields of a LXD storage volume.
@@ -53,6 +68,12 @@ type StorageVolumeSource struct {
 	Name string `json:"name" yaml:"name"`
 	Type string `json:"type" yaml:"type"`
 	Pool string `json:"pool" yaml:"pool"`
+
+	// API extension: storage_api_local_volume_handling
+	Certificate string            `json:"certificate" yaml:"certificate"`
+	Mode        string            `json:"mode,omitempty" yaml:"mode,omitempty"`
+	Operation   string            `json:"operation,omitempty" yaml:"operation,omitempty"`
+	Websockets  map[string]string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
 }
 
 // Writable converts a full StorageVolume struct into a StorageVolumePut struct

From 909db1ff992889e8153376582ab21cf0dc93ef1b Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 20:22:06 +0100
Subject: [PATCH 2/6] client: remote storage volume migration

Closes #3985.

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 client/interfaces.go          |   4 +
 client/lxd_storage_volumes.go | 279 +++++++++++++++++++++++++++++++++++++++---
 2 files changed, 268 insertions(+), 15 deletions(-)

diff --git a/client/interfaces.go b/client/interfaces.go
index 051ef104f..97e884f44 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -184,6 +184,7 @@ type ContainerServer interface {
 	RenameStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePost) (err error)
 	CopyStoragePoolVolume(pool string, source ContainerServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (op RemoteOperation, err error)
 	MoveStoragePoolVolume(pool string, source ContainerServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (op RemoteOperation, err error)
+	MigrateStoragePoolVolume(pool string, volume api.StorageVolumePost) (op Operation, err error)
 
 	// Cluster functions ("cluster" API extensions)
 	GetCluster() (cluster *api.Cluster, ETag string, err error)
@@ -279,6 +280,9 @@ type ImageCopyArgs struct {
 type StoragePoolVolumeCopyArgs struct {
 	// New name for the target
 	Name string
+
+	// The transfer mode, can be "pull" (default), "push" or "relay"
+	Mode string
 }
 
 // The StoragePoolVolumeMoveArgs struct is used to pass additional options
diff --git a/client/lxd_storage_volumes.go b/client/lxd_storage_volumes.go
index d44474bfd..389a38d25 100644
--- a/client/lxd_storage_volumes.go
+++ b/client/lxd_storage_volumes.go
@@ -94,16 +94,143 @@ func (r *ProtocolLXD) CreateStoragePoolVolume(pool string, volume api.StorageVol
 	return nil
 }
 
+// MigrateStoragePoolVolume requests that LXD prepares for a storage volume migration
+func (r *ProtocolLXD) MigrateStoragePoolVolume(pool string, volume api.StorageVolumePost) (Operation, error) {
+	// Sanity check
+	if !volume.Migration {
+		return nil, fmt.Errorf("Can't ask for a rename through MigrateStoragePoolVolume")
+	}
+
+	// Send the request
+	path := fmt.Sprintf("/storage-pools/%s/volumes/custom/%s", url.QueryEscape(pool), volume.Name)
+	if r.clusterTarget != "" {
+		path += fmt.Sprintf("?target=%s", r.clusterTarget)
+	}
+	op, _, err := r.queryOperation("POST", path, volume, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateStoragePoolVolume(source ContainerServer, pool string, req api.StorageVolumePost, urls []string) (RemoteOperation, error) {
+	if len(urls) == 0 {
+		return nil, fmt.Errorf("The source server isn't listening on the network")
+	}
+
+	rop := remoteOperation{
+		chDone: make(chan bool),
+	}
+
+	operation := req.Target.Operation
+
+	// Forward targetOp to remote op
+	go func() {
+		success := false
+		errors := []string{}
+		for _, serverURL := range urls {
+			req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.QueryEscape(operation))
+
+			// Send the request
+			top, err := source.MigrateStoragePoolVolume(pool, req)
+			if err != nil {
+				errors = append(errors, fmt.Sprintf("%s: %v", serverURL, err))
+				continue
+			}
+
+			rop := remoteOperation{
+				targetOp: top,
+				chDone:   make(chan bool),
+			}
+
+			for _, handler := range rop.handlers {
+				rop.targetOp.AddHandler(handler)
+			}
+
+			err = rop.targetOp.Wait()
+			if err != nil {
+				errors = append(errors, fmt.Sprintf("%s: %v", serverURL, err))
+				continue
+			}
+
+			success = true
+			break
+		}
+
+		if !success {
+			rop.err = fmt.Errorf("Failed storage volume creation:\n - %s", strings.Join(errors, "\n - "))
+		}
+
+		close(rop.chDone)
+	}()
+
+	return &rop, nil
+}
+
+func (r *ProtocolLXD) tryCreateStoragePoolVolume(pool string, req api.StorageVolumesPost, urls []string) (RemoteOperation, error) {
+	if len(urls) == 0 {
+		return nil, fmt.Errorf("The source server isn't listening on the network")
+	}
+
+	rop := remoteOperation{
+		chDone: make(chan bool),
+	}
+
+	operation := req.Source.Operation
+
+	// Forward targetOp to remote op
+	go func() {
+		success := false
+		errors := []string{}
+		for _, serverURL := range urls {
+			req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.QueryEscape(operation))
+
+			// Send the request
+			path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.QueryEscape(pool), url.QueryEscape(req.Type))
+			if r.clusterTarget != "" {
+				path += fmt.Sprintf("?target=%s", r.clusterTarget)
+			}
+			top, _, err := r.queryOperation("POST", path, req, "")
+			if err != nil {
+				continue
+			}
+
+			rop := remoteOperation{
+				targetOp: top,
+				chDone:   make(chan bool),
+			}
+
+			for _, handler := range rop.handlers {
+				rop.targetOp.AddHandler(handler)
+			}
+
+			err = rop.targetOp.Wait()
+			if err != nil {
+				errors = append(errors, fmt.Sprintf("%s: %v", serverURL, err))
+				continue
+			}
+
+			success = true
+			break
+		}
+
+		if !success {
+			rop.err = fmt.Errorf("Failed storage volume creation:\n - %s", strings.Join(errors, "\n - "))
+		}
+
+		close(rop.chDone)
+	}()
+
+	return &rop, nil
+}
+
 // CopyStoragePoolVolume copies an existing storage volume
 func (r *ProtocolLXD) CopyStoragePoolVolume(pool string, source ContainerServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (RemoteOperation, error) {
 	if !r.HasExtension("storage_api_local_volume_handling") {
 		return nil, fmt.Errorf("The server is missing the required \"storage_api_local_volume_handling\" API extension")
 	}
 
-	if r != source {
-		return nil, fmt.Errorf("Copying storage volumes between remotes is not implemented")
-	}
-
 	req := api.StorageVolumesPost{
 		Name: args.Name,
 		Type: volume.Type,
@@ -114,24 +241,146 @@ func (r *ProtocolLXD) CopyStoragePoolVolume(pool string, source ContainerServer,
 		},
 	}
 
-	// Send the request
-	op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/%s", url.QueryEscape(pool), url.QueryEscape(volume.Type)), req, "")
+	if r == source {
+		// Send the request
+		op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/%s", url.QueryEscape(pool), url.QueryEscape(volume.Type)), req, "")
+		if err != nil {
+			return nil, err
+		}
+
+		rop := remoteOperation{
+			targetOp: op,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	sourceReq := api.StorageVolumePost{
+		Migration: true,
+		Name:      volume.Name,
+		Pool:      sourcePool,
+	}
+
+	// Push mode migration
+	if args != nil && args.Mode == "push" {
+		// Get target server connection information
+		info, err := r.GetConnectionInfo()
+		if err != nil {
+			return nil, err
+		}
+
+		// Create the container
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+
+		// Send the request
+		path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.QueryEscape(pool), url.QueryEscape(volume.Type))
+		if r.clusterTarget != "" {
+			path += fmt.Sprintf("?target=%s", r.clusterTarget)
+		}
+
+		// Send the request
+		op, _, err := r.queryOperation("POST", path, req, "")
+		if err != nil {
+			return nil, err
+		}
+		opAPI := op.Get()
+
+		targetSecrets := map[string]string{}
+		for k, v := range opAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Prepare the source request
+		target := api.StorageVolumePostTarget{}
+		target.Operation = opAPI.ID
+		target.Websockets = targetSecrets
+		target.Certificate = info.Certificate
+		sourceReq.Target = &target
+
+		return r.tryMigrateStoragePoolVolume(source, sourcePool, sourceReq, info.Addresses)
+	}
+
+	// Get source server connection information
+	info, err := source.GetConnectionInfo()
 	if err != nil {
 		return nil, err
 	}
 
-	rop := remoteOperation{
-		targetOp: op,
-		chDone:   make(chan bool),
+	// Get secrets from source server
+	op, err := source.MigrateStoragePoolVolume(sourcePool, sourceReq)
+	if err != nil {
+		return nil, err
 	}
+	opAPI := op.Get()
 
-	// Forward targetOp to remote op
-	go func() {
-		rop.err = rop.targetOp.Wait()
-		close(rop.chDone)
-	}()
+	// Prepare source server secrets for remote
+	sourceSecrets := map[string]string{}
+	for k, v := range opAPI.Metadata {
+		sourceSecrets[k] = v.(string)
+	}
 
-	return &rop, nil
+	// Relay mode migration
+	if args != nil && args.Mode == "relay" {
+		// Push copy source fields
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+
+		// Send the request
+		path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.QueryEscape(pool), url.QueryEscape(volume.Type))
+		if r.clusterTarget != "" {
+			path += fmt.Sprintf("?target=%s", r.clusterTarget)
+		}
+
+		// Send the request
+		targetOp, _, err := r.queryOperation("POST", path, req, "")
+		if err != nil {
+			return nil, err
+		}
+		targetOpAPI := targetOp.Get()
+
+		// Extract the websockets
+		targetSecrets := map[string]string{}
+		for k, v := range targetOpAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Launch the relay
+		err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+		if err != nil {
+			return nil, err
+		}
+
+		// Prepare a tracking operation
+		rop := remoteOperation{
+			targetOp: targetOp,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Pull mode migration
+	req.Source.Type = "migration"
+	req.Source.Mode = "pull"
+	req.Source.Operation = opAPI.ID
+	req.Source.Websockets = sourceSecrets
+	req.Source.Certificate = info.Certificate
+
+	return r.tryCreateStoragePoolVolume(pool, req, info.Addresses)
 }
 
 // MoveStoragePoolVolume renames or moves an existing storage volume

From 00e1aea548896242fbdf05b91ec8cb9930633e4b Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 20:22:27 +0100
Subject: [PATCH 3/6] lxd/storage: remote storage volume migration

Closes #3985.

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 client/lxd_storage_volumes.go |   4 ++
 lxd/main_netcat.go            |  19 +++---
 lxd/migrate.go                |   6 ++
 lxd/storage.go                |   6 ++
 lxd/storage_btrfs.go          |  27 +++++++++
 lxd/storage_ceph.go           |  29 ++++++++++
 lxd/storage_dir.go            |  21 +++++++
 lxd/storage_lvm.go            |  21 +++++++
 lxd/storage_migration.go      |  51 ++++++++++++++++
 lxd/storage_mock.go           |  21 +++++++
 lxd/storage_volumes.go        | 132 ++++++++++++++++++++++++++++++++++++++++--
 lxd/storage_volumes_utils.go  |  27 ++++++---
 lxd/storage_zfs.go            |  27 +++++++++
 13 files changed, 371 insertions(+), 20 deletions(-)

diff --git a/client/lxd_storage_volumes.go b/client/lxd_storage_volumes.go
index 389a38d25..5b775f118 100644
--- a/client/lxd_storage_volumes.go
+++ b/client/lxd_storage_volumes.go
@@ -262,6 +262,10 @@ func (r *ProtocolLXD) CopyStoragePoolVolume(pool string, source ContainerServer,
 		return &rop, nil
 	}
 
+	if !r.HasExtension("storage_api_remote_volume_handling") {
+		return nil, fmt.Errorf("The server is missing the required \"storage_api_local_volume_handling\" API extension")
+	}
+
 	sourceReq := api.StorageVolumePost{
 		Migration: true,
 		Name:      volume.Name,
diff --git a/lxd/main_netcat.go b/lxd/main_netcat.go
index 17f775481..dbabf3eea 100644
--- a/lxd/main_netcat.go
+++ b/lxd/main_netcat.go
@@ -60,21 +60,24 @@ func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
 		os.Remove(logPath)
 	}
 
-	logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644)
-	if err != nil {
-		return err
+	logFile, logErr := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644)
+	if logErr == nil {
+		defer logFile.Close()
 	}
-	defer logFile.Close()
 
 	uAddr, err := net.ResolveUnixAddr("unix", args[0])
 	if err != nil {
-		logFile.WriteString(fmt.Sprintf("Could not resolve unix domain socket \"%s\": %s\n", args[0], err))
+		if logErr == nil {
+			logFile.WriteString(fmt.Sprintf("Could not resolve unix domain socket \"%s\": %s\n", args[0], err))
+		}
 		return err
 	}
 
 	conn, err := net.DialUnix("unix", nil, uAddr)
 	if err != nil {
-		logFile.WriteString(fmt.Sprintf("Could not dial unix domain socket \"%s\": %s\n", args[0], err))
+		if logErr == nil {
+			logFile.WriteString(fmt.Sprintf("Could not dial unix domain socket \"%s\": %s\n", args[0], err))
+		}
 		return err
 	}
 
@@ -83,7 +86,7 @@ func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
 
 	go func() {
 		_, err := io.Copy(eagain.Writer{Writer: os.Stdout}, eagain.Reader{Reader: conn})
-		if err != nil {
+		if err != nil && logErr == nil {
 			logFile.WriteString(fmt.Sprintf("Error while copying from stdout to unix domain socket \"%s\": %s\n", args[0], err))
 		}
 		conn.Close()
@@ -92,7 +95,7 @@ func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
 
 	go func() {
 		_, err := io.Copy(eagain.Writer{Writer: conn}, eagain.Reader{Reader: os.Stdin})
-		if err != nil {
+		if err != nil && logErr == nil {
 			logFile.WriteString(fmt.Sprintf("Error while copying from unix domain socket \"%s\" to stdin: %s\n", args[0], err))
 		}
 	}()
diff --git a/lxd/migrate.go b/lxd/migrate.go
index 04a223b1a..febc73d1e 100644
--- a/lxd/migrate.go
+++ b/lxd/migrate.go
@@ -38,6 +38,9 @@ type migrationFields struct {
 	live          bool
 	containerOnly bool
 	container     container
+
+	// storage specific fields
+	storage storage
 }
 
 func (c *migrationFields) send(m proto.Message) error {
@@ -258,6 +261,9 @@ type MigrationSinkArgs struct {
 	Live          bool
 	Container     container
 	ContainerOnly bool
+
+	// storage specific fields
+	Storage storage
 }
 
 func (c *migrationSink) connectWithSecret(secret string) (*websocket.Conn, error) {
diff --git a/lxd/storage.go b/lxd/storage.go
index bb6367838..b575b97c2 100644
--- a/lxd/storage.go
+++ b/lxd/storage.go
@@ -137,6 +137,7 @@ type storage interface {
 	GetStorageType() storageType
 	GetStorageTypeName() string
 	GetStorageTypeVersion() string
+	GetState() *state.State
 
 	// Functions dealing with storage pools.
 	StoragePoolInit() error
@@ -149,6 +150,7 @@ type storage interface {
 	StoragePoolUpdate(writable *api.StoragePoolPut, changedConfig []string) error
 	GetStoragePoolWritable() api.StoragePoolPut
 	SetStoragePoolWritable(writable *api.StoragePoolPut)
+	GetStoragePool() *api.StoragePool
 
 	// Functions dealing with custom storage volumes.
 	StoragePoolVolumeCreate() error
@@ -160,6 +162,7 @@ type storage interface {
 	StoragePoolVolumeCopy(source *api.StorageVolumeSource) error
 	GetStoragePoolVolumeWritable() api.StorageVolumePut
 	SetStoragePoolVolumeWritable(writable *api.StorageVolumePut)
+	GetStoragePoolVolume() *api.StorageVolume
 
 	// Functions dealing with container storage volumes.
 	// ContainerCreate creates an empty container (no rootfs/metadata.yaml)
@@ -228,6 +231,9 @@ type storage interface {
 		srcIdmap *idmap.IdmapSet,
 		op *operation,
 		containerOnly bool) error
+
+	StorageMigrationSource() (MigrationStorageSourceDriver, error)
+	StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error
 }
 
 func storageCoreInit(driver string) (storage, error) {
diff --git a/lxd/storage_btrfs.go b/lxd/storage_btrfs.go
index 0c363a149..c35b94150 100644
--- a/lxd/storage_btrfs.go
+++ b/lxd/storage_btrfs.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
@@ -2354,3 +2355,29 @@ func (s *storageBtrfs) StoragePoolVolumeCopy(source *api.StorageVolumeSource) er
 	logger.Infof(successMsg)
 	return nil
 }
+
+func (s *btrfsMigrationSourceDriver) SendStorageVolume(conn *websocket.Conn, op *operation, bwlimit string, storage storage) error {
+	msg := fmt.Sprintf("Function not implemented")
+	logger.Errorf(msg)
+	return fmt.Errorf(msg)
+}
+
+func (s *storageBtrfs) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageMigrationSource()
+}
+
+func (s *storageBtrfs) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return rsyncStorageMigrationSink(conn, op, storage)
+}
+
+func (s *storageBtrfs) GetStoragePool() *api.StoragePool {
+	return s.pool
+}
+
+func (s *storageBtrfs) GetStoragePoolVolume() *api.StorageVolume {
+	return s.volume
+}
+
+func (s *storageBtrfs) GetState() *state.State {
+	return s.s
+}
diff --git a/lxd/storage_ceph.go b/lxd/storage_ceph.go
index 2945da83e..c64d5f25c 100644
--- a/lxd/storage_ceph.go
+++ b/lxd/storage_ceph.go
@@ -6,7 +6,10 @@ import (
 	"strings"
 	"syscall"
 
+	"github.com/gorilla/websocket"
+
 	"github.com/lxc/lxd/lxd/db"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
 	"github.com/lxc/lxd/shared/logger"
@@ -2937,3 +2940,29 @@ func (s *storageCeph) StoragePoolVolumeCopy(source *api.StorageVolumeSource) err
 	logger.Infof(successMsg)
 	return nil
 }
+
+func (s *rbdMigrationSourceDriver) SendStorageVolume(conn *websocket.Conn, op *operation, bwlimit string, storage storage) error {
+	msg := fmt.Sprintf("Function not implemented")
+	logger.Errorf(msg)
+	return fmt.Errorf(msg)
+}
+
+func (s *storageCeph) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageMigrationSource()
+}
+
+func (s *storageCeph) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return rsyncStorageMigrationSink(conn, op, storage)
+}
+
+func (s *storageCeph) GetStoragePool() *api.StoragePool {
+	return s.pool
+}
+
+func (s *storageCeph) GetStoragePoolVolume() *api.StorageVolume {
+	return s.volume
+}
+
+func (s *storageCeph) GetState() *state.State {
+	return s.s
+}
diff --git a/lxd/storage_dir.go b/lxd/storage_dir.go
index 8630fdf93..d555596d7 100644
--- a/lxd/storage_dir.go
+++ b/lxd/storage_dir.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gorilla/websocket"
 
 	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
 	"github.com/lxc/lxd/shared/idmap"
@@ -1103,3 +1104,23 @@ func (s *storageDir) StoragePoolVolumeCopy(source *api.StorageVolumeSource) erro
 	logger.Infof(successMsg)
 	return nil
 }
+
+func (s *storageDir) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageMigrationSource()
+}
+
+func (s *storageDir) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return rsyncStorageMigrationSink(conn, op, storage)
+}
+
+func (s *storageDir) GetStoragePool() *api.StoragePool {
+	return s.pool
+}
+
+func (s *storageDir) GetStoragePoolVolume() *api.StorageVolume {
+	return s.volume
+}
+
+func (s *storageDir) GetState() *state.State {
+	return s.s
+}
diff --git a/lxd/storage_lvm.go b/lxd/storage_lvm.go
index 87b53254e..341ebe3e7 100644
--- a/lxd/storage_lvm.go
+++ b/lxd/storage_lvm.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gorilla/websocket"
 
 	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
 	"github.com/lxc/lxd/shared/idmap"
@@ -1925,3 +1926,23 @@ func (s *storageLvm) StoragePoolVolumeCopy(source *api.StorageVolumeSource) erro
 	logger.Infof(successMsg)
 	return nil
 }
+
+func (s *storageLvm) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageMigrationSource()
+}
+
+func (s *storageLvm) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return rsyncStorageMigrationSink(conn, op, storage)
+}
+
+func (s *storageLvm) GetStoragePool() *api.StoragePool {
+	return s.pool
+}
+
+func (s *storageLvm) GetStoragePoolVolume() *api.StorageVolume {
+	return s.volume
+}
+
+func (s *storageLvm) GetState() *state.State {
+	return s.s
+}
diff --git a/lxd/storage_migration.go b/lxd/storage_migration.go
index 975a8d021..6c699c111 100644
--- a/lxd/storage_migration.go
+++ b/lxd/storage_migration.go
@@ -10,6 +10,7 @@ import (
 	"github.com/lxc/lxd/lxd/types"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/idmap"
+	"github.com/lxc/lxd/shared/logger"
 )
 
 // MigrationStorageSourceDriver defines the functions needed to implement a
@@ -34,6 +35,8 @@ type MigrationStorageSourceDriver interface {
 	 * to clean up any temporary snapshots, etc.
 	 */
 	Cleanup()
+
+	SendStorageVolume(conn *websocket.Conn, op *operation, bwlimit string, storage storage) error
 }
 
 type rsyncStorageSourceDriver struct {
@@ -45,6 +48,26 @@ func (s rsyncStorageSourceDriver) Snapshots() []container {
 	return s.snapshots
 }
 
+func (s rsyncStorageSourceDriver) SendStorageVolume(conn *websocket.Conn, op *operation, bwlimit string, storage storage) error {
+	ourMount, err := storage.StoragePoolVolumeMount()
+	if err != nil {
+		return err
+	}
+	if ourMount {
+		defer storage.StoragePoolVolumeUmount()
+	}
+
+	pool := storage.GetStoragePool()
+	volume := storage.GetStoragePoolVolume()
+
+	wrapper := StorageProgressReader(op, "fs_progress", volume.Name)
+	state := storage.GetState()
+	path := getStoragePoolVolumeMountPoint(pool.Name, volume.Name)
+	path = shared.AddSlash(path)
+	logger.Debugf("Starting to send storage volume %s on storage pool %s from %s", volume.Name, pool.Name, path)
+	return RsyncSend(volume.Name, path, conn, wrapper, bwlimit, state.OS.ExecPath)
+}
+
 func (s rsyncStorageSourceDriver) SendWhileRunning(conn *websocket.Conn, op *operation, bwlimit string, containerOnly bool) error {
 	ctName, _, _ := containerGetParentAndSnapshotName(s.container.Name())
 
@@ -84,6 +107,10 @@ func (s rsyncStorageSourceDriver) Cleanup() {
 	// noop
 }
 
+func rsyncStorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageSourceDriver{nil, nil}, nil
+}
+
 func rsyncMigrationSource(c container, containerOnly bool) (MigrationStorageSourceDriver, error) {
 	var err error
 	var snapshots = []container{}
@@ -127,6 +154,30 @@ func snapshotProtobufToContainerArgs(containerName string, snap *migration.Snaps
 	}
 }
 
+func rsyncStorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	err := storage.StoragePoolVolumeCreate()
+	if err != nil {
+		return err
+	}
+
+	ourMount, err := storage.StoragePoolVolumeMount()
+	if err != nil {
+		return err
+	}
+	if ourMount {
+		defer storage.StoragePoolVolumeUmount()
+	}
+
+	pool := storage.GetStoragePool()
+	volume := storage.GetStoragePoolVolume()
+
+	wrapper := StorageProgressWriter(op, "fs_progress", volume.Name)
+	path := getStoragePoolVolumeMountPoint(pool.Name, volume.Name)
+	path = shared.AddSlash(path)
+	logger.Debugf("Starting to receive storage volume %s on storage pool %s into %s", volume.Name, pool.Name, path)
+	return RsyncRecv(path, conn, wrapper)
+}
+
 func rsyncMigrationSink(live bool, container container, snapshots []*migration.Snapshot, conn *websocket.Conn, srcIdmap *idmap.IdmapSet, op *operation, containerOnly bool) error {
 	ourStart, err := container.StorageStart()
 	if err != nil {
diff --git a/lxd/storage_mock.go b/lxd/storage_mock.go
index 1fa402f87..673fea4a6 100644
--- a/lxd/storage_mock.go
+++ b/lxd/storage_mock.go
@@ -6,6 +6,7 @@ import (
 	"github.com/gorilla/websocket"
 
 	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/shared/api"
 	"github.com/lxc/lxd/shared/idmap"
 	"github.com/lxc/lxd/shared/logger"
@@ -232,3 +233,23 @@ func (s *storageMock) StoragePoolResources() (*api.ResourcesStoragePool, error)
 func (s *storageMock) StoragePoolVolumeCopy(source *api.StorageVolumeSource) error {
 	return nil
 }
+
+func (s *storageMock) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return nil, nil
+}
+
+func (s *storageMock) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return nil
+}
+
+func (s *storageMock) GetStoragePool() *api.StoragePool {
+	return nil
+}
+
+func (s *storageMock) GetStoragePoolVolume() *api.StorageVolume {
+	return nil
+}
+
+func (s *storageMock) GetState() *state.State {
+	return nil
+}
diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go
index eeb4e6988..c601801ff 100644
--- a/lxd/storage_volumes.go
+++ b/lxd/storage_volumes.go
@@ -2,17 +2,23 @@ package main
 
 import (
 	"bytes"
+	"crypto/x509"
 	"encoding/json"
+	"encoding/pem"
 	"fmt"
 	"net/http"
 	"strings"
 
 	"github.com/gorilla/mux"
+	"github.com/gorilla/websocket"
 	"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/logger"
 	"github.com/lxc/lxd/shared/version"
+
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // /1.0/storage-pools/{name}/volumes
@@ -177,6 +183,8 @@ func storagePoolVolumesTypePost(d *Daemon, r *http.Request) Response {
 		return doVolumeCreateOrCopy(d, poolName, &req)
 	case "copy":
 		return doVolumeCreateOrCopy(d, poolName, &req)
+	case "migration":
+		return doVolumeMigration(d, poolName, &req)
 	default:
 		return BadRequest(fmt.Errorf("unknown source type %s", req.Source.Type))
 	}
@@ -212,6 +220,86 @@ func doVolumeCreateOrCopy(d *Daemon, poolName string, req *api.StorageVolumesPos
 	return OperationResponse(op)
 }
 
+func doVolumeMigration(d *Daemon, poolName string, req *api.StorageVolumesPost) Response {
+	// Validate migration mode
+	if req.Source.Mode != "pull" && req.Source.Mode != "push" {
+		return NotImplemented
+	}
+
+	storage, err := storagePoolVolumeDBCreateInternal(d.State(), poolName, req)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	// create new certificate
+	var cert *x509.Certificate
+	if req.Source.Certificate != "" {
+		certBlock, _ := pem.Decode([]byte(req.Source.Certificate))
+		if certBlock == nil {
+			return InternalError(fmt.Errorf("Invalid certificate"))
+		}
+
+		cert, err = x509.ParseCertificate(certBlock.Bytes)
+		if err != nil {
+			return InternalError(err)
+		}
+	}
+
+	config, err := shared.GetTLSConfig("", "", "", cert)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	push := false
+	if req.Source.Mode == "push" {
+		push = true
+	}
+
+	migrationArgs := MigrationSinkArgs{
+		Url: req.Source.Operation,
+		Dialer: websocket.Dialer{
+			TLSClientConfig: config,
+			NetDial:         shared.RFC3493Dialer},
+		Secrets: req.Source.Websockets,
+		Push:    push,
+		Storage: storage,
+	}
+
+	sink, err := NewStorageMigrationSink(&migrationArgs)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	resources := map[string][]string{}
+	resources["storage"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, req.Name)}
+
+	run := func(op *operation) error {
+		// And finally run the migration.
+		err = sink.DoStorage(op)
+		if err != nil {
+			logger.Error("Error during migration sink", log.Ctx{"err": err})
+			return fmt.Errorf("Error transferring storage volume: %s", err)
+		}
+
+		return nil
+	}
+
+	var op *operation
+	if push {
+		op, err = operationCreate(d.cluster, operationClassWebsocket, "Creating storage volume", resources, sink.Metadata(), run, nil, sink.Connect)
+		if err != nil {
+			return InternalError(err)
+		}
+	} else {
+		op, err = operationCreate(d.cluster, operationClassTask, "Copying storage volume", resources, nil, run, nil, nil)
+		if err != nil {
+			return InternalError(err)
+		}
+	}
+
+	return OperationResponse(op)
+}
+
 var storagePoolVolumesTypeCmd = Command{name: "storage-pools/{name}/volumes/{type}", get: storagePoolVolumesTypeGet, post: storagePoolVolumesTypePost}
 
 // /1.0/storage-pools/{name}/volumes/{type}/{name}
@@ -288,6 +376,45 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request) Response {
 		return response
 	}
 
+	s, err := storagePoolVolumeInit(d.State(), poolName, volumeName, storagePoolVolumeTypeCustom)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	// This is a migration request so send back requested secrets
+	if req.Migration {
+		ws, err := NewStorageMigrationSource(s)
+		if err != nil {
+			return InternalError(err)
+		}
+
+		resources := map[string][]string{}
+		resources["storage"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, volumeName)}
+
+		if req.Target != nil {
+			// Push mode
+			err := ws.ConnectStorageTarget(*req.Target)
+			if err != nil {
+				return InternalError(err)
+			}
+
+			op, err := operationCreate(d.cluster, operationClassTask, "Migrating storage volume", resources, nil, ws.DoStorage, nil, nil)
+			if err != nil {
+				return InternalError(err)
+			}
+
+			return OperationResponse(op)
+		}
+
+		// Pull mode
+		op, err := operationCreate(d.cluster, operationClassWebsocket, "Migrating storage volume", resources, ws.Metadata(), ws.DoStorage, nil, ws.Connect)
+		if err != nil {
+			return InternalError(err)
+		}
+
+		return OperationResponse(op)
+	}
+
 	// Check that the name isn't already in use.
 	_, err = d.cluster.StoragePoolNodeVolumeGetTypeID(req.Name,
 		storagePoolVolumeTypeCustom, poolID)
@@ -296,11 +423,6 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request) Response {
 	}
 
 	doWork := func() error {
-		s, err := storagePoolVolumeInit(d.State(), poolName, volumeName, storagePoolVolumeTypeCustom)
-		if err != nil {
-			return err
-		}
-
 		ctsUsingVolume, err := storagePoolVolumeUsedByRunningContainersWithProfilesGet(d.State(), poolName, volumeName, storagePoolVolumeTypeNameCustom, true)
 		if err != nil {
 			return err
diff --git a/lxd/storage_volumes_utils.go b/lxd/storage_volumes_utils.go
index 7290965e9..d7e4a9ef6 100644
--- a/lxd/storage_volumes_utils.go
+++ b/lxd/storage_volumes_utils.go
@@ -517,7 +517,7 @@ func storagePoolVolumeDBCreate(s *state.State, poolName string, volumeName, volu
 	return nil
 }
 
-func storagePoolVolumeCreateInternal(state *state.State, poolName string, vol *api.StorageVolumesPost) error {
+func storagePoolVolumeDBCreateInternal(state *state.State, poolName string, vol *api.StorageVolumesPost) (storage, error) {
 	volumeName := vol.Name
 	volumeDescription := vol.Description
 	volumeTypeName := vol.Type
@@ -528,13 +528,13 @@ func storagePoolVolumeCreateInternal(state *state.State, poolName string, vol *a
 		// between storage drivers.
 		s, err := storagePoolInit(state, poolName)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
 		driver := s.GetStorageTypeName()
 		newConfig, err := storageVolumePropertiesTranslate(vol.Config, driver)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
 		vol.Config = newConfig
@@ -544,25 +544,34 @@ func storagePoolVolumeCreateInternal(state *state.State, poolName string, vol *a
 	// Create database entry for new storage volume.
 	err := storagePoolVolumeDBCreate(state, poolName, volumeName, volumeDescription, volumeTypeName, volumeConfig)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// Convert the volume type name to our internal integer representation.
 	poolID, err := state.Cluster.StoragePoolGetID(poolName)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	volumeType, err := storagePoolVolumeTypeNameToType(volumeTypeName)
 	if err != nil {
 		state.Cluster.StoragePoolVolumeDelete(volumeName, volumeType, poolID)
-		return err
+		return nil, err
 	}
 
 	// Initialize new storage volume on the target storage pool.
 	s, err := storagePoolVolumeInit(state, poolName, volumeName, volumeType)
 	if err != nil {
 		state.Cluster.StoragePoolVolumeDelete(volumeName, volumeType, poolID)
+		return nil, err
+	}
+
+	return s, nil
+}
+
+func storagePoolVolumeCreateInternal(state *state.State, poolName string, vol *api.StorageVolumesPost) error {
+	s, err := storagePoolVolumeDBCreateInternal(state, poolName, vol)
+	if err != nil {
 		return err
 	}
 
@@ -572,7 +581,11 @@ func storagePoolVolumeCreateInternal(state *state.State, poolName string, vol *a
 		err = s.StoragePoolVolumeCopy(&vol.Source)
 	}
 	if err != nil {
-		state.Cluster.StoragePoolVolumeDelete(volumeName, volumeType, poolID)
+		poolID, _, _ := s.GetContainerPoolInfo()
+		volumeType, err := storagePoolVolumeTypeNameToType(vol.Type)
+		if err == nil {
+			state.Cluster.StoragePoolVolumeDelete(vol.Name, volumeType, poolID)
+		}
 		return err
 	}
 
diff --git a/lxd/storage_zfs.go b/lxd/storage_zfs.go
index e071108bc..e657c3081 100644
--- a/lxd/storage_zfs.go
+++ b/lxd/storage_zfs.go
@@ -14,6 +14,7 @@ import (
 	"github.com/gorilla/websocket"
 
 	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
@@ -2563,3 +2564,29 @@ func (s *storageZfs) StoragePoolVolumeCopy(source *api.StorageVolumeSource) erro
 	logger.Infof(successMsg)
 	return nil
 }
+
+func (s *zfsMigrationSourceDriver) SendStorageVolume(conn *websocket.Conn, op *operation, bwlimit string, storage storage) error {
+	msg := fmt.Sprintf("Function not implemented")
+	logger.Errorf(msg)
+	return fmt.Errorf(msg)
+}
+
+func (s *storageZfs) StorageMigrationSource() (MigrationStorageSourceDriver, error) {
+	return rsyncStorageMigrationSource()
+}
+
+func (s *storageZfs) StorageMigrationSink(conn *websocket.Conn, op *operation, storage storage) error {
+	return rsyncStorageMigrationSink(conn, op, storage)
+}
+
+func (s *storageZfs) GetStoragePool() *api.StoragePool {
+	return s.pool
+}
+
+func (s *storageZfs) GetStoragePoolVolume() *api.StorageVolume {
+	return s.volume
+}
+
+func (s *storageZfs) GetState() *state.State {
+	return s.s
+}

From a9232dd61a79a9ba95a75002267f76288ebbaf86 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 20:22:44 +0100
Subject: [PATCH 4/6] lxc/storage: remote storage volume migration

Closes #3985.

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 lxc/storage.go | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/lxc/storage.go b/lxc/storage.go
index 1caa1bc2c..9ad7a9aca 100644
--- a/lxc/storage.go
+++ b/lxc/storage.go
@@ -25,6 +25,7 @@ import (
 type storageCmd struct {
 	resources bool
 	byteflag  bool
+	mode      string
 	target    string
 }
 
@@ -162,6 +163,7 @@ lxc storage volume show default container/data
 func (c *storageCmd) flags() {
 	gnuflag.BoolVar(&c.resources, "resources", false, i18n.G("Show the resources available to the storage pool"))
 	gnuflag.BoolVar(&c.byteflag, "bytes", false, i18n.G("Show the used and free space in bytes"))
+	gnuflag.StringVar(&c.mode, "mode", "pull", i18n.G("Transfer mode. One of pull (default), push or relay."))
 	gnuflag.StringVar(&c.target, "target", "", i18n.G("Node name"))
 }
 
@@ -1142,18 +1144,26 @@ func (c *storageCmd) doStoragePoolVolumeCopy(source lxd.ContainerServer, sourceR
 		}
 	}
 
+	// Parse the mode
+	mode := "pull"
+	if c.mode != "" {
+		mode = c.mode
+	}
+
 	var op lxd.RemoteOperation
 	opMsg := ""
 	finalMsg := ""
-	if move {
+	if move && sourceRemote == destRemote {
 		args := &lxd.StoragePoolVolumeMoveArgs{}
 		args.Name = dstVolName
+		args.Mode = mode
 		op, err = dest.MoveStoragePoolVolume(dstVolPool, source, srcVolPool, *srcVol, args)
 		opMsg = i18n.G("Moving the storage volume: %s")
 		finalMsg = i18n.G("Storage volume moved successfully!")
 	} else {
 		args := &lxd.StoragePoolVolumeCopyArgs{}
 		args.Name = dstVolName
+		args.Mode = mode
 		op, err = dest.CopyStoragePoolVolume(dstVolPool, source, srcVolPool, *srcVol, args)
 		opMsg = i18n.G("Copying the storage volume: %s")
 		finalMsg = i18n.G("Storage volume copied successfully!")
@@ -1175,6 +1185,13 @@ func (c *storageCmd) doStoragePoolVolumeCopy(source lxd.ContainerServer, sourceR
 		return err
 	}
 
+	if move && sourceRemote != destRemote {
+		err := source.DeleteStoragePoolVolume(srcVolPool, srcVol.Type, srcVolName)
+		if err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
 

From 09f5157eb9440deb65ae71829ada865eb43b7b55 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 21:55:16 +0100
Subject: [PATCH 5/6] test: add minimal remote storage volume testing

Closes #3985.

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 test/suites/migration.sh | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/test/suites/migration.sh b/test/suites/migration.sh
index 5cd8232d1..8495ff8b5 100644
--- a/test/suites/migration.sh
+++ b/test/suites/migration.sh
@@ -222,6 +222,18 @@ migration() {
     lxc storage unset "lxdtest-$(basename "${LXD_DIR}")" zfs.clone_copy
   fi
 
+  storage_pool1="lxdtest-$(basename "${LXD_DIR}")"
+  storage_pool2="lxdtest-$(basename "${lxd2_dir}")"
+
+  lxc_remote storage volume create l1:"$storage_pool1" vol1
+
+  lxc_remote storage volume copy l1:"$storage_pool1/vol1" l2:"$storage_pool2/vol2"
+  lxc_remote storage volume move l1:"$storage_pool1/vol1" l2:"$storage_pool2/vol3"
+  ! lxc_remote storage volume list l1:"$storage_pool1/vol1"
+
+  lxc_remote storage volume delete l2:"$storage_pool2" vol2
+  lxc_remote storage volume delete l2:"$storage_pool2" vol3
+
   if ! which criu >/dev/null 2>&1; then
     echo "==> SKIP: live migration with CRIU (missing binary)"
     return

From 7c299033c292d17136d46450a5242696a56a7f28 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at ubuntu.com>
Date: Fri, 23 Mar 2018 21:00:02 +0100
Subject: [PATCH 6/6] api: add "storage_api_remote_volume_handling"

Closes #3985.

Signed-off-by: Christian Brauner <christian.brauner at ubuntu.com>
---
 doc/api-extensions.md  |  3 +++
 doc/rest-api.md        | 37 ++++++++++++++++++++++++++++++++++++-
 lxd/storage_volumes.go |  4 ++--
 shared/version/api.go  |  1 +
 4 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 156e45c1b..d0b5820cc 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -446,3 +446,6 @@ The following existing endpoints have been modified:
 
 ## event_lifecycle
 This adds a new `lifecycle` message type to the events API.
+
+## storage\_api\_remote\_volume\_handling
+This add the ability to copy and move custom storage volumes remotely.
diff --git a/doc/rest-api.md b/doc/rest-api.md
index 6f5e541ed..8f9835dde 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -2313,7 +2313,22 @@ Input (when copying a volume):
         "source": {
             "pool": "pool2",
             "name": "vol2",
-            "type": "custom"
+            "type": "copy"
+        }
+    }
+
+Input (when migrating a volume):
+
+    {
+        "config": {},
+        "pool": "pool1",
+        "name": "vol1",
+        "type": "custom"
+        "source": {
+            "pool": "pool2",
+            "name": "vol2",
+            "type": "migration"
+            "mode": "pull",                                                 # One of "pull" (default), "push", "relay"
         }
     }
 
@@ -2332,6 +2347,26 @@ Input:
         "pool": "pool3"
     }
 
+Input (migration across lxd instances):
+
+    {
+        "name": "vol1"
+        "pool": "pool3"
+        "migration": true
+    }
+
+The migration does not actually start until someone (i.e. another lxd instance)
+connects to all the websockets and begins negotiation with the source.
+
+Output in metadata section (for migration):
+
+    {
+        "control": "secret1",       # Migration control socket
+        "fs": "secret3"             # Filesystem transfer socket
+    }
+
+These are the secrets that should be passed to the create call.
+
 ### GET
  * Description: information about a storage volume of a given type on a storage pool
  * Introduced: with API extension `storage`
diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go
index c601801ff..b002ddc4b 100644
--- a/lxd/storage_volumes.go
+++ b/lxd/storage_volumes.go
@@ -271,7 +271,7 @@ func doVolumeMigration(d *Daemon, poolName string, req *api.StorageVolumesPost)
 	}
 
 	resources := map[string][]string{}
-	resources["storage"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, req.Name)}
+	resources["storage_volumes"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, req.Name)}
 
 	run := func(op *operation) error {
 		// And finally run the migration.
@@ -389,7 +389,7 @@ func storagePoolVolumeTypePost(d *Daemon, r *http.Request) Response {
 		}
 
 		resources := map[string][]string{}
-		resources["storage"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, volumeName)}
+		resources["storage_volumes"] = []string{fmt.Sprintf("%s/volumes/custom/%s", poolName, volumeName)}
 
 		if req.Target != nil {
 			// Push mode
diff --git a/shared/version/api.go b/shared/version/api.go
index 009f166c0..f6e718a42 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -100,6 +100,7 @@ var APIExtensions = []string{
 	"operation_description",
 	"clustering",
 	"event_lifecycle",
+	"storage_api_remote_volume_handling",
 }
 
 // APIExtensionsCount returns the number of available API extensions.


More information about the lxc-devel mailing list