From 676d0114f6622fe345a861f0c5cf3790e13f0f4b Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Mon, 24 Sep 2018 15:44:06 +0200 Subject: [PATCH 01/14] cluster endpoint read support Signed-off-by: Felix Engelmann --- 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 Date: Mon, 24 Sep 2018 15:52:01 +0200 Subject: [PATCH 02/14] renamed node to more close cluster_member Signed-off-by: Felix Engelmann --- 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 Date: Wed, 26 Sep 2018 00:10:01 +0200 Subject: [PATCH 03/14] Added functionality to specify target cluster member in containers.create Signed-off-by: Felix Engelmann --- 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 Date: Mon, 8 Oct 2018 22:56:22 +0200 Subject: [PATCH 04/14] Added tests for Cluster Members and Targeted create container Signed-off-by: Felix Engelmann --- 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 Date: Mon, 8 Oct 2018 22:58:39 +0200 Subject: [PATCH 05/14] Added to contributors Signed-off-by: Felix Engelmann --- 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 Date: Mon, 8 Oct 2018 23:15:08 +0200 Subject: [PATCH 06/14] pep8 compliance Signed-off-by: Felix Engelmann --- 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 Date: Tue, 9 Oct 2018 01:16:15 +0200 Subject: [PATCH 07/14] Added integration test for cluster member info and all attributes Signed-off-by: Felix Engelmann --- 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 Date: Tue, 11 Dec 2018 02:30:30 +0100 Subject: [PATCH 08/14] more efficient pop and params post parameter Signed-off-by: Felix Engelmann --- 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 Date: Tue, 11 Dec 2018 23:25:53 +0100 Subject: [PATCH 09/14] 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 --- 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 Date: Tue, 11 Dec 2018 23:47:32 +0100 Subject: [PATCH 10/14] Adapted tests for ClusterMembers at client.members Signed-off-by: Felix Engelmann --- 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 Date: Tue, 11 Dec 2018 23:51:15 +0100 Subject: [PATCH 11/14] pep8 compliance Signed-off-by: Felix Engelmann --- 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 Date: Wed, 12 Dec 2018 14:22:03 +0100 Subject: [PATCH 12/14] removed attributes name and state, as they are not returned Signed-off-by: Felix Engelmann --- 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 Date: Wed, 12 Dec 2018 15:03:44 +0100 Subject: [PATCH 13/14] Removed debug print outputs and Integration tests working Signed-off-by: Felix Engelmann --- 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 Date: Wed, 12 Dec 2018 15:38:55 +0100 Subject: [PATCH 14/14] Added tests for cluster endpoint Signed-off-by: Felix Engelmann --- 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)