The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6367
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === - Updates Storage volume copy to use the migration logic with a bi-directional in-memory pipe. - Adds progress tracker indicator to storage migration functions so that when doing a local copy one can disable the progress tracker on one side.
From 19100009dea691fa57b8fa0e24f6db522e82c7df Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:28:27 +0000 Subject: [PATCH 1/9] lxd/storage/memorypipe: Adds in-memory bidirectional pipe Compatible with the io.ReadWriteCloser interface. Used for simulating WebsocketIO between local endpoints. Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/memorypipe/memory_pipe.go | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lxd/storage/memorypipe/memory_pipe.go diff --git a/lxd/storage/memorypipe/memory_pipe.go b/lxd/storage/memorypipe/memory_pipe.go new file mode 100644 index 0000000000..2484c30c3b --- /dev/null +++ b/lxd/storage/memorypipe/memory_pipe.go @@ -0,0 +1,72 @@ +package memorypipe + +import ( + "io" +) + +// msg represents an internal structure sent between the pipes. +type msg struct { + data []byte + err error +} + +// pipe provides a bidirectional pipe compatible with io.ReadWriteCloser interface. +// Note, however, that it does not behave exactly how one would expect an io.ReadWriteCloser to +// behave. Specifically the Close() function does not close the pipe, but instead delivers an io.EOF +// error to the next reader. After which it can be read again to receive new data. This means the +// pipe can be closed multiple times. Each time it indicates that one particular session has ended. +// The reason for this is to emulate the WebsocketIO's behaviour by allowing a single persistent +// connection to be used for multiple sessions. +type pipe struct { + ch chan msg + otherEnd *pipe +} + +// Read reads from the pipe into p. Returns number of bytes read and any errors. +func (p *pipe) Read(b []byte) (int, error) { + msg := <-p.ch + if msg.err == io.EOF { + return -1, msg.err + } + n := copy(b, msg.data) + return n, msg.err +} + +// Write writes to the pipe from p. Returns number of bytes written and any errors. +func (p *pipe) Write(b []byte) (int, error) { + msg := msg{ + data: append(b[:0:0], b...), // Create copy of b in case it is modified externally. + err: nil, + } + p.otherEnd.ch <- msg // Send msg to the other side's Read function. + return len(msg.data), msg.err +} + +// Close is unusual in that it doesn't actually close the pipe. Instead it sends an io.EOF error +// to the other side's Read function. This is so the other side can detect that a session has ended. +// Each call to Close will indicate to the other side that a session has ended, whilst allowing the +// reuse of a single persistent pipe for multiple sessions. +func (p *pipe) Close() error { + p.otherEnd.ch <- msg{ + data: nil, + err: io.EOF, // Indicates to the other side's Read function that session has ended. + } + return nil +} + +// NewPipePair returns a pair of io.ReadWriterCloser pipes that are connected together such that +// writes to one will appear as reads on the other and vice versa. Calling Close() on one end will +// indicate to the other end that the session has ended. +func NewPipePair() (io.ReadWriteCloser, io.ReadWriteCloser) { + aEnd := &pipe{ + ch: make(chan msg, 1), + } + + bEnd := &pipe{ + ch: make(chan msg, 1), + } + + aEnd.otherEnd = bEnd + bEnd.otherEnd = aEnd + return aEnd, bEnd +} From 126de79bafff4e082cac3b078e674d0fc3dad7c9 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:29:16 +0000 Subject: [PATCH 2/9] lxd/migrate/storage/volumes: Updates use of migrate functions Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/migrate_storage_volumes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/migrate_storage_volumes.go b/lxd/migrate_storage_volumes.go index ce232d9f65..a60ea178ba 100644 --- a/lxd/migrate_storage_volumes.go +++ b/lxd/migrate_storage_volumes.go @@ -156,7 +156,7 @@ func (s *migrationSourceWs) DoStorage(state *state.State, poolName string, volNa Snapshots: snapshotNames, } - err = pool.MigrateCustomVolume(&shared.WebsocketIO{Conn: s.fsConn}, volSourceArgs, migrateOp) + err = pool.MigrateCustomVolume(&shared.WebsocketIO{Conn: s.fsConn}, volSourceArgs, true, migrateOp) if err != nil { go s.sendControl(err) return err @@ -371,7 +371,7 @@ func (c *migrationSink) DoStorage(state *state.State, poolName string, req *api. } } - return pool.CreateCustomVolumeFromMigration(&shared.WebsocketIO{Conn: conn}, volTargetArgs, op) + return pool.CreateCustomVolumeFromMigration(&shared.WebsocketIO{Conn: conn}, volTargetArgs, true, op) } } else { // Setup legacy storage migration sink if destination pool isn't supported yet by From b4b678d07c379741d0a9ee89ff301b38923a6a49 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:31:24 +0000 Subject: [PATCH 3/9] lxd/storage/backend/lxd: Updates CreateCustomVolumeFromCopy to use migration logic Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/backend_lxd.go | 99 ++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go index 277c2b1f4f..368f7f5173 100644 --- a/lxd/storage/backend_lxd.go +++ b/lxd/storage/backend_lxd.go @@ -11,6 +11,7 @@ import ( "github.com/lxc/lxd/lxd/operations" "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/lxd/storage/drivers" + "github.com/lxc/lxd/lxd/storage/memorypipe" "github.com/lxc/lxd/shared" "github.com/lxc/lxd/shared/api" ) @@ -273,12 +274,6 @@ func (b *lxdBackend) CreateCustomVolume(volName, desc string, config map[string] // CreateCustomVolumeFromCopy creates a custom volume from an existing custom volume. // It copies the snapshots from the source volume by default, but can be disabled if requested. func (b *lxdBackend) CreateCustomVolumeFromCopy(volName, desc string, config map[string]string, srcPoolName, srcVolName string, srcVolOnly bool, op *operations.Operation) error { - // Default to copying snapshots too, but if VolumeOnly is supplied then we only copy volume. - copySnapshots := true - if srcVolOnly { - copySnapshots = false - } - // Setup the source pool backend instance. var srcPool *lxdBackend if b.name == srcPoolName { @@ -319,60 +314,72 @@ func (b *lxdBackend) CreateCustomVolumeFromCopy(volName, desc string, config map desc = srcVolRow.Description } - // Check the supplied config and remove any fields not relevant for destination pool type. - err = b.driver.ValidateVolume(config, true) - if err != nil { - return err - } + // If we are copying snapshots, retrieve a list of snapshots from source volume. + snapshotNames := []string{} + if !srcVolOnly { + snapshots, err := VolumeSnapshotsGet(b.state, srcPoolName, srcVolName, db.StoragePoolVolumeTypeCustom) + if err != nil { + return err + } - // Create slice to record DB volumes created if revert needed later. - revertDBVolumes := []string{} - defer func() { - // Remove any DB volume rows created if we are reverting. - for _, volName := range revertDBVolumes { - b.state.Cluster.StoragePoolVolumeDelete("default", volName, db.StoragePoolVolumeTypeCustom, b.ID()) + for _, snapshot := range snapshots { + _, snapShotName, _ := shared.ContainerGetParentAndSnapshotName(snapshot.Name) + snapshotNames = append(snapshotNames, snapShotName) } - }() + } - // Create database entry for new storage volume. - err = VolumeDBCreate(b.state, b.name, volName, desc, db.StoragePoolVolumeTypeNameCustom, false, config) + // Create in-memory pipe pair to simulate a connection between the sender and receiver. + aEnd, bEnd := memorypipe.NewPipePair() + + // Negotiate the migration type to use. + offeredTypes := srcPool.MigrationTypes(drivers.ContentTypeFS) + offerHeader := migration.TypesToHeader(offeredTypes...) + migrationType, err := migration.MatchTypes(offerHeader, b.MigrationTypes(drivers.ContentTypeFS)) if err != nil { - return err + return fmt.Errorf("Failed to neogotiate copy migration type: %v", err) } - revertDBVolumes = append(revertDBVolumes, volName) + // Run sender and receiver in separate go routines to prevent deadlocks. + aEndErrCh := make(chan error, 1) + bEndErrCh := make(chan error, 1) + go func() { + err := srcPool.MigrateCustomVolume(aEnd, migration.VolumeSourceArgs{ + Name: srcVolName, + Snapshots: snapshotNames, + MigrationType: migrationType, + }, false, op) // Do not use a progress tracker on sender. - if copySnapshots { - // If we are copying snapshots, retrieve a list of snapshots from source volume. - snapshots, err := VolumeSnapshotsGet(b.state, srcPoolName, volName, db.StoragePoolVolumeTypeCustom) - if err != nil { - return err - } + aEndErrCh <- err + }() - // Create a database entry and copy the volume for each snapshot. - for _, srcSnapshot := range snapshots { - // Convert the source snapshot volume name into the new snapshot volume name. - _, snapName, _ := shared.ContainerGetParentAndSnapshotName(srcSnapshot.Name) - newSnapshotName := drivers.GetSnapshotVolumeName(volName, snapName) + go func() { + err := b.CreateCustomVolumeFromMigration(bEnd, migration.VolumeTargetArgs{ + Name: volName, + Description: desc, + Config: config, + Snapshots: snapshotNames, + MigrationType: migrationType, + }, true, op) // Do use a progress tracker on receiver. - // Create database entry for new storage volume. - err = VolumeDBCreate(b.state, b.name, newSnapshotName, srcSnapshot.Description, db.StoragePoolVolumeTypeNameCustom, true, config) - if err != nil { - return err - } + bEndErrCh <- err + }() - revertDBVolumes = append(revertDBVolumes, newSnapshotName) - } + // Capture errors from the sender and receiver from their result channels. + errs := []error{} + aEndErr := <-aEndErrCh + if aEndErr != nil { + errs = append(errs, aEndErr) } - srcVol := srcPool.newVolume(drivers.VolumeTypeCustom, drivers.ContentTypeFS, srcVolName, nil) - targetVol := b.newVolume(drivers.VolumeTypeCustom, drivers.ContentTypeFS, volName, config) - err = b.driver.CreateVolumeFromCopy(targetVol, srcVol, copySnapshots, op) - if err != nil { - return err + bEndErr := <-bEndErrCh + if bEndErr != nil { + errs = append(errs, bEndErr) + } + + if len(errs) > 0 { + return fmt.Errorf("Create custom volume from copy failed: %v", errs) } - revertDBVolumes = nil // Don't revert DB volumes. return nil } From 786215e75dabffc672cd3abd467a967cc4efb80b Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:31:51 +0000 Subject: [PATCH 4/9] lxd/storage/backend/lxd: Updates migration functions to accept progress tracker indicator Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/backend_lxd.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go index 368f7f5173..7216bb7932 100644 --- a/lxd/storage/backend_lxd.go +++ b/lxd/storage/backend_lxd.go @@ -384,9 +384,9 @@ func (b *lxdBackend) CreateCustomVolumeFromCopy(volName, desc string, config map } // MigrateCustomVolume sends a volume for migration. -func (b *lxdBackend) MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, op *operations.Operation) error { +func (b *lxdBackend) MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, trackProgress bool, op *operations.Operation) error { vol := b.newVolume(drivers.VolumeTypeCustom, drivers.ContentTypeFS, args.Name, nil) - err := b.driver.MigrateVolume(vol, conn, args, op) + err := b.driver.MigrateVolume(vol, conn, args, trackProgress, op) if err != nil { return err } @@ -395,7 +395,7 @@ func (b *lxdBackend) MigrateCustomVolume(conn io.ReadWriteCloser, args migration } // CreateCustomVolumeFromMigration receives a volume being migrated. -func (b *lxdBackend) CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, op *operations.Operation) error { +func (b *lxdBackend) CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, trackProgress bool, op *operations.Operation) error { // Create slice to record DB volumes created if revert needed later. revertDBVolumes := []string{} defer func() { @@ -434,7 +434,7 @@ func (b *lxdBackend) CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, ar } vol := b.newVolume(drivers.VolumeTypeCustom, drivers.ContentTypeFS, args.Name, args.Config) - err = b.driver.CreateVolumeFromMigration(vol, conn, args, op) + err = b.driver.CreateVolumeFromMigration(vol, conn, args, trackProgress, op) if err != nil { return nil } From e752afc647705551d6bbb4c789e55c14ca36ffa5 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:32:26 +0000 Subject: [PATCH 5/9] lxd/storage/backend/mock: Updates migration functions to accept progress tracker indicator Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/backend_mock.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/storage/backend_mock.go b/lxd/storage/backend_mock.go index 86562bc73e..d2987626af 100644 --- a/lxd/storage/backend_mock.go +++ b/lxd/storage/backend_mock.go @@ -168,11 +168,11 @@ func (b *mockBackend) DeleteCustomVolume(volName string, op *operations.Operatio return nil } -func (b *mockBackend) MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, op *operations.Operation) error { +func (b *mockBackend) MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, trackProgress bool, op *operations.Operation) error { return nil } -func (b *mockBackend) CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, op *operations.Operation) error { +func (b *mockBackend) CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, trackProgress bool, op *operations.Operation) error { return nil } From b78993cf27476721627e061ee7bc790eb054416a Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:33:02 +0000 Subject: [PATCH 6/9] lxd/storage/drivers/driver/common: Improves comment Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/drivers/driver_common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/storage/drivers/driver_common.go b/lxd/storage/drivers/driver_common.go index f549975c3f..b2298ff330 100644 --- a/lxd/storage/drivers/driver_common.go +++ b/lxd/storage/drivers/driver_common.go @@ -70,8 +70,8 @@ func (d *common) validateVolume(volConfig map[string]string, driverRules map[str return nil } -// MigrationType returns the type of transfer method used when doing migrations between pools -// of the same type. +// MigrationType returns the type of transfer methods to be used when doing migrations between pools +// in preference order. func (d *common) MigrationTypes(contentType ContentType) []migration.Type { if contentType != ContentTypeFS { return nil From 0f3fa0c3b7ac54ccb2d9c01956b280878cd1e01b Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:33:32 +0000 Subject: [PATCH 7/9] lxd/storage/drivers/driver/dir: Updates migration functions to accept progress tracker indicator Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/drivers/driver_dir.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lxd/storage/drivers/driver_dir.go b/lxd/storage/drivers/driver_dir.go index 19e856df35..c9e3cbd28f 100644 --- a/lxd/storage/drivers/driver_dir.go +++ b/lxd/storage/drivers/driver_dir.go @@ -15,6 +15,7 @@ import ( "github.com/lxc/lxd/lxd/storage/quota" "github.com/lxc/lxd/shared" "github.com/lxc/lxd/shared/api" + "github.com/lxc/lxd/shared/ioprogress" "github.com/lxc/lxd/shared/units" ) @@ -184,7 +185,7 @@ func (d *dir) CreateVolume(vol Volume, filler func(path string) error, op *opera } // MigrateVolume sends a volume for migration. -func (d *dir) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, op *operations.Operation) error { +func (d *dir) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, trackProgress bool, op *operations.Operation) error { if volSrcArgs.MigrationType.FSType != migration.MigrationFSType_RSYNC { return fmt.Errorf("Migration type not supported") } @@ -199,7 +200,10 @@ func (d *dir) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migr // Send snapshot to recipient (ensure local snapshot volume is mounted if needed). err = snapshot.MountTask(func(mountPath string, op *operations.Operation) error { - wrapper := migration.ProgressTracker(op, "fs_progress", snapshot.name) + var wrapper *ioprogress.ProgressTracker + if trackProgress { + wrapper = migration.ProgressTracker(op, "fs_progress", snapshot.name) + } path := shared.AddSlash(mountPath) return rsync.Send(snapshot.name, path, conn, wrapper, volSrcArgs.MigrationType.Features, bwlimit, d.state.OS.ExecPath) }, op) @@ -210,14 +214,17 @@ func (d *dir) MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migr // Send volume to recipient (ensure local volume is mounted if needed). return vol.MountTask(func(mountPath string, op *operations.Operation) error { - wrapper := migration.ProgressTracker(op, "fs_progress", vol.name) + var wrapper *ioprogress.ProgressTracker + if trackProgress { + wrapper = migration.ProgressTracker(op, "fs_progress", vol.name) + } path := shared.AddSlash(mountPath) return rsync.Send(vol.name, path, conn, wrapper, volSrcArgs.MigrationType.Features, bwlimit, d.state.OS.ExecPath) }, op) } // CreateVolumeFromMigration creates a volume being sent via a migration. -func (d *dir) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, op *operations.Operation) error { +func (d *dir) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, trackProgress bool, op *operations.Operation) error { if vol.contentType != ContentTypeFS { return fmt.Errorf("Content type not supported") } @@ -266,7 +273,10 @@ func (d *dir) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, vol // Receive snapshot from sender (ensure local snapshot volume is mounted if needed). err = snapshot.MountTask(func(mountPath string, op *operations.Operation) error { - wrapper := migration.ProgressTracker(op, "fs_progress", snapshot.name) + var wrapper *ioprogress.ProgressTracker + if trackProgress { + wrapper = migration.ProgressTracker(op, "fs_progress", snapshot.name) + } path := shared.AddSlash(mountPath) return rsync.Recv(path, conn, wrapper, volTargetArgs.MigrationType.Features) }, op) @@ -299,7 +309,10 @@ func (d *dir) CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, vol // Receive volume from sender (ensure local volume is mounted if needed). err = vol.MountTask(func(mountPath string, op *operations.Operation) error { - wrapper := migration.ProgressTracker(op, "fs_progress", vol.name) + var wrapper *ioprogress.ProgressTracker + if trackProgress { + wrapper = migration.ProgressTracker(op, "fs_progress", vol.name) + } path := shared.AddSlash(mountPath) return rsync.Recv(path, conn, wrapper, volTargetArgs.MigrationType.Features) }, op) From 0737102ab3e9e8ad8e2a2eef79c69a7d4bb8d018 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:33:58 +0000 Subject: [PATCH 8/9] lxd/storage/drivers/interface: Updates migration functions to accept progress tracker indicator Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/drivers/interface.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/storage/drivers/interface.go b/lxd/storage/drivers/interface.go index c7d79fd84b..2e6024982c 100644 --- a/lxd/storage/drivers/interface.go +++ b/lxd/storage/drivers/interface.go @@ -59,6 +59,6 @@ type Driver interface { // Migration. MigrationTypes(contentType ContentType) []migration.Type - MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, op *operations.Operation) error - CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, op *operations.Operation) error + MigrateVolume(vol Volume, conn io.ReadWriteCloser, volSrcArgs migration.VolumeSourceArgs, trackProgress bool, op *operations.Operation) error + CreateVolumeFromMigration(vol Volume, conn io.ReadWriteCloser, volTargetArgs migration.VolumeTargetArgs, trackProgress bool, op *operations.Operation) error } From 05f3e1a478f3813abf945657f591f63b5af4e6d9 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Wed, 30 Oct 2019 13:34:18 +0000 Subject: [PATCH 9/9] lxd/storage/interfaces: Updates migration functions to accept progress tracker indicator Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/storage/interfaces.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxd/storage/interfaces.go b/lxd/storage/interfaces.go index d1d3cbdbba..3412d03351 100644 --- a/lxd/storage/interfaces.go +++ b/lxd/storage/interfaces.go @@ -88,6 +88,6 @@ type Pool interface { // Custom volume migration. MigrationTypes(contentType drivers.ContentType) []migration.Type - CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, op *operations.Operation) error - MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, op *operations.Operation) error + CreateCustomVolumeFromMigration(conn io.ReadWriteCloser, args migration.VolumeTargetArgs, trackProgress bool, op *operations.Operation) error + MigrateCustomVolume(conn io.ReadWriteCloser, args migration.VolumeSourceArgs, trackProgress bool, op *operations.Operation) error }
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel