[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