[lxc-devel] [pylxd/master] Added minimal cluster support

felix-engelmann on Github lxc-bot at linuxcontainers.org
Mon Oct 8 21:09:46 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 401 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20181008/daba4a81/attachment.bin>
-------------- next part --------------
From 676d0114f6622fe345a861f0c5cf3790e13f0f4b Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Mon, 24 Sep 2018 15:44:06 +0200
Subject: [PATCH 1/5] cluster endpoint read support

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py          |  3 +++
 pylxd/managers.py        |  3 +++
 pylxd/models/__init__.py |  1 +
 pylxd/models/node.py     | 52 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 59 insertions(+)
 create mode 100644 pylxd/models/node.py

diff --git a/pylxd/client.py b/pylxd/client.py
index e3558f40..90df3096 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -71,6 +71,8 @@ def __getattr__(self, name):
         # Special case for storage_pools which needs to become 'storage-pools'
         if name == 'storage_pools':
             name = 'storage-pools'
+        if name == 'nodes':
+            name = 'cluster/members'
         return self.__class__('{}/{}'.format(self._api_endpoint, name),
                               cert=self.session.cert,
                               verify=self.session.verify)
@@ -296,6 +298,7 @@ def __init__(
                 requests.exceptions.InvalidURL):
             raise exceptions.ClientConnectionFailed()
 
+        self.nodes = managers.NodeManager(self)
         self.certificates = managers.CertificateManager(self)
         self.containers = managers.ContainerManager(self)
         self.images = managers.ImageManager(self)
diff --git a/pylxd/managers.py b/pylxd/managers.py
index a7dbb7ef..40bc0c53 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -27,6 +27,9 @@ def __init__(self, *args, **kwargs):
         return super(BaseManager, self).__init__()
 
 
+class NodeManager(BaseManager):
+    manager_for = 'pylxd.models.Node'
+
 class CertificateManager(BaseManager):
     manager_for = 'pylxd.models.Certificate'
 
diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py
index 74b82ad5..d2f89a21 100644
--- a/pylxd/models/__init__.py
+++ b/pylxd/models/__init__.py
@@ -1,3 +1,4 @@
+from pylxd.models.node import Node  # NOQA
 from pylxd.models.certificate import Certificate  # NOQA
 from pylxd.models.container import Container, Snapshot  # NOQA
 from pylxd.models.image import Image  # NOQA
diff --git a/pylxd/models/node.py b/pylxd/models/node.py
new file mode 100644
index 00000000..009bebf2
--- /dev/null
+++ b/pylxd/models/node.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import binascii
+
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.serialization import Encoding
+
+from pylxd.models import _model as model
+
+
+class Node(model.Model):
+    """A LXD certificate."""
+
+    name = model.Attribute()
+    url = model.Attribute()
+    database = model.Attribute()
+    state = model.Attribute()
+
+    @classmethod
+    def get(cls, client, name):
+        """Get a certificate by fingerprint."""
+        response = client.api.nodes[name].get()
+
+        return cls(client, **response.json()['metadata'])
+
+    @classmethod
+    def all(cls, client):
+        """Get all certificates."""
+        response = client.api.nodes.get()
+
+        nodes = []
+        for node in response.json()['metadata']:
+            name = node.split('/')[-1]
+            nodes.append(cls(client, name=name))
+        return nodes
+
+    @property
+    def api(self):
+        return self.client.api.nodes[self.name]

From 420c0d9e1f88123193f1dffe4d86cf0230372bce Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Mon, 24 Sep 2018 15:52:01 +0200
Subject: [PATCH 2/5] renamed node to more close cluster_member

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py                             |  4 ++--
 pylxd/managers.py                           |  4 ++--
 pylxd/models/__init__.py                    |  2 +-
 pylxd/models/{node.py => cluster_member.py} | 15 ++++-----------
 4 files changed, 9 insertions(+), 16 deletions(-)
 rename pylxd/models/{node.py => cluster_member.py} (76%)

diff --git a/pylxd/client.py b/pylxd/client.py
index 90df3096..dc60f0da 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -71,7 +71,7 @@ def __getattr__(self, name):
         # Special case for storage_pools which needs to become 'storage-pools'
         if name == 'storage_pools':
             name = 'storage-pools'
