[lxc-devel] [pylxd/master] Added compatibility with operations project values

felix-engelmann on Github lxc-bot at linuxcontainers.org
Thu Dec 13 14:48:14 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 750 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20181213/87e14c29/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 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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
  ===============  ==================================
 

From 997d47336d63ce0787ffb5ffe7d8313c76064271 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Mon, 8 Oct 2018 23:15:08 +0200
Subject: [PATCH 06/21] pep8 compliance

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/tests/mock_lxd.py                   | 10 ++--------
 pylxd/tests/models/test_cluster_member.py |  2 --
 2 files changed, 2 insertions(+), 10 deletions(-)

diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 5139f44a..90bce5ad 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -7,12 +7,6 @@ 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
@@ -244,7 +238,7 @@ def snapshot_DELETE(request, context):
         'url': r'^http://pylxd.test/1.0/containers$',
     },
     {
-        'text': containers_remote_POST,
+        'text': containers_POST,
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers\?target=an-remote',
     },
@@ -342,7 +336,7 @@ def snapshot_DELETE(request, context):
                 '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",
+                'location': "an-remote",
                 'status': "Running",
                 'status_code': 103,
                 'unsupportedbypylxd': "This attribute is not supported by "\
diff --git a/pylxd/tests/models/test_cluster_member.py b/pylxd/tests/models/test_cluster_member.py
index a36e40fd..3e9b6356 100644
--- a/pylxd/tests/models/test_cluster_member.py
+++ b/pylxd/tests/models/test_cluster_member.py
@@ -11,9 +11,7 @@
 #    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
 
 

From fab37f584b57cec29426d021c1b958f591401e74 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Tue, 9 Oct 2018 01:16:15 +0200
Subject: [PATCH 07/21] Added integration test for cluster member info and all
 attributes

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 integration/test_cluster_members.py | 41 +++++++++++++++++++++++++++++
 pylxd/models/cluster_member.py      | 11 +++++---
 2 files changed, 48 insertions(+), 4 deletions(-)
 create mode 100644 integration/test_cluster_members.py

diff --git a/integration/test_cluster_members.py b/integration/test_cluster_members.py
new file mode 100644
index 00000000..a69cc360
--- /dev/null
+++ b/integration/test_cluster_members.py
@@ -0,0 +1,41 @@
+# 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.
+
+from integration.testing import IntegrationTestCase
+
+
+class ClusterMemberTestCase(IntegrationTestCase):
+
+    def setUp(self):
+        super(ClusterMemberTestCase, self).setUp()
+
+        if not self.client.has_api_extension('clustering'):
+            self.skipTest('Required LXD API extension not available!')
+
+
+class TestClusterMembers(ClusterMemberTestCase):
+    """Tests for `Client.cluster_members.`"""
+
+    def test_get(self):
+        """A cluster member is fetched by its name."""
+
+        members = self.client.cluster_members.all()
+
+        random_member_name = "%s" % members[0].name
+        random_member_url = "%s" % members[0].url
+
+        member = self.client.cluster_members.get(random_member_name)
+
+        new_url = "%s" % member.url
+        self.assertEqual(random_member_url, new_url)
diff --git a/pylxd/models/cluster_member.py b/pylxd/models/cluster_member.py
index 96f7bd7c..caa4abd2 100644
--- a/pylxd/models/cluster_member.py
+++ b/pylxd/models/cluster_member.py
@@ -17,10 +17,13 @@
 class ClusterMember(model.Model):
     """A LXD certificate."""
 
-    name = model.Attribute()
-    url = model.Attribute()
-    database = model.Attribute()
-    state = model.Attribute()
+    name = model.Attribute(readonly=True)
+    url = model.Attribute(readonly=True)
+    database = model.Attribute(readonly=True)
+    state = model.Attribute(readonly=True)
+    server_name = model.Attribute(readonly=True)
+    status = model.Attribute(readonly=True)
+    message = model.Attribute(readonly=True)
 
     @classmethod
     def get(cls, client, name):

From e6304b8f936a01ad0ed8afd971622deaa19e2aa9 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Tue, 11 Dec 2018 02:30:30 +0100
Subject: [PATCH 08/21] more efficient pop and params post parameter

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index 0293ba30..2836d6e4 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -153,15 +153,14 @@ def get(self, *args, **kwargs):
     def post(self, *args, **kwargs):
         """Perform an HTTP POST."""
         kwargs['timeout'] = kwargs.get('timeout', self._timeout)
