[lxc-devel] [lxd/master] Fix listing and creating cluster containers inside projects
freeekanayaka on Github
lxc-bot at linuxcontainers.org
Mon Oct 15 13:54:49 UTC 2018
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 686 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20181015/a607ecbc/attachment.bin>
-------------- next part --------------
From b8d35a02146e163c7c44adce21824931ec6fa658 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Mon, 15 Oct 2018 14:24:29 +0200
Subject: [PATCH 1/3] Make containers on other nodes visible also in the
non-default project
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/container_console.go | 2 +-
lxd/container_exec.go | 2 +-
lxd/containers_get.go | 12 ++++++---
lxd/containers_post.go | 7 +++--
lxd/response.go | 13 +++++++---
test/main.sh | 1 +
test/suites/clustering.sh | 54 +++++++++++++++++++++++++++++++++++++++
7 files changed, 80 insertions(+), 11 deletions(-)
diff --git a/lxd/container_console.go b/lxd/container_console.go
index 821b9ff556..f5270af7e2 100644
--- a/lxd/container_console.go
+++ b/lxd/container_console.go
@@ -282,7 +282,7 @@ func containerConsolePost(d *Daemon, r *http.Request) Response {
}
opAPI := op.Get()
- return ForwardedOperationResponse(&opAPI)
+ return ForwardedOperationResponse(project, &opAPI)
}
c, err := containerLoadByProjectAndName(d.State(), project, name)
diff --git a/lxd/container_exec.go b/lxd/container_exec.go
index c73f403558..2a1d5049e0 100644
--- a/lxd/container_exec.go
+++ b/lxd/container_exec.go
@@ -360,7 +360,7 @@ func containerExecPost(d *Daemon, r *http.Request) Response {
}
opAPI := op.Get()
- return ForwardedOperationResponse(&opAPI)
+ return ForwardedOperationResponse(project, &opAPI)
}
c, err := containerLoadByProjectAndName(d.State(), project, name)
diff --git a/lxd/containers_get.go b/lxd/containers_get.go
index 1115f7265e..d93190be33 100644
--- a/lxd/containers_get.go
+++ b/lxd/containers_get.go
@@ -151,7 +151,7 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
cert := d.endpoints.NetworkCert()
if recursion == 1 {
- cs, err := doContainersGetFromNode(address, cert)
+ cs, err := doContainersGetFromNode(project, address, cert)
if err != nil {
for _, name := range containers {
resultListAppend(name, api.Container{}, err)
@@ -167,7 +167,7 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
return
}
- cs, err := doContainersFullGetFromNode(address, cert)
+ cs, err := doContainersFullGetFromNode(project, address, cert)
if err != nil {
for _, name := range containers {
resultFullListAppend(name, api.ContainerFull{}, err)
@@ -262,13 +262,15 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
// Fetch information about the containers on the given remote node, using the
// rest API and with a timeout of 30 seconds.
-func doContainersGetFromNode(node string, cert *shared.CertInfo) ([]api.Container, error) {
+func doContainersGetFromNode(project, node string, cert *shared.CertInfo) ([]api.Container, error) {
f := func() ([]api.Container, error) {
client, err := cluster.Connect(node, cert, true)
if err != nil {
return nil, errors.Wrapf(err, "Failed to connect to node %s", node)
}
+ client = client.UseProject(project)
+
containers, err := client.GetContainers()
if err != nil {
return nil, errors.Wrapf(err, "Failed to get containers from node %s", node)
@@ -297,13 +299,15 @@ func doContainersGetFromNode(node string, cert *shared.CertInfo) ([]api.Containe
return containers, err
}
-func doContainersFullGetFromNode(node string, cert *shared.CertInfo) ([]api.ContainerFull, error) {
+func doContainersFullGetFromNode(project, node string, cert *shared.CertInfo) ([]api.ContainerFull, error) {
f := func() ([]api.ContainerFull, error) {
client, err := cluster.Connect(node, cert, true)
if err != nil {
return nil, errors.Wrapf(err, "Failed to connect to node %s", node)
}
+ client = client.UseProject(project)
+
containers, err := client.GetContainersFull()
if err != nil {
return nil, errors.Wrapf(err, "Failed to get containers from node %s", node)
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index 0594d7f174..72076d4c66 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -660,14 +660,17 @@ func containersPost(d *Daemon, r *http.Request) Response {
return SmartError(err)
}
+ client = client.UseProject(project)
+ client = client.UseTarget(targetNode)
+
logger.Debugf("Forward container post request to %s", address)
- op, err := client.UseTarget(targetNode).CreateContainer(req)
+ op, err := client.CreateContainer(req)
if err != nil {
return SmartError(err)
}
opAPI := op.Get()
- return ForwardedOperationResponse(&opAPI)
+ return ForwardedOperationResponse(project, &opAPI)
}
}
diff --git a/lxd/response.go b/lxd/response.go
index d3490db2f0..62bcc9669f 100644
--- a/lxd/response.go
+++ b/lxd/response.go
@@ -386,11 +386,15 @@ func OperationResponse(op *operation) Response {
//
// Returned when the operation has been created on another node
type forwardedOperationResponse struct {
- op *api.Operation
+ op *api.Operation
+ project string
}
func (r *forwardedOperationResponse) Render(w http.ResponseWriter) error {
url := fmt.Sprintf("/%s/operations/%s", version.APIVersion, r.op.ID)
+ if r.project != "" {
+ url += fmt.Sprintf("?project=%s", r.project)
+ }
body := api.ResponseRaw{
Type: api.AsyncResponse,
@@ -412,8 +416,11 @@ func (r *forwardedOperationResponse) String() string {
// ForwardedOperationResponse creates a response that forwards the metadata of
// an operation created on another node.
-func ForwardedOperationResponse(op *api.Operation) Response {
- return &forwardedOperationResponse{op}
+func ForwardedOperationResponse(project string, op *api.Operation) Response {
+ return &forwardedOperationResponse{
+ op: op,
+ project: project,
+ }
}
// Error response
diff --git a/test/main.sh b/test/main.sh
index eb1f02700a..e1f9be3b33 100755
--- a/test/main.sh
+++ b/test/main.sh
@@ -227,6 +227,7 @@ run_test test_clustering_publish "clustering publish"
run_test test_clustering_profiles "clustering profiles"
run_test test_clustering_join_api "clustering join api"
run_test test_clustering_shutdown_nodes "clustering shutdown"
+run_test test_clustering_projects "clustering projects"
#run_test test_clustering_upgrade "clustering upgrade"
# shellcheck disable=SC2034
diff --git a/test/suites/clustering.sh b/test/suites/clustering.sh
index 17af533c07..8377ed2342 100644
--- a/test/suites/clustering.sh
+++ b/test/suites/clustering.sh
@@ -984,3 +984,57 @@ test_clustering_shutdown_nodes() {
kill_lxd "${LXD_ONE_DIR}"
kill_lxd "${LXD_THREE_DIR}"
}
+
+test_clustering_projects() {
+ setup_clustering_bridge
+ prefix="lxd$$"
+ bridge="${prefix}"
+
+ setup_clustering_netns 1
+ LXD_ONE_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
+ chmod +x "${LXD_ONE_DIR}"
+ ns1="${prefix}1"
+ spawn_lxd_and_bootstrap_cluster "${ns1}" "${bridge}" "${LXD_ONE_DIR}"
+
+ # Add a newline at the end of each line. YAML as weird rules..
+ cert=$(sed ':a;N;$!ba;s/\n/\n\n/g' "${LXD_ONE_DIR}/server.crt")
+
+ # Spawn a second node
+ setup_clustering_netns 2
+ LXD_TWO_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
+ chmod +x "${LXD_TWO_DIR}"
+ ns2="${prefix}2"
+ spawn_lxd_and_join_cluster "${ns2}" "${bridge}" "${cert}" 2 1 "${LXD_TWO_DIR}"
+
+ LXD_DIR="${LXD_TWO_DIR}" ensure_import_testimage
+
+ # Create a test project
+ LXD_DIR="${LXD_ONE_DIR}" lxc project create p1 -c features.images=false
+ LXD_DIR="${LXD_ONE_DIR}" lxc project switch p1
+ LXD_DIR="${LXD_ONE_DIR}" lxc profile device add default root disk path="/" pool="data"
+
+ # Create a container in the project.
+ #LXD_DIR="${LXD_ONE_DIR}" deps/import-busybox --project p1 --alias testimage
+ LXD_DIR="${LXD_ONE_DIR}" lxc init --target node1 testimage c1
+
+ # The container is visible through both nodes
+ LXD_DIR="${LXD_ONE_DIR}" lxc list | grep -q c1
+ LXD_DIR="${LXD_TWO_DIR}" lxc list | grep -q c1
+
+ LXD_DIR="${LXD_ONE_DIR}" lxc delete c1
+ #LXD_DIR="${LXD_ONE_DIR}" lxc image delete testimage
+
+ LXD_DIR="${LXD_ONE_DIR}" lxc project switch default
+
+ LXD_DIR="${LXD_TWO_DIR}" lxd shutdown
+ LXD_DIR="${LXD_ONE_DIR}" lxd shutdown
+ sleep 2
+ rm -f "${LXD_TWO_DIR}/unix.socket"
+ rm -f "${LXD_ONE_DIR}/unix.socket"
+
+ teardown_clustering_netns
+ teardown_clustering_bridge
+
+ kill_lxd "${LXD_ONE_DIR}"
+ kill_lxd "${LXD_TWO_DIR}"
+}
From 36d5b09d1379369c32f21d367a34cbfc86bc93fb Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Mon, 15 Oct 2018 15:00:11 +0200
Subject: [PATCH 2/3] Propagate events about all projects to all cluster nodes
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/api_project.go | 4 ++++
lxd/cluster/events.go | 5 +++++
lxd/events.go | 2 +-
test/suites/clustering.sh | 2 +-
4 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/lxd/api_project.go b/lxd/api_project.go
index b4f7c25259..0c5d6fc605 100644
--- a/lxd/api_project.go
+++ b/lxd/api_project.go
@@ -84,6 +84,10 @@ func apiProjectsPost(d *Daemon, r *http.Request) Response {
return BadRequest(fmt.Errorf("Project names may not contain slashes"))
}
+ if project.Name == "*" {
+ return BadRequest(fmt.Errorf("Reserved project name"))
+ }
+
if shared.StringInSlice(project.Name, []string{".", ".."}) {
return BadRequest(fmt.Errorf("Invalid project name '%s'", project.Name))
}
diff --git a/lxd/cluster/events.go b/lxd/cluster/events.go
index cd55cbdb51..93cb77a91f 100644
--- a/lxd/cluster/events.go
+++ b/lxd/cluster/events.go
@@ -116,5 +116,10 @@ func eventsConnect(address string, cert *shared.CertInfo) (*lxd.EventListener, e
if err != nil {
return nil, err
}
+
+ // Set the project to the special wildcard in order to get notified
+ // about all events across all projects.
+ client = client.UseProject("*")
+
return client.GetEvents()
}
diff --git a/lxd/events.go b/lxd/events.go
index d553e021eb..be5538db7e 100644
--- a/lxd/events.go
+++ b/lxd/events.go
@@ -148,7 +148,7 @@ func eventBroadcast(event shared.Jmap) error {
eventsLock.Lock()
listeners := eventListeners
for _, listener := range listeners {
- if event["project"] != "" && event["project"] != listener.project {
+ if event["project"] != "" && listener.project != "*" && event["project"] != listener.project {
continue
}
diff --git a/test/suites/clustering.sh b/test/suites/clustering.sh
index 8377ed2342..c7ab3b82f2 100644
--- a/test/suites/clustering.sh
+++ b/test/suites/clustering.sh
@@ -1015,7 +1015,7 @@ test_clustering_projects() {
# Create a container in the project.
#LXD_DIR="${LXD_ONE_DIR}" deps/import-busybox --project p1 --alias testimage
- LXD_DIR="${LXD_ONE_DIR}" lxc init --target node1 testimage c1
+ LXD_DIR="${LXD_ONE_DIR}" lxc init --target node2 testimage c1
# The container is visible through both nodes
LXD_DIR="${LXD_ONE_DIR}" lxc list | grep -q c1
From 3651ef6ee89bddaec1d5032a18db67f805caf39f Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Mon, 15 Oct 2018 15:51:12 +0200
Subject: [PATCH 3/3] Support creating project-bound container using an image
on another node
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
client/lxd_images.go | 12 +++++++-----
lxd/api_cluster_test.go | 2 +-
lxd/container.go | 6 +++++-
lxd/db/images.go | 4 ++--
test/suites/clustering.sh | 8 +++-----
5 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/client/lxd_images.go b/client/lxd_images.go
index 43b8237c9c..3b1bd11465 100644
--- a/client/lxd_images.go
+++ b/client/lxd_images.go
@@ -78,9 +78,9 @@ func (r *ProtocolLXD) GetPrivateImage(fingerprint string, secret string) (*api.I
image := api.Image{}
// Build the API path
- path := fmt.Sprintf("/images/%s", url.QueryEscape(fingerprint))
+ path := fmt.Sprintf("/images/%s?project=%s", url.QueryEscape(fingerprint), r.project)
if secret != "" {
- path = fmt.Sprintf("%s?secret=%s", path, url.QueryEscape(secret))
+ path += fmt.Sprintf("&secret=%s", url.QueryEscape(secret))
}
// Fetch the raw value
@@ -99,9 +99,11 @@ func (r *ProtocolLXD) GetPrivateImageFile(fingerprint string, secret string, req
return nil, fmt.Errorf("No file requested")
}
+ uri := fmt.Sprintf("/1.0/images/%s/export?project=%s", url.QueryEscape(fingerprint), r.project)
+
// Attempt to download from host
if secret == "" && shared.PathExists("/dev/lxd/sock") && os.Geteuid() == 0 {
- unixURI := fmt.Sprintf("http://unix.socket/1.0/images/%s/export", url.QueryEscape(fingerprint))
+ unixURI := fmt.Sprintf("http://unix.socket%s", uri)
// Setup the HTTP client
devlxdHTTP, err := unixHTTPClient(nil, "/dev/lxd/sock")
@@ -114,9 +116,9 @@ func (r *ProtocolLXD) GetPrivateImageFile(fingerprint string, secret string, req
}
// Build the URL
- uri := fmt.Sprintf("%s/1.0/images/%s/export", r.httpHost, url.QueryEscape(fingerprint))
+ uri = fmt.Sprintf("%s%s", r.httpHost, uri)
if secret != "" {
- uri = fmt.Sprintf("%s?secret=%s", uri, url.QueryEscape(secret))
+ uri += fmt.Sprintf("&secret=%s", url.QueryEscape(secret))
}
return lxdDownloadImage(fingerprint, uri, r.httpUserAgent, r.http, req)
diff --git a/lxd/api_cluster_test.go b/lxd/api_cluster_test.go
index c33776d500..c284ce3509 100644
--- a/lxd/api_cluster_test.go
+++ b/lxd/api_cluster_test.go
@@ -427,7 +427,7 @@ func TestCluster_LeaveWithImages(t *testing.T) {
// If we now associate the image with the other node as well, leaving
// the cluster is fine.
daemon = daemons[0]
- err = daemon.State().Cluster.ImageAssociateNode("abc")
+ err = daemon.State().Cluster.ImageAssociateNode("default", "abc")
require.NoError(t, err)
err = client.DeleteClusterMember("rusp-0", false)
diff --git a/lxd/container.go b/lxd/container.go
index f171bb38cc..ba8ac1edbc 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -773,11 +773,15 @@ func containerCreateFromImage(d *Daemon, args db.ContainerArgs, hash string) (co
if err != nil {
return nil, err
}
+
+ client = client.UseProject(args.Project)
+
err = imageImportFromNode(filepath.Join(d.os.VarDir, "images"), client, hash)
if err != nil {
return nil, err
}
- err = d.cluster.ImageAssociateNode(hash)
+
+ err = d.cluster.ImageAssociateNode(args.Project, hash)
if err != nil {
return nil, err
}
diff --git a/lxd/db/images.go b/lxd/db/images.go
index 11a8104416..56bd993fde 100644
--- a/lxd/db/images.go
+++ b/lxd/db/images.go
@@ -521,8 +521,8 @@ WHERE images.fingerprint = ?
// ImageAssociateNode creates a new entry in the images_nodes table for
// tracking that the current node has the given image.
-func (c *Cluster) ImageAssociateNode(fingerprint string) error {
- imageID, _, err := c.ImageGet("default", fingerprint, false, true)
+func (c *Cluster) ImageAssociateNode(project, fingerprint string) error {
+ imageID, _, err := c.ImageGet(project, fingerprint, false, true)
if err != nil {
return err
}
diff --git a/test/suites/clustering.sh b/test/suites/clustering.sh
index c7ab3b82f2..c3fb76a1a6 100644
--- a/test/suites/clustering.sh
+++ b/test/suites/clustering.sh
@@ -1006,15 +1006,13 @@ test_clustering_projects() {
ns2="${prefix}2"
spawn_lxd_and_join_cluster "${ns2}" "${bridge}" "${cert}" 2 1 "${LXD_TWO_DIR}"
- LXD_DIR="${LXD_TWO_DIR}" ensure_import_testimage
-
# Create a test project
- LXD_DIR="${LXD_ONE_DIR}" lxc project create p1 -c features.images=false
+ LXD_DIR="${LXD_ONE_DIR}" lxc project create p1
LXD_DIR="${LXD_ONE_DIR}" lxc project switch p1
LXD_DIR="${LXD_ONE_DIR}" lxc profile device add default root disk path="/" pool="data"
# Create a container in the project.
- #LXD_DIR="${LXD_ONE_DIR}" deps/import-busybox --project p1 --alias testimage
+ LXD_DIR="${LXD_ONE_DIR}" deps/import-busybox --project p1 --alias testimage
LXD_DIR="${LXD_ONE_DIR}" lxc init --target node2 testimage c1
# The container is visible through both nodes
@@ -1022,7 +1020,7 @@ test_clustering_projects() {
LXD_DIR="${LXD_TWO_DIR}" lxc list | grep -q c1
LXD_DIR="${LXD_ONE_DIR}" lxc delete c1
- #LXD_DIR="${LXD_ONE_DIR}" lxc image delete testimage
+ LXD_DIR="${LXD_ONE_DIR}" lxc image delete testimage
LXD_DIR="${LXD_ONE_DIR}" lxc project switch default
More information about the lxc-devel
mailing list