-        if name == 'nodes':
+        if name == 'cluster_members':
             name = 'cluster/members'
         return self.__class__('{}/{}'.format(self._api_endpoint, name),
                               cert=self.session.cert,
@@ -298,7 +298,7 @@ def __init__(
                 requests.exceptions.InvalidURL):
             raise exceptions.ClientConnectionFailed()
 
-        self.nodes = managers.NodeManager(self)
+        self.cluster_members = managers.ClusterMemberManager(self)
         self.certificates = managers.CertificateManager(self)
         self.containers = managers.ContainerManager(self)
         self.images = managers.ImageManager(self)
diff --git a/pylxd/managers.py b/pylxd/managers.py
index 40bc0c53..11447807 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -27,8 +27,8 @@ def __init__(self, *args, **kwargs):
         return super(BaseManager, self).__init__()
 
 
-class NodeManager(BaseManager):
-    manager_for = 'pylxd.models.Node'
+class ClusterMemberManager(BaseManager):
+    manager_for = 'pylxd.models.ClusterMember'
 
 class CertificateManager(BaseManager):
     manager_for = 'pylxd.models.Certificate'
diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py
index d2f89a21..56c8693d 100644
--- a/pylxd/models/__init__.py
+++ b/pylxd/models/__init__.py
@@ -1,4 +1,4 @@
-from pylxd.models.node import Node  # NOQA
+from pylxd.models.cluster_member import ClusterMember  # NOQA
 from pylxd.models.certificate import Certificate  # NOQA
 from pylxd.models.container import Container, Snapshot  # NOQA
 from pylxd.models.image import Image  # NOQA
diff --git a/pylxd/models/node.py b/pylxd/models/cluster_member.py
similarity index 76%
rename from pylxd/models/node.py
rename to pylxd/models/cluster_member.py
index 009bebf2..96f7bd7c 100644
--- a/pylxd/models/node.py
+++ b/pylxd/models/cluster_member.py
@@ -11,17 +11,10 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-import binascii
-
-from cryptography import x509
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.serialization import Encoding
-
 from pylxd.models import _model as model
 
 
-class Node(model.Model):
+class ClusterMember(model.Model):
     """A LXD certificate."""
 
     name = model.Attribute()
@@ -32,14 +25,14 @@ class Node(model.Model):
     @classmethod
     def get(cls, client, name):
         """Get a certificate by fingerprint."""
-        response = client.api.nodes[name].get()
+        response = client.api.cluster_members[name].get()
 
         return cls(client, **response.json()['metadata'])
 
     @classmethod
     def all(cls, client):
         """Get all certificates."""
-        response = client.api.nodes.get()
+        response = client.api.cluster_members.get()
 
         nodes = []
         for node in response.json()['metadata']:
@@ -49,4 +42,4 @@ def all(cls, client):
 
     @property
     def api(self):
-        return self.client.api.nodes[self.name]
+        return self.client.api.cluster_members[self.name]

From 5e79bae092de28f98ce1e711ca2b38845f8ff61a Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 26 Sep 2018 00:10:01 +0200
Subject: [PATCH 3/5] Added functionality to specify target cluster member in
 containers.create

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py           | 10 +++++++++-
 pylxd/models/container.py |  4 ++--
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index dc60f0da..9334fc93 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -153,7 +153,15 @@ def get(self, *args, **kwargs):
     def post(self, *args, **kwargs):
         """Perform an HTTP POST."""
         kwargs['timeout'] = kwargs.get('timeout', self._timeout)
-        response = self.session.post(self._api_endpoint, *args, **kwargs)
+        target = kwargs.get('target', None)
+        kwargs.pop("target", None)
+
+        if target is not None:
+            endpoint="{}?target={}".format(self._api_endpoint,target)
+        else:
+            endpoint = self._api_endpoint
+
+        response = self.session.post(endpoint, *args, **kwargs)
         # Prior to LXD 2.0.3, successful synchronous requests returned 200,
         # rather than 201.
         self._assert_response(response, allowed_status_codes=(200, 201, 202))
diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index a985691f..11570fca 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -256,9 +256,9 @@ def all(cls, client):
         return containers
 
     @classmethod
-    def create(cls, client, config, wait=False):
+    def create(cls, client, config, wait=False, target=None):
         """Create a new container config."""
-        response = client.api.containers.post(json=config)
+        response = client.api.containers.post(json=config, target=target)
 
         if wait:
             client.operations.wait_for_operation(response.json()['operation'])

From 4423a1a9ac2be3498d299c444d88d20124d9bc6d Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Mon, 8 Oct 2018 22:56:22 +0200
Subject: [PATCH 4/5] Added tests for Cluster Members and Targeted create
 container

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py                           |  2 +-
 pylxd/managers.py                         |  1 +
 pylxd/tests/mock_lxd.py                   | 59 +++++++++++++++++++++++
 pylxd/tests/models/test_cluster_member.py | 33 +++++++++++++
 pylxd/tests/models/test_container.py      | 10 ++++
 5 files changed, 104 insertions(+), 1 deletion(-)
 create mode 100644 pylxd/tests/models/test_cluster_member.py

diff --git a/pylxd/client.py b/pylxd/client.py
index 9334fc93..0293ba30 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -157,7 +157,7 @@ def post(self, *args, **kwargs):
         kwargs.pop("target", None)
 
         if target is not None:
-            endpoint="{}?target={}".format(self._api_endpoint,target)
+            endpoint = "{}?target={}".format(self._api_endpoint, target)
         else:
             endpoint = self._api_endpoint
 
diff --git a/pylxd/managers.py b/pylxd/managers.py
index 11447807..5ed834bd 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -30,6 +30,7 @@ def __init__(self, *args, **kwargs):
 class ClusterMemberManager(BaseManager):
     manager_for = 'pylxd.models.ClusterMember'
 
+
 class CertificateManager(BaseManager):
     manager_for = 'pylxd.models.Certificate'
 
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index c8c21efd..5139f44a 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -7,6 +7,12 @@ def containers_POST(request, context):
         'type': 'async',
         'operation': 'operation-abc'})
 
