The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/129
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This is a first pass at unifying the error handling of pylxd. The LXD API response codification is pretty tight, and it's pretty easy to figure out when something isn't right. I think there are probably a few more patches that can happen to help organize this a bit, but this is a good start. Most of the patch is updating the tests to act more like the LXD API.
From 28daa60c1176519c819016f1d7af91e678984a6e Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@canonical.com> Date: Fri, 3 Jun 2016 13:32:05 -0600 Subject: [PATCH 1/3] Add response assertions for lxd This exposed a bug where deleting a running container doesn't report that the deletion didn't occur. --- integration/test_containers.py | 2 +- integration/test_images.py | 10 ++++++-- integration/testing.py | 29 +++++++++++++++++++---- pylxd/client.py | 54 +++++++++++++++++++++++++++++++++++++----- pylxd/container.py | 42 ++++++++++++++++---------------- pylxd/exceptions.py | 33 ++++++++++++++++++++++++-- pylxd/image.py | 25 +++++++++++-------- pylxd/profile.py | 19 +++++++++------ 8 files changed, 162 insertions(+), 52 deletions(-) diff --git a/integration/test_containers.py b/integration/test_containers.py index 609f19b..156f50e 100644 --- a/integration/test_containers.py +++ b/integration/test_containers.py @@ -132,7 +132,7 @@ def test_snapshot(self): def test_put_get_file(self): """A file is written to the container and then read.""" filepath = '/tmp/an_file' - data = 'abcdef' + data = b'abcdef' retval = self.container.put_file(filepath, data) diff --git a/integration/test_images.py b/integration/test_images.py index a16932e..4e2d94f 100644 --- a/integration/test_images.py +++ b/integration/test_images.py @@ -11,6 +11,8 @@ # 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 time + from pylxd import exceptions from integration.testing import create_busybox_image, IntegrationTestCase @@ -33,6 +35,9 @@ def test_all(self): fingerprint, _ = self.create_image() self.addCleanup(self.delete_image, fingerprint) + # XXX: rockstar (02 Jun 2016) - This seems to have a failure + # of some sort. This is a hack. + time.sleep(5) images = self.client.images.all() self.assertIn(fingerprint, [image.fingerprint for image in images]) @@ -42,8 +47,9 @@ def test_create(self): path, fingerprint = create_busybox_image() self.addCleanup(self.delete_image, fingerprint) - with open(path) as f: - image = self.client.images.create(f.read(), wait=True) + with open(path, 'rb') as f: + data = f.read() + image = self.client.images.create(data, wait=True) self.assertEqual(fingerprint, image.fingerprint) diff --git a/integration/testing.py b/integration/testing.py index c8ee7be..a6ea93b 100644 --- a/integration/testing.py +++ b/integration/testing.py @@ -14,6 +14,7 @@ import unittest import uuid +from pylxd import exceptions from pylxd.client import Client from integration.busybox import create_busybox_image @@ -59,9 +60,19 @@ def delete_container(self, name, enforce=False): # enforce is a hack. There's a race somewhere in the delete. # To ensure we don't get an infinite loop, let's count. count = 0 - result = self.lxd['containers'][name].delete() - while enforce and result.status_code == 404 and count < 10: + try: result = self.lxd['containers'][name].delete() + except exceptions.LXDAPIException as e: + if e.response.status_code in (400, 404): + return + raise + while enforce and result.status_code == 404 and count < 10: + try: + result = self.lxd['containers'][name].delete() + except exceptions.LXDAPIException as e: + if e.response.status_code in (400, 404): + return + raise count += 1 try: operation_uuid = result.json()['operation'].split('/')[-1] @@ -92,7 +103,12 @@ def create_image(self): def delete_image(self, fingerprint): """Delete an image in lxd.""" - self.lxd.images[fingerprint].delete() + try: + self.lxd.images[fingerprint].delete() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + return + raise def create_profile(self): """Create a profile.""" @@ -106,7 +122,12 @@ def create_profile(self): def delete_profile(self, name): """Delete a profile.""" - self.lxd.profiles[name].delete() + try: + self.lxd.profiles[name].delete() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + return + raise def assertCommon(self, response): """Assert common LXD responses. diff --git a/pylxd/client.py b/pylxd/client.py index cfec79a..b8ea444 100644 --- a/pylxd/client.py +++ b/pylxd/client.py @@ -27,8 +27,7 @@ class _APINode(object): - """An api node object. - """ + """An api node object.""" def __init__(self, api_endpoint, cert=None, verify=True): self._api_endpoint = api_endpoint @@ -50,21 +49,64 @@ def __getitem__(self, item): '{}/{}'.format(self._api_endpoint, item), cert=self.session.cert, verify=self.session.verify) + def _assert_response(self, response, allowed_status_codes=(200,)): + """Assert properties of the response. + + LXD's API clearly defines specific responses. If the API + response is something unexpected (i.e. an error), then + we need to raise an exception and have the call points + handle the errors or just let the issue be raised to the + user. + """ + if response.status_code not in allowed_status_codes: + raise exceptions.LXDAPIException(response) + + try: + data = response.json() + except ValueError: + # Not a JSON response + return + + if response.status_code == 200: + # Synchronous request + try: + if data['type'] != 'sync': + raise exceptions.LXDAPIException(response) + except KeyError: + # Missing 'type' in response + raise exceptions.LXDAPIException(response) + + if response.status_code == 202: + try: + if data['type'] != 'async': + raise exceptions.LXDAPIException(response) + except KeyError: + # Missing 'type' in response + raise exceptions.LXDAPIException(response) + def get(self, *args, **kwargs): """Perform an HTTP GET.""" - return self.session.get(self._api_endpoint, *args, **kwargs) + response = self.session.get(self._api_endpoint, *args, **kwargs) + self._assert_response(response) + return response def post(self, *args, **kwargs): """Perform an HTTP POST.""" - return self.session.post(self._api_endpoint, *args, **kwargs) + response = self.session.post(self._api_endpoint, *args, **kwargs) + self._assert_response(response, allowed_status_codes=(200, 202)) + return response def put(self, *args, **kwargs): """Perform an HTTP PUT.""" - return self.session.put(self._api_endpoint, *args, **kwargs) + response = self.session.put(self._api_endpoint, *args, **kwargs) + self._assert_response(response, allowed_status_codes=(200, 202)) + return response def delete(self, *args, **kwargs): """Perform an HTTP delete.""" - return self.session.delete(self._api_endpoint, *args, **kwargs) + response = self.session.delete(self._api_endpoint, *args, **kwargs) + self._assert_response(response, allowed_status_codes=(200, 202)) + return response class Client(object): diff --git a/pylxd/container.py b/pylxd/container.py index 4620f58..21a5c69 100644 --- a/pylxd/container.py +++ b/pylxd/container.py @@ -44,16 +44,15 @@ def put(self, filepath, data): return response.status_code == 200 def get(self, filepath): - response = self._client.api.containers[ - self._container.name].files.get( - params={'path': filepath}) - if response.status_code == 500: - # XXX: rockstar (15 Feb 2016) - This should really - # return a 404. I blame LXD. - raise exceptions.NotFound({ - 'error': '{} not found in container {}'.format( - filepath, self._container.name - )}) + try: + response = self._client.api.containers[ + self._container.name].files.get( + params={'path': filepath}) + except exceptions.LXDAPIException as e: + # LXD 2.0.3+ return 404, not 500, + if e.response.status_code in (500, 404): + raise exceptions.NotFound() + raise return response.content __slots__ = [ @@ -65,10 +64,13 @@ def get(self, filepath): @classmethod def get(cls, client, name): """Get a container by name.""" - response = client.api.containers[name].get() + try: + response = client.api.containers[name].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise - if response.status_code == 404: - raise exceptions.NotFound(response.json()) container = cls(_client=client, **response.json()['metadata']) return container @@ -95,7 +97,7 @@ def create(cls, client, config, wait=False): response = client.api.containers.post(json=config) if response.status_code != 202: - raise exceptions.CreateFailed(response.json()) + raise exceptions.CreateFailed(response) if wait: Operation.wait_for_operation(client, response.json()['operation']) return cls(name=config['name'], _client=client) @@ -151,8 +153,6 @@ def delete(self, wait=False): """Delete the container.""" response = self._client.api.containers[self.name].delete() - if response.status_code != 202: - raise RuntimeError('Error deleting instance {}'.format(self.name)) if wait: self.wait_for_operation(response.json()['operation']) @@ -260,10 +260,12 @@ class Snapshot(mixin.Waitable, mixin.Marshallable): @classmethod def get(cls, client, container, name): - response = client.api.containers[container.name].snapshots[name].get() - - if response.status_code == 404: - raise exceptions.NotFound(response.json()) + try: + response = client.api.containers[container.name].snapshots[name].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise snapshot = cls( _client=client, _container=container, diff --git a/pylxd/exceptions.py b/pylxd/exceptions.py index 9e243dd..8fe70a1 100644 --- a/pylxd/exceptions.py +++ b/pylxd/exceptions.py @@ -9,12 +9,38 @@ def __str__(self): return "LXD client certificates are not trusted.""" +class LXDAPIException(Exception): + """A generic exception for representing unexpected LXD API responses. + + LXD API responses are clearly documented, and are either a standard + return value, and background operation, or an error. This exception + is raised on an error case, or when the response status code is + not expected for the response. + """ + + def __init__(self, response): + super(LXDAPIException, self).__init__() + self.response = response + + def __str__(self): + try: + data = self.response.json() + return data['error'] + except (ValueError, KeyError): + pass + return self.response.content.decode('utf-8') + + class _LXDAPIException(Exception): """A LXD API Exception. An exception representing an issue in the LXD API. It contains the error information returned from LXD. + This exception type should be phased out, with the exception being + raised at a lower level (i.e. where we actually talk to the LXD + API, not in our pylxd logic). + DO NOT CATCH THIS EXCEPTION DIRECTLY. """ @@ -25,8 +51,11 @@ def __str__(self): return self.data.get('error') -class NotFound(_LXDAPIException): - """Generic get failure exception.""" +class NotFound(Exception): + """Generic NotFound exception.""" + + def __str__(self): + return 'Object not found' class CreateFailed(_LXDAPIException): diff --git a/pylxd/image.py b/pylxd/image.py index 535c083..104d91f 100644 --- a/pylxd/image.py +++ b/pylxd/image.py @@ -30,11 +30,14 @@ class Image(mixin.Waitable, mixin.Marshallable): @classmethod def get(cls, client, fingerprint): """Get an image.""" - response = client.api.images[fingerprint].get() + try: + response = client.api.images[fingerprint].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise - if response.status_code == 404: - raise exceptions.NotFound(response.json()) - image = Image(_client=client, **response.json()['metadata']) + image = cls(_client=client, **response.json()['metadata']) return image @classmethod @@ -45,7 +48,7 @@ def all(cls, client): images = [] for url in response.json()['metadata']: fingerprint = url.split('/')[-1] - images.append(Image(_client=client, fingerprint=fingerprint)) + images.append(cls(_client=client, fingerprint=fingerprint)) return images @classmethod @@ -64,7 +67,7 @@ def create(cls, client, image_data, public=False, wait=False): if wait: Operation.wait_for_operation(client, response.json()['operation']) - return cls.get(client, fingerprint) + return cls(_client=client, fingerprint=fingerprint) def __init__(self, **kwargs): super(Image, self).__init__() @@ -89,10 +92,12 @@ def delete(self, wait=False): def fetch(self): """Fetch the object from LXD, populating attributes.""" - response = self._client.api.images[self.fingerprint].get() - - if response.status_code == 404: - raise exceptions.NotFound(response.json()) + try: + response = self._client.api.images[self.fingerprint].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise for key, val in six.iteritems(response.json()['metadata']): setattr(self, key, val) diff --git a/pylxd/profile.py b/pylxd/profile.py index 749aa0d..7cf1024 100644 --- a/pylxd/profile.py +++ b/pylxd/profile.py @@ -26,10 +26,13 @@ class Profile(mixin.Marshallable): @classmethod def get(cls, client, name): """Get a profile.""" - response = client.api.profiles[name].get() + try: + response = client.api.profiles[name].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise - if response.status_code == 404: - raise exceptions.NotFound(response.json()) return cls(_client=client, **response.json()['metadata']) @classmethod @@ -85,10 +88,12 @@ def delete(self): def fetch(self): """Fetch the object from LXD, populating attributes.""" - response = self._client.api.profiles[self.name].get() - - if response.status_code == 404: - raise exceptions.NotFound(response.json()) + try: + response = self._client.api.profiles[self.name].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise for key, val in six.iteritems(response.json()['metadata']): setattr(self, key, val) From 0b4a0a323701cafd690e20a50d26e64e0e91515e Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@canonical.com> Date: Fri, 3 Jun 2016 14:27:45 -0600 Subject: [PATCH 2/3] Fix the unit tests --- pylxd/container.py | 17 ++++--- pylxd/image.py | 10 ++-- pylxd/profile.py | 8 +-- pylxd/tests/mock_lxd.py | 115 ++++++++++++++++++++++++++++-------------- pylxd/tests/test_client.py | 24 +++++++-- pylxd/tests/test_container.py | 8 +-- 6 files changed, 121 insertions(+), 61 deletions(-) diff --git a/pylxd/container.py b/pylxd/container.py index 21a5c69..39524f7 100644 --- a/pylxd/container.py +++ b/pylxd/container.py @@ -94,10 +94,11 @@ def all(cls, client): @classmethod def create(cls, client, config, wait=False): """Create a new container config.""" - response = client.api.containers.post(json=config) + try: + response = client.api.containers.post(json=config) + except exceptions.LXDAPIException as e: + raise exceptions.CreateFailed(e.response) - if response.status_code != 202: - raise exceptions.CreateFailed(response) if wait: Operation.wait_for_operation(client, response.json()['operation']) return cls(name=config['name'], _client=client) @@ -112,10 +113,12 @@ def __init__(self, **kwargs): def fetch(self): """Reload the container information.""" - response = self._client.api.containers[self.name].get() - if response.status_code == 404: - raise NameError( - 'Container named "{}" has gone away'.format(self.name)) + try: + response = self._client.api.containers[self.name].get() + except exceptions.LXDAPIException as e: + if e.response.status_code == 404: + raise exceptions.NotFound() + raise for key, value in six.iteritems(response.json()['metadata']): setattr(self, key, value) # XXX: rockstar (28 Mar 2016) - This method was named improperly diff --git a/pylxd/image.py b/pylxd/image.py index 104d91f..82b965d 100644 --- a/pylxd/image.py +++ b/pylxd/image.py @@ -59,11 +59,11 @@ def create(cls, client, image_data, public=False, wait=False): headers = {} if public: headers['X-LXD-Public'] = '1' - response = client.api.images.post( - data=image_data, headers=headers) - - if response.status_code != 202: - raise exceptions.CreateFailed(response.json()) + try: + response = client.api.images.post( + data=image_data, headers=headers) + except exceptions.LXDAPIException as e: + raise exceptions.CreateFailed(e.response.json()) if wait: Operation.wait_for_operation(client, response.json()['operation']) diff --git a/pylxd/profile.py b/pylxd/profile.py index 7cf1024..1bcc0e0 100644 --- a/pylxd/profile.py +++ b/pylxd/profile.py @@ -54,10 +54,10 @@ def create(cls, client, name, config=None, devices=None): profile['config'] = config if devices is not None: profile['devices'] = devices - response = client.api.profiles.post(json=profile) - - if response.status_code is not 200: - raise exceptions.CreateFailed(response.json()) + try: + client.api.profiles.post(json=profile) + except exceptions.LXDAPIException as e: + raise exceptions.CreateFailed(e.response.json()) return cls.get(client, name) diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py index 07c0f39..6df21c9 100644 --- a/pylxd/tests/mock_lxd.py +++ b/pylxd/tests/mock_lxd.py @@ -3,32 +3,43 @@ def containers_POST(request, context): context.status_code = 202 - return json.dumps({'operation': 'operation-abc'}) + return json.dumps({ + 'type': 'async', + 'operation': 'operation-abc'}) def container_DELETE(request, context): context.status_code = 202 - return json.dumps({'operation': 'operation-abc'}) + return json.dumps({ + 'type': 'async', + 'operation': 'operation-abc'}) def images_POST(request, context): context.status_code = 202 - return json.dumps({'metadata': {}}) + return json.dumps({ + 'type': 'async', + 'metadata': {}}) def profiles_POST(request, context): context.status_code = 200 - return json.dumps({'metadata': {}}) + return json.dumps({ + 'type': 'sync', + 'metadata': {}}) def snapshot_DELETE(request, context): context.status_code = 202 - return json.dumps({'operation': 'operation-abc'}) + return json.dumps({ + 'type': 'async', + 'operation': 'operation-abc'}) def profile_GET(request, context): name = request.path.split('/')[-1] return json.dumps({ + 'type': 'sync', 'metadata': { 'name': name, 'description': 'An description', @@ -41,8 +52,10 @@ def profile_GET(request, context): RULES = [ # General service endpoints { - 'text': json.dumps({'metadata': {'auth': 'trusted', - 'environment': {}}}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': {'auth': 'trusted', + 'environment': {}}}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0$', }, @@ -50,9 +63,11 @@ def profile_GET(request, context): # Containers { - 'text': json.dumps({'metadata': [ - 'http://pylxd.test/1.0/containers/an-container', - ]}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': [ + 'http://pylxd.test/1.0/containers/an-container', + ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers$', }, @@ -62,28 +77,36 @@ def profile_GET(request, context): 'url': r'^http://pylxd.test/1.0/containers$', }, { - 'text': json.dumps({'metadata': { - 'name': 'an-container', - 'ephemeral': True, - }}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': { + 'name': 'an-container', + 'ephemeral': True, + }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { - 'text': json.dumps({'metadata': { - 'status': 'Running', - 'status_code': 103, - }}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': { + 'status': 'Running', + 'status_code': 103, + }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/state$', # NOQA }, { - 'text': json.dumps({'operation': 'operation-abc'}), + 'text': json.dumps({ + 'type': 'sync', # This should be async + 'operation': 'operation-abc'}), 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { - 'text': json.dumps({'operation': 'operation-abc'}), + 'text': json.dumps({ + 'type': 'sync', # This should be async + 'operation': 'operation-abc'}), 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, @@ -96,27 +119,35 @@ def profile_GET(request, context): # Container Snapshots { - 'text': json.dumps({'metadata': [ - '/1.0/containers/an_container/snapshots/an-snapshot', - ]}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': [ + '/1.0/containers/an_container/snapshots/an-snapshot', + ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { - 'text': json.dumps({'operation': 'operation-abc'}), + 'text': json.dumps({ + 'type': 'sync', # This should be async + 'operation': 'operation-abc'}), 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { - 'text': json.dumps({'metadata': { - 'name': 'an_container/an-snapshot', - 'stateful': False, - }}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': { + 'name': 'an_container/an-snapshot', + 'stateful': False, + }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { - 'text': json.dumps({'operation': 'operation-abc'}), + 'text': json.dumps({ + 'type': 'sync', # This should be async + 'operation': 'operation-abc'}), 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, @@ -142,9 +173,11 @@ def profile_GET(request, context): # Images { - 'text': json.dumps({'metadata': [ - 'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA - ]}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': [ + 'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA + ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images$', }, @@ -155,6 +188,7 @@ def profile_GET(request, context): }, { 'text': json.dumps({ + 'type': 'sync', 'metadata': { 'aliases': [ { @@ -182,9 +216,11 @@ def profile_GET(request, context): # Profiles { - 'text': json.dumps({'metadata': [ - 'http://pylxd.test/1.0/profiles/an-profile', - ]}), + 'text': json.dumps({ + 'type': 'sync', + 'metadata': [ + 'http://pylxd.test/1.0/profiles/an-profile', + ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles$', }, @@ -201,12 +237,17 @@ def profile_GET(request, context): # Operations { - 'text': '{"metadata": {"id": "operation-abc"}}', + 'text': json.dumps({ + 'type': 'sync', + 'metadata': {'id': 'operation-abc'}, + }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc$', }, { - 'text': '{"metadata": {}', + 'text': json.dumps({ + 'type': 'sync', + }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc/wait$', }, diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py index 3f69d31..7871531 100644 --- a/pylxd/tests/test_client.py +++ b/pylxd/tests/test_client.py @@ -117,7 +117,11 @@ def test_session_unix_socket(self): @mock.patch('pylxd.client.requests.Session') def test_get(self, Session): """Perform a session get.""" - session = mock.Mock() + response = mock.Mock(**{ + 'status_code': 200, + 'json.return_value': {'type': 'sync'}, + }) + session = mock.Mock(**{'get.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') @@ -129,7 +133,11 @@ def test_get(self, Session): @mock.patch('pylxd.client.requests.Session') def test_post(self, Session): """Perform a session post.""" - session = mock.Mock() + response = mock.Mock(**{ + 'status_code': 200, + 'json.return_value': {'type': 'sync'}, + }) + session = mock.Mock(**{'post.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') @@ -141,7 +149,11 @@ def test_post(self, Session): @mock.patch('pylxd.client.requests.Session') def test_put(self, Session): """Perform a session put.""" - session = mock.Mock() + response = mock.Mock(**{ + 'status_code': 200, + 'json.return_value': {'type': 'sync'}, + }) + session = mock.Mock(**{'put.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') @@ -153,7 +165,11 @@ def test_put(self, Session): @mock.patch('pylxd.client.requests.Session') def test_delete(self, Session): """Perform a session delete.""" - session = mock.Mock() + response = mock.Mock(**{ + 'status_code': 200, + 'json.return_value': {'type': 'sync'}, + }) + session = mock.Mock(**{'delete.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py index 932a38e..8a4d6b3 100644 --- a/pylxd/tests/test_container.py +++ b/pylxd/tests/test_container.py @@ -79,7 +79,7 @@ def test_fetch(self): self.assertTrue(an_container.ephemeral) def test_fetch_not_found(self): - """NameError is raised on a 404 for updating container.""" + """NotFound is raised on a 404 for updating container.""" def not_found(request, context): context.status_code = 404 return json.dumps({ @@ -95,7 +95,7 @@ def not_found(request, context): an_container = container.Container( name='an-missing-container', _client=self.client) - self.assertRaises(NameError, an_container.fetch) + self.assertRaises(exceptions.NotFound, an_container.fetch) def test_update(self): """A container is updated.""" @@ -214,7 +214,7 @@ def test_delete(self): # TODO: add an assertion here def test_delete_failure(self): - """If the response indicates delete failure, raise RuntimeError.""" + """If the response indicates delete failure, raise an exception.""" def not_found(request, context): context.status_code = 404 return json.dumps({ @@ -231,7 +231,7 @@ def not_found(request, context): _client=self.client, _container=self.container, name='an-snapshot') - self.assertRaises(RuntimeError, snapshot.delete) + self.assertRaises(exceptions.LXDAPIException, snapshot.delete) class TestFiles(testing.PyLXDTestCase): From 6ead81fe0246b2e6b12e2649c2df2781bf921f37 Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@canonical.com> Date: Fri, 3 Jun 2016 14:30:38 -0600 Subject: [PATCH 3/3] Fix lint --- pylxd/container.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylxd/container.py b/pylxd/container.py index 39524f7..6df712c 100644 --- a/pylxd/container.py +++ b/pylxd/container.py @@ -264,7 +264,8 @@ class Snapshot(mixin.Waitable, mixin.Marshallable): @classmethod def get(cls, client, container, name): try: - response = client.api.containers[container.name].snapshots[name].get() + response = client.api.containers[ + container.name].snapshots[name].get() except exceptions.LXDAPIException as e: if e.response.status_code == 404: raise exceptions.NotFound()
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel