[lxc-devel] [lxd/master] Support direct `copy` within a cluster

stgraber on Github lxc-bot at linuxcontainers.org
Fri Apr 26 23:51:10 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190426/30b4d3ea/attachment.bin>
-------------- next part --------------
From 0f66ec308c2f03a43578e2efff7da306a5c3cb56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 26 Apr 2019 19:47:56 -0400
Subject: [PATCH 1/5] client: Fix copy from snapshot
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client/lxd_containers.go | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/client/lxd_containers.go b/client/lxd_containers.go
index 17210f01b5..eaa3f1fa94 100644
--- a/client/lxd_containers.go
+++ b/client/lxd_containers.go
@@ -1105,8 +1105,32 @@ func (r *ProtocolLXD) CopyContainerSnapshot(source ContainerServer, containerNam
 		}
 	}
 
+	sourceInfo, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+	}
+
+	destInfo, err := r.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+	}
+
+	container, _, err := source.GetContainer(cName)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get container info: %v", err)
+	}
+
 	// Optimization for the local copy case
-	if r == source && r.clusterTarget == "" {
+	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || container.Location == r.clusterTarget) {
+		// Project handling
+		if destInfo.Project != sourceInfo.Project {
+			if !r.HasExtension("container_copy_project") {
+				return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+			}
+
+			req.Source.Project = sourceInfo.Project
+		}
+
 		// Local copy source fields
 		req.Source.Type = "copy"
 		req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)

From a37a67dd17e214200dfcc4f7b6dc798e5afcf04f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 26 Apr 2019 19:16:56 -0400
Subject: [PATCH 2/5] lxd/storage/ceph: Fix snapshot of running containers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/storage_ceph.go | 32 ++++++++++----------------------
 1 file changed, 10 insertions(+), 22 deletions(-)

diff --git a/lxd/storage_ceph.go b/lxd/storage_ceph.go
index af574e0529..9e0be80504 100644
--- a/lxd/storage_ceph.go
+++ b/lxd/storage_ceph.go
@@ -1128,25 +1128,15 @@ func (s *storageCeph) ContainerCopy(target container, source container,
 	sourceContainerName := source.Name()
 	logger.Debugf(`Copying RBD container storage %s to %s`, sourceContainerName, target.Name())
 
-	revert := true
-
-	ourStart, err := source.StorageStart()
-	if err != nil {
-		logger.Errorf(`Failed to initialize storage for container "%s": %s`, sourceContainerName, err)
-		return err
-	}
-	logger.Debugf(`Initialized storage for container "%s"`,
-		sourceContainerName)
-	if ourStart {
-		defer source.StorageStop()
-	}
-
+	// Handle cross pool copies
 	_, sourcePool, _ := source.Storage().GetContainerPoolInfo()
 	_, targetPool, _ := target.Storage().GetContainerPoolInfo()
 	if sourcePool != targetPool {
 		return s.doCrossPoolContainerCopy(target, source, containerOnly, false, nil)
 	}
 
+	revert := true
+
 	snapshots, err := source.Snapshots()
 	if err != nil {
 		logger.Errorf(`Failed to retrieve snapshots of container "%s": %s`, sourceContainerName, err)
@@ -1370,21 +1360,12 @@ func (s *storageCeph) ContainerCopy(target container, source container,
 	logger.Debugf(`Copied RBD container storage %s to %s`, sourceContainerName, target.Name())
 
 	revert = false
-
 	return nil
 }
 
 func (s *storageCeph) ContainerRefresh(target container, source container, snapshots []container) error {
 	logger.Debugf(`Refreshing RBD container storage for %s from %s`, target.Name(), source.Name())
 
-	ourStart, err := source.StorageStart()
-	if err != nil {
-		return err
-	}
-	if ourStart {
-		defer source.StorageStop()
-	}
-
 	return s.doCrossPoolContainerCopy(target, source, len(snapshots) == 0, true, snapshots)
 }
 
@@ -1862,6 +1843,13 @@ func (s *storageCeph) ContainerSnapshotStart(c container) (bool, error) {
 	containerMntPoint := getSnapshotMountPoint(c.Project(), s.pool.Name, containerName)
 	RBDFilesystem := s.getRBDFilesystem()
 	mountFlags, mountOptions := lxdResolveMountoptions(s.getRBDMountOptions())
+	if RBDFilesystem == "xfs" {
+		idx := strings.Index(mountOptions, "nouuid")
+		if idx < 0 {
+			mountOptions += ",nouuid"
+		}
+	}
+
 	err = tryMount(
 		RBDDevPath,
 		containerMntPoint,

From f0b21676b8bd4d9d1d22ececac03e28360419e3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 26 Apr 2019 19:16:20 -0400
Subject: [PATCH 3/5] api: Add cluster_internal_copy API extension
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 5b93381a6e..67a93d82c5 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -747,3 +747,8 @@ Adds support for RBAC (role based access control). This introduces new config ke
   * rbac.agent.username
   * rbac.agent.private\_key
   * rbac.agent.public\_key
+
+## cluster\_internal\_copy
+This makes it possible to do a normal "POST /1.0/containers" to copy a
+container between cluster nodes with LXD internally detecting whether a
+migration is required.
diff --git a/shared/version/api.go b/shared/version/api.go
index 8e25895f8e..02546c9005 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -150,6 +150,7 @@ var APIExtensions = []string{
 	"network_nat_address",
 	"container_nic_routes",
 	"rbac",
+	"cluster_internal_copy",
 }
 
 // APIExtensionsCount returns the number of available API extensions.

From d901df4c4f3998c189bbe54cedbd4d31f7c8bb2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 26 Apr 2019 19:49:06 -0400
Subject: [PATCH 4/5] client: Add support for cluster_internal_copy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client/lxd_containers.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/lxd_containers.go b/client/lxd_containers.go
index eaa3f1fa94..4351c7f632 100644
--- a/client/lxd_containers.go
+++ b/client/lxd_containers.go
@@ -355,7 +355,7 @@ func (r *ProtocolLXD) CopyContainer(source ContainerServer, container api.Contai
 	}
 
 	// Optimization for the local copy case
-	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || container.Location == r.clusterTarget) {
+	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || container.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
 		// Project handling
 		if destInfo.Project != sourceInfo.Project {
 			if !r.HasExtension("container_copy_project") {
@@ -1121,7 +1121,7 @@ func (r *ProtocolLXD) CopyContainerSnapshot(source ContainerServer, containerNam
 	}
 
 	// Optimization for the local copy case
-	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || container.Location == r.clusterTarget) {
+	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || container.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
 		// Project handling
 		if destInfo.Project != sourceInfo.Project {
 			if !r.HasExtension("container_copy_project") {

From 3540585adb22b4fcde2a1a8f0850ad0153e1fb6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 26 Apr 2019 19:49:53 -0400
Subject: [PATCH 5/5] lxd/containers: Add support for internal cluster copy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #5700

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/containers_post.go | 243 ++++++++++++++++++++++++++++++++---------
 1 file changed, 191 insertions(+), 52 deletions(-)

diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index 5d6229e36b..c51d85510e 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -231,57 +231,9 @@ func createFromMigration(d *Daemon, project string, req *api.ContainersPost) Res
 		}
 	}
 
-	// Grab the container's root device if one is specified
-	storagePool := ""
-	storagePoolProfile := ""
-
-	localRootDiskDeviceKey, localRootDiskDevice, _ := shared.GetRootDiskDevice(req.Devices)
-	if localRootDiskDeviceKey != "" {
-		storagePool = localRootDiskDevice["pool"]
-	}
-
-	// Handle copying/moving between two storage-api LXD instances.
-	if storagePool != "" {
-		_, err := d.cluster.StoragePoolGetID(storagePool)
-		if err == db.ErrNoSuchObject {
-			storagePool = ""
-			// Unset the local root disk device storage pool if not
-			// found.
-			localRootDiskDevice["pool"] = ""
-		}
-	}
-
-	// If we don't have a valid pool yet, look through profiles
-	if storagePool == "" {
-		for _, pName := range req.Profiles {
-			_, p, err := d.cluster.ProfileGet(project, pName)
-			if err != nil {
-				return SmartError(err)
-			}
-
-			k, v, _ := shared.GetRootDiskDevice(p.Devices)
-			if k != "" && v["pool"] != "" {
-				// Keep going as we want the last one in the profile chain
-				storagePool = v["pool"]
-				storagePoolProfile = pName
-			}
-		}
-	}
-
-	// If there is just a single pool in the database, use that
-	if storagePool == "" {
-		logger.Debugf("No valid storage pool in the container's local root disk device and profiles found")
-		pools, err := d.cluster.StoragePools()
-		if err != nil {
-			if err == db.ErrNoSuchObject {
-				return BadRequest(fmt.Errorf("This LXD instance does not have any storage pools configured"))
-			}
-			return SmartError(err)
-		}
-
-		if len(pools) == 1 {
-			storagePool = pools[0]
-		}
+	storagePool, storagePoolProfile, localRootDiskDeviceKey, localRootDiskDevice, resp := containerFindStoragePool(d, project, req)
+	if resp != nil {
+		return resp
 	}
 
 	if storagePool == "" {
@@ -496,9 +448,56 @@ func createFromCopy(d *Daemon, project string, req *api.ContainersPost) Response
 		return SmartError(err)
 	}
 
+	// Check if we need to redirect to migration
+	clustered, err := cluster.Enabled(d.db)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	// When clustered, use the node name, otherwise use the hostname.
+	if clustered {
+		var serverName string
+		err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
+			serverName, err = tx.NodeName()
+			return err
+		})
+		if err != nil {
+			return SmartError(err)
+		}
+
+		if serverName != source.Location() {
+			// Check if we are copying from a ceph-based container.
+			sourcePoolName, err := d.cluster.ContainerPool(source.Project(), source.Name())
+			if err != nil {
+				err = errors.Wrap(err, "Failed to fetch container's pool name")
+				return SmartError(err)
+			}
+
+			destPoolName, _, _, _, resp := containerFindStoragePool(d, targetProject, req)
+			if resp != nil {
+				return resp
+			}
+
+			if sourcePoolName != destPoolName {
+				// Redirect to migration
+				return clusterCopyContainerInternal(d, source, project, req)
+			}
+
+			_, pool, err := d.cluster.StoragePoolGet(sourcePoolName)
+			if err != nil {
+				err = errors.Wrap(err, "Failed to fetch container's pool info")
+				return SmartError(err)
+			}
+
+			if pool.Driver != "ceph" {
+				// Redirect to migration
+				return clusterCopyContainerInternal(d, source, project, req)
+			}
+		}
+	}
+
 	// Config override
 	sourceConfig := source.LocalConfig()
-
 	if req.Config == nil {
 		req.Config = make(map[string]string)
 	}
@@ -795,3 +794,143 @@ func containersPost(d *Daemon, r *http.Request) Response {
 		return BadRequest(fmt.Errorf("unknown source type %s", req.Source.Type))
 	}
 }
+
+func containerFindStoragePool(d *Daemon, project string, req *api.ContainersPost) (string, string, string, map[string]string, Response) {
+	// Grab the container's root device if one is specified
+	storagePool := ""
+	storagePoolProfile := ""
+
+	localRootDiskDeviceKey, localRootDiskDevice, _ := shared.GetRootDiskDevice(req.Devices)
+	if localRootDiskDeviceKey != "" {
+		storagePool = localRootDiskDevice["pool"]
+	}
+
+	// Handle copying/moving between two storage-api LXD instances.
+	if storagePool != "" {
+		_, err := d.cluster.StoragePoolGetID(storagePool)
+		if err == db.ErrNoSuchObject {
+			storagePool = ""
+			// Unset the local root disk device storage pool if not
+			// found.
+			localRootDiskDevice["pool"] = ""
+		}
+	}
+
+	// If we don't have a valid pool yet, look through profiles
+	if storagePool == "" {
+		for _, pName := range req.Profiles {
+			_, p, err := d.cluster.ProfileGet(project, pName)
+			if err != nil {
+				return "", "", "", nil, SmartError(err)
+			}
+
+			k, v, _ := shared.GetRootDiskDevice(p.Devices)
+			if k != "" && v["pool"] != "" {
+				// Keep going as we want the last one in the profile chain
+				storagePool = v["pool"]
+				storagePoolProfile = pName
+			}
+		}
+	}
+
+	// If there is just a single pool in the database, use that
+	if storagePool == "" {
+		logger.Debugf("No valid storage pool in the container's local root disk device and profiles found")
+		pools, err := d.cluster.StoragePools()
+		if err != nil {
+			if err == db.ErrNoSuchObject {
+				return "", "", "", nil, BadRequest(fmt.Errorf("This LXD instance does not have any storage pools configured"))
+			}
+			return "", "", "", nil, SmartError(err)
+		}
+
+		if len(pools) == 1 {
+			storagePool = pools[0]
+		}
+	}
+
+	return storagePool, storagePoolProfile, localRootDiskDeviceKey, localRootDiskDevice, nil
+}
+
+func clusterCopyContainerInternal(d *Daemon, source container, project string, req *api.ContainersPost) Response {
+	name := req.Source.Source
+
+	// Locate the source of the container
+	var nodeAddress string
+	err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+		var err error
+
+		// Load source node.
+		nodeAddress, err = tx.ContainerNodeAddress(project, name)
+		if err != nil {
+			return errors.Wrap(err, "Failed to get address of container's node")
+		}
+
+		return nil
+	})
+	if err != nil {
+		return SmartError(err)
+	}
+
+	if nodeAddress == "" {
+		return BadRequest(fmt.Errorf("The container source is currently offline"))
+	}
+
+	// Connect to the container source
+	client, err := cluster.Connect(nodeAddress, d.endpoints.NetworkCert(), false)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	client = client.UseProject(source.Project())
+
+	// Setup websockets
+	var opAPI api.Operation
+	if shared.IsSnapshot(req.Source.Source) {
+		cName, sName, _ := containerGetParentAndSnapshotName(req.Source.Source)
+
+		pullReq := api.ContainerSnapshotPost{
+			Migration: true,
+			Live:      req.Source.Live,
+			Name:      req.Name,
+		}
+
+		op, err := client.MigrateContainerSnapshot(cName, sName, pullReq)
+		if err != nil {
+			return SmartError(err)
+		}
+
+		opAPI = op.Get()
+	} else {
+		pullReq := api.ContainerPost{
+			Migration:     true,
+			Live:          req.Source.Live,
+			ContainerOnly: req.Source.ContainerOnly,
+			Name:          req.Name,
+		}
+
+		op, err := client.MigrateContainer(req.Source.Source, pullReq)
+		if err != nil {
+			return SmartError(err)
+		}
+
+		opAPI = op.Get()
+	}
+
+	websockets := map[string]string{}
+	for k, v := range opAPI.Metadata {
+		websockets[k] = v.(string)
+	}
+
+	// Reset the source for a migration
+	req.Source.Type = "migration"
+	req.Source.Certificate = string(d.endpoints.NetworkCert().PublicKey())
+	req.Source.Mode = "pull"
+	req.Source.Operation = fmt.Sprintf("https://%s/1.0/operations/%s", nodeAddress, opAPI.ID)
+	req.Source.Websockets = websockets
+	req.Source.Source = ""
+	req.Source.Project = ""
+
+	// Run the migration
+	return createFromMigration(d, project, req)
+}


More information about the lxc-devel mailing list