-        target = kwargs.get('target', None)
-        kwargs.pop("target", None)
+        target = kwargs.pop("target", None)
 
         if target is not None:
-            endpoint = "{}?target={}".format(self._api_endpoint, target)
-        else:
-            endpoint = self._api_endpoint
+            params = kwargs.get("params", {})
+            params["target"] = target
+            kwargs["params"] = params
 
-        response = self.session.post(endpoint, *args, **kwargs)
+        response = self.session.post(self._api_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))

From 676badfe99eef05f30202ec8c5a9bdd367f30d31 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Tue, 11 Dec 2018 23:25:53 +0100
Subject: [PATCH 09/21] Copied approach of a singleton manager from ajkavanagh
 Adapted it that the class directly exposes .members @property of members with
 getter to _members did not work

missing tests

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/client.py                               |  4 +-
 pylxd/managers.py                             | 21 +++++++--
 pylxd/models/__init__.py                      |  2 +-
 pylxd/models/_model.py                        |  3 ++
 .../models/{cluster_member.py => cluster.py}  | 45 +++++++++++++++----
 5 files changed, 59 insertions(+), 16 deletions(-)
 rename pylxd/models/{cluster_member.py => cluster.py} (53%)

diff --git a/pylxd/client.py b/pylxd/client.py
index 0293ba30..5c622681 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -71,8 +71,6 @@ def __getattr__(self, name):
         # Special case for storage_pools which needs to become 'storage-pools'
         if name == 'storage_pools':
             name = 'storage-pools'
-        if name == 'cluster_members':
-            name = 'cluster/members'
         return self.__class__('{}/{}'.format(self._api_endpoint, name),
                               cert=self.session.cert,
                               verify=self.session.verify)
@@ -306,7 +304,7 @@ def __init__(
                 requests.exceptions.InvalidURL):
             raise exceptions.ClientConnectionFailed()
 