+def containers_remote_POST(request, context):
+    context.status_code = 202
+    return json.dumps({
+        'type': 'async',
+        'operation': 'operation-abc'})
+
 
 def container_POST(request, context):
     context.status_code = 202
@@ -192,6 +198,31 @@ def snapshot_DELETE(request, context):
     },
 
 
+    # Cluster Members
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': [
+                'http://pylxd.test/1.0/certificates/an-member',
+                'http://pylxd.test/1.0/certificates/nd-member',
+            ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/cluster/members$',
+    },
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': {
+                "name": "an-member",
+                "url": "https://10.1.1.101:8443",
+                "database": "true",
+                "state": "Online",
+            }}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/cluster/members/an-member$',  # NOQA
+    },
+
+
     # Containers
     {
         'text': json.dumps({
@@ -212,6 +243,11 @@ def snapshot_DELETE(request, context):
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers$',
     },
+    {
+        'text': containers_remote_POST,
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers\?target=an-remote',
+    },
     {
         'json': {
             'type': 'sync',
@@ -293,6 +329,29 @@ def snapshot_DELETE(request, context):
         'method': 'GET',
         'url': r'^http://pylxd.test/1.0/containers/an-container/state$',  # NOQA
     },
+    {
+        'json': {
+            'type': 'sync',
+            'metadata': {
+                'name': 'an-new-remote-container',
+
+                'architecture': "x86_64",
+                'config': {
+                    'security.privileged': "true",
+                },
+                'created_at': "1983-06-16T00:00:00-00:00",
+                'last_used_at': "1983-06-16T00:00:00-00:00",
+                'description': "Some description",
+                'location':"an-remote",
+                'status': "Running",
+                'status_code': 103,
+                'unsupportedbypylxd': "This attribute is not supported by "\
+                    "pylxd. We want to test whether the mere presence of it "\
+                    "makes it crash."
+            }},
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/containers/an-new-remote-container$',
+    },
     {
         'status_code': 202,
         'json': {
diff --git a/pylxd/tests/models/test_cluster_member.py b/pylxd/tests/models/test_cluster_member.py
new file mode 100644
index 00000000..a36e40fd
--- /dev/null
+++ b/pylxd/tests/models/test_cluster_member.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import os
+
+from pylxd import models
+from pylxd.tests import testing
+
+
+class TestClusterMember(testing.PyLXDTestCase):
+    """Tests for pylxd.models.ClusterMember."""
+
+    def test_get(self):
+        """A cluster member is retrieved."""
+        member = self.client.cluster_members.get('an-member')
+
+        self.assertEqual('https://10.1.1.101:8443', member.url)
+
+    def test_all(self):
+        """All cluster members are returned."""
+        members = self.client.cluster_members.all()
+
+        self.assertIn('an-member', [m.name for m in members])
diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py
index b689387e..d18e3fc8 100644
--- a/pylxd/tests/models/test_container.py
+++ b/pylxd/tests/models/test_container.py
@@ -78,6 +78,16 @@ def test_create(self):
 
         self.assertEqual(config['name'], an_new_container.name)
 
+    def test_create_remote(self):
+        """A new container is created at target."""
+        config = {'name': 'an-new-remote-container'}
+
+        an_new_remote_container = models.Container.create(
+            self.client, config, wait=True, target="an-remote")
+
+        self.assertEqual(config['name'], an_new_remote_container.name)
+        self.assertEqual("an-remote", an_new_remote_container.location)
+
     def test_exists(self):
         """A container exists."""
         name = 'an-container'

From 34d1601652f55b4f2ac7243e641e9249f3bb353e Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Mon, 8 Oct 2018 22:58:39 +0200
Subject: [PATCH 5/5] Added to contributors

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 CONTRIBUTORS.rst | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst
index 86035b24..a992363a 100644
--- a/CONTRIBUTORS.rst
+++ b/CONTRIBUTORS.rst
@@ -34,5 +34,6 @@ These are the contributors to pylxd according to the Github repository.
  chrismacnaughton Chris MacNaughton
  ppkt             Karol Werner
  mrtc0            Kohei Morita
+ felix-engelmann  Felix Engelmann
  ===============  ==================================
 


More information about the lxc-devel mailing list