[lxc-devel] [pylxd/master] Better http assertions

rockstar on Github lxc-bot at linuxcontainers.org
Fri Jun 3 20:36:52 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 667 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160603/44199bef/attachment.bin>
-------------- next part --------------
From 28daa60c1176519c819016f1d7af91e678984a6e Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at 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.hummer at 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.hummer at 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()


More information about the lxc-devel mailing list