-        self.cluster_members = managers.ClusterMemberManager(self)
+        self.cluster = managers.ClusterManager(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 5ed834bd..958a2cfe 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -4,6 +4,8 @@
 import importlib
 import inspect
 
+from pylxd import managers
+
 
 class BaseManager(object):
     """A BaseManager class for handling collection operations."""
@@ -27,10 +29,6 @@ def __init__(self, *args, **kwargs):
         return super(BaseManager, self).__init__()
 
 
-class ClusterMemberManager(BaseManager):
-    manager_for = 'pylxd.models.ClusterMember'
-
-
 class CertificateManager(BaseManager):
     manager_for = 'pylxd.models.Certificate'
 
@@ -63,6 +61,21 @@ class StoragePoolManager(BaseManager):
     manager_for = 'pylxd.models.StoragePool'
 
 
+class ClusterMemberManager(BaseManager):
+    manager_for = 'pylxd.models.ClusterMember'
+
+
+class ClusterManager(BaseManager):
+
+    manager_for = 'pylxd.models.Cluster'
+
+    def __init__(self, client, *args, **kwargs):
+        super(ClusterManager, self).__init__(client, *args, **kwargs)
+        self._client = client
+        self.members = managers.ClusterMemberManager(client)
+
+
+
 @contextmanager
 def web_socket_manager(manager):
     try:
diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py
index 56c8693d..c71504cc 100644
--- a/pylxd/models/__init__.py
+++ b/pylxd/models/__init__.py
@@ -1,4 +1,4 @@
-from pylxd.models.cluster_member import ClusterMember  # NOQA
+from pylxd.models.cluster import (Cluster, 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/_model.py b/pylxd/models/_model.py
index 83fd0673..bf69a172 100644
--- a/pylxd/models/_model.py
+++ b/pylxd/models/_model.py
@@ -121,6 +121,8 @@ def __getattribute__(self, name):
         try:
             return super(Model, self).__getattribute__(name)
         except AttributeError:
+            print(self.__attributes__)
+            print(self.__slots__)
             if name in self.__attributes__:
                 self.sync()
                 return super(Model, self).__getattribute__(name)
@@ -153,6 +155,7 @@ def sync(self, rollback=False):
         # on existing attributes.
         response = self.api.get()
         payload = response.json()['metadata']
+        print(payload)
         for key, val in payload.items():
             if key not in self.__dirty__ or rollback:
                 try:
diff --git a/pylxd/models/cluster_member.py b/pylxd/models/cluster.py
similarity index 53%
rename from pylxd/models/cluster_member.py
rename to pylxd/models/cluster.py
index caa4abd2..dcdfa46a 100644
--- a/pylxd/models/cluster_member.py
+++ b/pylxd/models/cluster.py
@@ -12,12 +12,39 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 from pylxd.models import _model as model
+from pylxd import managers
 
 
+class Cluster(model.Model):
+    """An LXD Cluster.
+    """
+
+    server_name = model.Attribute()
+    enabled = model.Attribute()
+    member_config = model.Attribute()
+
+    members = model.Manager()
+
+    def __init__(self, *args, **kwargs):
+        super(Cluster, self).__init__(*args, **kwargs)
+        self.members = managers.ClusterMemberManager(self.client, self)
+
+    @property
+    def api(self):
+        return self.client.api.cluster
+
+    @classmethod
+    def get(cls, client, *args):
+        """Get cluster details"""
+        print(args)
+        response = client.api.cluster.get()
+        print(response.json())
+        container = cls(client, **response.json()['metadata'])
+        return container
+
 class ClusterMember(model.Model):
-    """A LXD certificate."""
+    """A LXD cluster member."""
 
-    name = model.Attribute(readonly=True)
     url = model.Attribute(readonly=True)
     database = model.Attribute(readonly=True)
     state = model.Attribute(readonly=True)
@@ -25,24 +52,26 @@ class ClusterMember(model.Model):
     status = model.Attribute(readonly=True)
     message = model.Attribute(readonly=True)
 
+    cluster = model.Parent()
+
     @classmethod
     def get(cls, client, name):
-        """Get a certificate by fingerprint."""
-        response = client.api.cluster_members[name].get()
+        """Get a cluster member by name."""
+        response = client.api.cluster.members[name].get()
 
         return cls(client, **response.json()['metadata'])
 
     @classmethod
-    def all(cls, client):
+    def all(cls, client, *args):
         """Get all certificates."""
-        response = client.api.cluster_members.get()
+        response = client.api.cluster.members.get()
 
         nodes = []
         for node in response.json()['metadata']:
             name = node.split('/')[-1]
-            nodes.append(cls(client, name=name))
+            nodes.append(cls(client, server_name=name))
         return nodes
 
     @property
     def api(self):
-        return self.client.api.cluster_members[self.name]
+        return self.client.api.cluster.members[self.server_name]

From f1f6e94d09c6aef544aa17f0176fbec5305bc734 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Tue, 11 Dec 2018 23:47:32 +0100
Subject: [PATCH 10/21] Adapted tests for ClusterMembers at client.members

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 integration/test_cluster_members.py       | 4 ++--
 pylxd/managers.py                         | 6 +-----
 pylxd/models/cluster.py                   | 2 ++
 pylxd/tests/models/test_cluster_member.py | 6 +++---
 4 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/integration/test_cluster_members.py b/integration/test_cluster_members.py
index a69cc360..cf958a17 100644
--- a/integration/test_cluster_members.py
+++ b/integration/test_cluster_members.py
@@ -30,12 +30,12 @@ class TestClusterMembers(ClusterMemberTestCase):
     def test_get(self):
         """A cluster member is fetched by its name."""
 
-        members = self.client.cluster_members.all()
+        members = self.client.cluster.members.all()
 
         random_member_name = "%s" % members[0].name
         random_member_url = "%s" % members[0].url
 
-        member = self.client.cluster_members.get(random_member_name)
+        member = self.client.cluster.members.get(random_member_name)
 
         new_url = "%s" % member.url
         self.assertEqual(random_member_url, new_url)
diff --git a/pylxd/managers.py b/pylxd/managers.py
index 958a2cfe..102f8921 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -4,9 +4,6 @@
 import importlib
 import inspect
 
-from pylxd import managers
-
-
 class BaseManager(object):
     """A BaseManager class for handling collection operations."""
 
@@ -72,8 +69,7 @@ class ClusterManager(BaseManager):
     def __init__(self, client, *args, **kwargs):
         super(ClusterManager, self).__init__(client, *args, **kwargs)
         self._client = client
-        self.members = managers.ClusterMemberManager(client)
-
+        self.members = ClusterMemberManager(client)
 
 
 @contextmanager
diff --git a/pylxd/models/cluster.py b/pylxd/models/cluster.py
index dcdfa46a..b854b217 100644
--- a/pylxd/models/cluster.py
+++ b/pylxd/models/cluster.py
@@ -42,9 +42,11 @@ def get(cls, client, *args):
         container = cls(client, **response.json()['metadata'])
         return container
 
+
 class ClusterMember(model.Model):
     """A LXD cluster member."""
 
+    name = model.Attribute(readonly=True)
     url = model.Attribute(readonly=True)
     database = model.Attribute(readonly=True)
     state = model.Attribute(readonly=True)
diff --git a/pylxd/tests/models/test_cluster_member.py b/pylxd/tests/models/test_cluster_member.py
index 3e9b6356..c593b3a5 100644
--- a/pylxd/tests/models/test_cluster_member.py
+++ b/pylxd/tests/models/test_cluster_member.py
@@ -20,12 +20,12 @@ class TestClusterMember(testing.PyLXDTestCase):
 
     def test_get(self):
         """A cluster member is retrieved."""
-        member = self.client.cluster_members.get('an-member')
+        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()
+        members = self.client.cluster.members.all()
 
-        self.assertIn('an-member', [m.name for m in members])
+        self.assertIn('an-member', [m.server_name for m in members])

From 5cd87998bafe74fe5be054dff98013528b89f20c Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Tue, 11 Dec 2018 23:51:15 +0100
Subject: [PATCH 11/21] pep8 compliance

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

diff --git a/pylxd/managers.py b/pylxd/managers.py
index 102f8921..1b6cd478 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -4,6 +4,7 @@
 import importlib
 import inspect
 
+
 class BaseManager(object):
     """A BaseManager class for handling collection operations."""
 

From 76cda97b187510755f8ebd22e1e6a69abc78d7f4 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 12 Dec 2018 14:22:03 +0100
Subject: [PATCH 12/21] removed attributes name and state, as they are not
 returned

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 integration/test_cluster_members.py |  2 +-
 pylxd/models/cluster.py             | 10 ++++------
 pylxd/tests/mock_lxd.py             |  7 ++++---
 3 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/integration/test_cluster_members.py b/integration/test_cluster_members.py
index cf958a17..7708a264 100644
--- a/integration/test_cluster_members.py
+++ b/integration/test_cluster_members.py
@@ -32,7 +32,7 @@ def test_get(self):
 
         members = self.client.cluster.members.all()
 
-        random_member_name = "%s" % members[0].name
+        random_member_name = "%s" % members[0].server_name
         random_member_url = "%s" % members[0].url
 
         member = self.client.cluster.members.get(random_member_name)
diff --git a/pylxd/models/cluster.py b/pylxd/models/cluster.py
index b854b217..85baa0b1 100644
--- a/pylxd/models/cluster.py
+++ b/pylxd/models/cluster.py
@@ -46,10 +46,8 @@ def get(cls, client, *args):
 class ClusterMember(model.Model):
     """A LXD cluster member."""
 
-    name = model.Attribute(readonly=True)
     url = model.Attribute(readonly=True)
     database = model.Attribute(readonly=True)
-    state = model.Attribute(readonly=True)
     server_name = model.Attribute(readonly=True)
     status = model.Attribute(readonly=True)
     message = model.Attribute(readonly=True)
@@ -57,9 +55,9 @@ class ClusterMember(model.Model):
     cluster = model.Parent()
 
     @classmethod
-    def get(cls, client, name):
+    def get(cls, client, server_name):
         """Get a cluster member by name."""
-        response = client.api.cluster.members[name].get()
+        response = client.api.cluster.members[server_name].get()
 
         return cls(client, **response.json()['metadata'])
 
@@ -70,8 +68,8 @@ def all(cls, client, *args):
 
         nodes = []
         for node in response.json()['metadata']:
-            name = node.split('/')[-1]
-            nodes.append(cls(client, server_name=name))
+            server_name = node.split('/')[-1]
+            nodes.append(cls(client, server_name=server_name))
         return nodes
 
     @property
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 90bce5ad..7efd2297 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -207,10 +207,11 @@ def snapshot_DELETE(request, context):
         'text': json.dumps({
             'type': 'sync',
             'metadata': {
-                "name": "an-member",
+                "server_name": "an-member",
                 "url": "https://10.1.1.101:8443",
-                "database": "true",
-                "state": "Online",
+                "database": 'false',
+                "status": "Online",
+                "message": "fully operational",
             }}),
         'method': 'GET',
         'url': r'^http://pylxd.test/1.0/cluster/members/an-member$',  # NOQA

From b9162a27e7dcdfc6c3d75b594ed1efbe13d37b2d Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 12 Dec 2018 15:03:44 +0100
Subject: [PATCH 13/21] Removed debug print outputs and Integration tests
 working

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/models/_model.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/pylxd/models/_model.py b/pylxd/models/_model.py
index bf69a172..83fd0673 100644
--- a/pylxd/models/_model.py
+++ b/pylxd/models/_model.py
@@ -121,8 +121,6 @@ def __getattribute__(self, name):
         try:
             return super(Model, self).__getattribute__(name)
         except AttributeError:
-            print(self.__attributes__)
-            print(self.__slots__)
             if name in self.__attributes__:
                 self.sync()
                 return super(Model, self).__getattribute__(name)
@@ -155,7 +153,6 @@ def sync(self, rollback=False):
         # on existing attributes.
         response = self.api.get()
         payload = response.json()['metadata']
-        print(payload)
         for key, val in payload.items():
             if key not in self.__dirty__ or rollback:
                 try:

From a22c40151c3f019b579fb37d6c4c503f86761e84 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 12 Dec 2018 15:38:55 +0100
Subject: [PATCH 14/21] Added tests for cluster endpoint

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/tests/mock_lxd.py            | 31 ++++++++++++++++++++++++++++++
 pylxd/tests/models/test_cluster.py | 25 ++++++++++++++++++++++++
 2 files changed, 56 insertions(+)
 create mode 100644 pylxd/tests/models/test_cluster.py

diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 7efd2297..f0470609 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -192,6 +192,37 @@ def snapshot_DELETE(request, context):
     },
 
 
+    # Cluster
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': {
+                "server_name": "an-member",
+                "enabled": 'true',
+                "member_config": [{
+                    "entity": "storage-pool",
+                    "name": "local",
+                    "key": "source",
+                    "value": "",
+                    "description":
+                        "\"source\" property for storage pool \"local\""
+                },
+                {
+                    "entity": "storage-pool",
+                    "name": "local",
+                    "key": "volatile.initial_source",
+                    "value": "",
+                    "description":
+                        "\"volatile.initial_source\" property for"
+                        " storage pool \"local\""
+                }]
+            }
+        }),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/cluster$',
+    },
+
+
     # Cluster Members
     {
         'text': json.dumps({
diff --git a/pylxd/tests/models/test_cluster.py b/pylxd/tests/models/test_cluster.py
new file mode 100644
index 00000000..1b4733c2
--- /dev/null
+++ b/pylxd/tests/models/test_cluster.py
@@ -0,0 +1,25 @@
+# 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.
+
+from pylxd.tests import testing
+
+
+class TestCluster(testing.PyLXDTestCase):
+    """Tests for pylxd.models.Cluster."""
+
+    def test_get(self):
+        """A cluster is retrieved."""
+        cluster = self.client.cluster.get()
+
+        self.assertEqual("an-member", cluster.server_name)

From 013cc0d56b71e6d05ec263ec911ab8aeed9bf796 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 12 Dec 2018 22:53:29 +0100
Subject: [PATCH 15/21] Added documentation for clustering and target in
 container creation

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 .gitignore                   |  4 ++
 doc/source/clustering.rst    | 85 ++++++++++++++++++++++++++++++++++++
 doc/source/index.rst         |  1 +
 doc/source/storage-pools.rst |  2 +-
 pylxd/models/container.py    | 15 ++++++-
 5 files changed, 105 insertions(+), 2 deletions(-)
 create mode 100644 doc/source/clustering.rst

diff --git a/.gitignore b/.gitignore
index 96f9cc7b..185c3b90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ pip-log.txt
 nosetests.xml
 .testrepository
 .venv
+htmlcov/
 
 # Translations
 *.mo
@@ -37,6 +38,9 @@ nosetests.xml
 .project
 .pydevproject
 
+# Pycharm
+.idea/
+
 # Complexity
 output/*.html
 output/*/index.html
diff --git a/doc/source/clustering.rst b/doc/source/clustering.rst
new file mode 100644
index 00000000..299e2b32
--- /dev/null
+++ b/doc/source/clustering.rst
@@ -0,0 +1,85 @@
+Clustering
+=============
+
+LXD supports clustering. There is only one cluster object.
+
+Cluster object
+--------------
+
+The :py:class:`~pylxd.models.cluster.Cluster` object represents the json
+object that is returned from `GET /1.0/cluster`.
+
+There is also a :py:class:`~pylxd.models.cluster.ClusterMember` and object that represents a
+cluster member at `GET
+/1.0/cluster/members`.  Note that it should be
+accessed from the cluster object.  For example:
+
+.. code:: python
+
+    client = pylxd.Client()
+    cluster = client.cluster.get()
+    member = cluster.members.get('node-5')
+
+
+.. note:: Please see the pylxd API documentation for more information on
+        storage pool methods and parameters.  The following is a summary.
+
+Cluster methods
+^^^^^^^^^^^^^^^
+
+A cluster can be queried through the following client manager methods:
+
+
+  - `get()` - Returns the cluster.
+
+
+Cluster Object attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For more information about the specifics of these attributes, please see
+the `LXD Cluster REST API`_ documentation.
+
+  - `server_name` - the name of the server in the cluster
+  - `enabled` - if the node is enabled
+  - `member_config` - configuration information for new cluster members.
+
+
+Cluster Members
+---------------
+
+Cluster Members are stored in a cluster.  On the `pylxd` API they are
+accessed from a cluster object:
+
+.. code:: Python
+
+    cluster = client.cluster.get()
+    members = cluster.members.all()
+    named_member = cluster.members.get('membername')
+
+
+Methods available on `<cluster_object>.members`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following methods are accessed from the `members` attribute on the cluster object.
+
+  - `all` - get all the members of the cluster.
+  - `get` - a get a single named member of the cluster.
+
+
+Cluster Member Object attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For more information about the specifics of these attributes, please see
+the `LXD Cluster REST API`_ documentation.
+
+  - `server_name` - the name of the server in the cluster
+  - `url` - the url the lxd endpoint
+  - `database` - if the distributed database is replicated on this node
+  - `status` - if the member is off or online
+  - `message` - a general message
+
+.. links
+
+.. _LXD Storage Pools: https://lxd.readthedocs.io/en/latest/storage/
+.. _LXD REST API: https://github.com/lxc/lxd/blob/master/doc/rest-api.md
+.. _LXD Cluster REST API: https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10cluster
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 7f5409c5..5e38792d 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -23,6 +23,7 @@ Contents:
    profiles
    operations
    storage-pools
+   clustering
 
    contributing
    api
diff --git a/doc/source/storage-pools.rst b/doc/source/storage-pools.rst
index f2929ef3..53a08392 100644
--- a/doc/source/storage-pools.rst
+++ b/doc/source/storage-pools.rst
@@ -98,7 +98,7 @@ Storage Volumes
 Storage Volumes are stored in storage pools.  On the `pylxd` API they are
 accessed from a storage pool object:
 
-... code:: Python
+.. code:: Python
 
     storage_pool = client.storage_pools.get('pool1')
     volumes = storage_pool.volumes.all()
diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index ed3f8d63..d725da15 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -258,7 +258,20 @@ def all(cls, client):
 
     @classmethod
     def create(cls, client, config, wait=False, target=None):
-        """Create a new container config."""
+        """Create a new container config.
+
+        :param client: client instance
+        :type client: Client
+        :param config: The configuration for the new container.
+        :type config: dict
+        :param wait: Whether to wait for async operations to complete.
+        :type wait: bool
+        :param target: If in cluster mode, the target member.
+        :type target: str
+        :raises LXDAPIException: if something goes wrong.
+        :returns: a container if successful
+        :rtype: :class:`Container`
+        """
         response = client.api.containers.post(json=config, target=target)
 
         if wait:

From 119ad719cea47fb9fdc9f92ba859d8a68c8b56bc Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Wed, 12 Dec 2018 22:57:05 +0100
Subject: [PATCH 16/21] Add autogenerated class documentation for cluster

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 doc/source/api.rst      | 9 +++++++++
 pylxd/models/cluster.py | 2 +-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/doc/source/api.rst b/doc/source/api.rst
index a7215e89..db0d9000 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -62,3 +62,12 @@ Storage Pool
 
 .. autoclass:: pylxd.models.StoragePool
    :members:
+
+Cluster
+------------
+
+.. autoclass:: pylxd.models.Cluster
+   :members:
+
+.. autoclass:: pylxd.models.ClusterMember
+   :members:
diff --git a/pylxd/models/cluster.py b/pylxd/models/cluster.py
index 85baa0b1..bb1c57ba 100644
--- a/pylxd/models/cluster.py
+++ b/pylxd/models/cluster.py
@@ -63,7 +63,7 @@ def get(cls, client, server_name):
 
     @classmethod
     def all(cls, client, *args):
-        """Get all certificates."""
+        """Get all cluster members."""
         response = client.api.cluster.members.get()
 
         nodes = []

From 46578239e9650d811c9e47a735a1a57fce25f091 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Thu, 13 Dec 2018 10:15:04 +0100
Subject: [PATCH 17/21] Fixed RST underlines

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 doc/source/api.rst        | 2 +-
 doc/source/clustering.rst | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/source/api.rst b/doc/source/api.rst
index db0d9000..a22cb53f 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -64,7 +64,7 @@ Storage Pool
    :members:
 
 Cluster
-------------
+-------
 
 .. autoclass:: pylxd.models.Cluster
    :members:
diff --git a/doc/source/clustering.rst b/doc/source/clustering.rst
index 299e2b32..ed241b5b 100644
--- a/doc/source/clustering.rst
+++ b/doc/source/clustering.rst
@@ -1,5 +1,5 @@
 Clustering
-=============
+==========
 
 LXD supports clustering. There is only one cluster object.
 

From ac298a90f6abf0db0e3ef981d4e107965250e4f3 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Thu, 13 Dec 2018 15:00:49 +0100
Subject: [PATCH 18/21] extracting operation id correctly for container.execute
 to work

The API returns a ?project=default query which has to be cut.
21ec910a-5b2f-467c-8412-f4f7223514f9?project=default

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/models/container.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index d725da15..2d1d52f2 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -397,7 +397,7 @@ def execute(
         })
 
         fds = response.json()['metadata']['metadata']['fds']
-        operation_id = response.json()['operation'].split('/')[-1]
+        operation_id = response.json()['operation'].split('/')[-1].split('?')[0]
         parsed = parse.urlparse(
             self.client.api.operations[operation_id].websocket._api_endpoint)
 

From 1713cc99ca4816f1cd182cc8a88973ac29038b93 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Thu, 13 Dec 2018 15:39:26 +0100
Subject: [PATCH 19/21] Added compatibility with operations project values

the LXD Rest api returns an operation id with the query string
?project=default to notify in which project the operation belongs. This
was not sufficiently cut away by .split('/')[-1]
but also needed a subsequent .split('?')[0]

The Mock_lxd has been adapted to test for this feature

In the container.execute the operation_id is needed directly, where it
is fixed too

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/models/container.py |  4 ++--
 pylxd/models/operation.py |  2 ++
 pylxd/tests/mock_lxd.py   | 38 +++++++++++++++++++++-----------------
 3 files changed, 25 insertions(+), 19 deletions(-)

diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index 2d1d52f2..3466cb2a 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -273,7 +273,6 @@ def create(cls, client, config, wait=False, target=None):
         :rtype: :class:`Container`
         """
         response = client.api.containers.post(json=config, target=target)
-
         if wait:
             client.operations.wait_for_operation(response.json()['operation'])
         return cls(client, name=config['name'])
@@ -397,7 +396,8 @@ def execute(
         })
 
         fds = response.json()['metadata']['metadata']['fds']
-        operation_id = response.json()['operation'].split('/')[-1].split('?')[0]
+        operation_id = response.json()['operation']\
+            .split('/')[-1].split('?')[0]
         parsed = parse.urlparse(
             self.client.api.operations[operation_id].websocket._api_endpoint)
 
diff --git a/pylxd/models/operation.py b/pylxd/models/operation.py
index 94e102d8..c8172f49 100644
--- a/pylxd/models/operation.py
+++ b/pylxd/models/operation.py
@@ -37,6 +37,8 @@ def get(cls, client, operation_id):
         """Get an operation."""
         if operation_id.startswith('/'):
             operation_id = operation_id.split('/')[-1]
+        if '?' in operation_id:
+            operation_id = operation_id.split('?')[0]
         response = client.api.operations[operation_id].get()
         return cls(_client=client, **response.json()['metadata'])
 
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index f0470609..aa921bdb 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -5,7 +5,7 @@ def containers_POST(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 def container_POST(request, context):
@@ -13,11 +13,11 @@ def container_POST(request, context):
     if not request.json().get('migration', False):
         return {
             'type': 'async',
-            'operation': 'operation-abc'}
+            'operation': '/1.0/operations/operation-abc?project=default'}
     else:
         return {
             'type': 'async',
-            'operation': 'operation-abc',
+            'operation': '/1.0/operations/operation-abc?project=default',
             'metadata': {
                 'metadata': {
                     '0': 'abc',
@@ -32,21 +32,22 @@ def container_DELETE(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 def images_POST(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
-        'operation': 'images-create-operation'})
+        'operation': '/1.0/operations/images-create-operation?project=default'
+    })
 
 
 def image_DELETE(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 def networks_GET(request, _):
@@ -80,7 +81,7 @@ def networks_DELETE(_, context):
     context.status_code = 202
     return json.dumps({
         'type': 'sync',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 def profile_GET(request, context):
@@ -108,14 +109,14 @@ def profile_DELETE(request, context):
     context.status_code = 200
     return json.dumps({
         'type': 'sync',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 def snapshot_DELETE(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
-        'operation': 'operation-abc'})
+        'operation': '/1.0/operations/operation-abc?project=default'})
 
 
 RULES = [
@@ -382,7 +383,7 @@ def snapshot_DELETE(request, context):
         'status_code': 202,
         'json': {
             'type': 'async',
-            'operation': 'operation-abc'},
+            'operation': '/1.0/operations/operation-abc?project=default'},
         'method': 'PUT',
         'url': r'^http://pylxd.test/1.0/containers/an-container/state$',  # NOQA
     },
@@ -394,7 +395,7 @@ def snapshot_DELETE(request, context):
     {
         'text': json.dumps({
             'type': 'async',
-            'operation': 'operation-abc'}),
+            'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'PUT',
         'url': r'^http://pylxd.test/1.0/containers/an-container$',
@@ -417,7 +418,7 @@ def snapshot_DELETE(request, context):
                     }
                 },
             },
-            'operation': 'operation-abc'},
+            'operation': '/1.0/operations/operation-abc?project=default'},
         'status_code': 202,
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers/an-container/exec$',  # NOQA
@@ -436,7 +437,7 @@ def snapshot_DELETE(request, context):
     {
         'text': json.dumps({
             'type': 'async',
-            'operation': 'operation-abc'}),
+            'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$',  # NOQA
@@ -454,7 +455,7 @@ def snapshot_DELETE(request, context):
     {
         'text': json.dumps({
             'type': 'async',
-            'operation': 'operation-abc'}),
+            'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$',  # NOQA
@@ -579,7 +580,9 @@ def snapshot_DELETE(request, context):
         'url': r'^http://pylxd2.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$',  # NOQA
     },
     {
-        'text': json.dumps({'type': 'async', 'operation': 'operation-abc'}),
+        'text': json.dumps({
+            'type': 'async',
+            'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'PUT',
         'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$',  # NOQA
@@ -808,7 +811,8 @@ def snapshot_DELETE(request, context):
     },
     # create an async storage volume
     {
-        'json': {'type': 'async', 'operation': 'operation-abc'},
+        'json': {'type': 'async',
+                 'operation': '/1.0/operations/operation-abc?project=default'},
         'status_code': 202,
         'method': 'POST',
         'url': (r'^http://pylxd.test/1.0/storage-pools/'
@@ -852,7 +856,7 @@ def snapshot_DELETE(request, context):
     {
         'json': {
             "type": "async",
-            "operation": "operation-abc",
+            "operation": "/1.0/operations/operation-abc?project=default",
             "metadata": {
                 "control": "secret1",
                 "fs": "secret2"

From 004e5ce1f41b7f880ea0f741d550818c99b3dc59 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Thu, 13 Dec 2018 15:45:39 +0100
Subject: [PATCH 20/21] readded blank line for clarity

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/models/container.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index 3466cb2a..b31ab0c6 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -273,6 +273,7 @@ def create(cls, client, config, wait=False, target=None):
         :rtype: :class:`Container`
         """
         response = client.api.containers.post(json=config, target=target)
+        
         if wait:
             client.operations.wait_for_operation(response.json()['operation'])
         return cls(client, name=config['name'])

From fd36f6907daca27cd8d7c3b904884715c98cb287 Mon Sep 17 00:00:00 2001
From: Felix Engelmann <fe-github at nlogn.org>
Date: Thu, 13 Dec 2018 15:46:41 +0100
Subject: [PATCH 21/21] removed whitespace

Signed-off-by: Felix Engelmann <fe-github at nlogn.org>
---
 pylxd/models/container.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index b31ab0c6..c182c218 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -273,7 +273,7 @@ def create(cls, client, config, wait=False, target=None):
         :rtype: :class:`Container`
         """
         response = client.api.containers.post(json=config, target=target)
-        
+
         if wait:
             client.operations.wait_for_operation(response.json()['operation'])
         return cls(client, name=config['name'])


More information about the lxc-devel mailing list