[lxc-devel] [pylxd/master] Image export
rockstar on Github
lxc-bot at linuxcontainers.org
Fri Jun 3 23:53:42 UTC 2016
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 467 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160603/01bb5713/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 01/10] 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 02/10] 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 03/10] 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()
From 4cf4ff87f05d0c45332b1fd465bf02a9f13f4db9 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 14:46:27 -0600
Subject: [PATCH 04/10] Update client tests for more coverage
---
pylxd/tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index 7871531..aaa70d1 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -147,6 +147,54 @@ def test_post(self, Session):
session.post.assert_called_once_with('http://test.com')
@mock.patch('pylxd.client.requests.Session')
+ def test_post_200_not_sync(self, Session):
+ """A status code of 200 with async request raises an exception."""
+ response = mock.Mock(**{
+ 'status_code': 200,
+ 'json.return_value': {'type': 'async'},
+ })
+ session = mock.Mock(**{'post.return_value': response})
+ Session.return_value = session
+
+ node = client._APINode('http://test.com')
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ node.post)
+
+ @mock.patch('pylxd.client.requests.Session')
+ def test_post_202_sync(self, Session):
+ """A status code of 202 with sync request raises an exception."""
+ response = mock.Mock(**{
+ 'status_code': 202,
+ 'json.return_value': {'type': 'sync'},
+ })
+ session = mock.Mock(**{'post.return_value': response})
+ Session.return_value = session
+
+ node = client._APINode('http://test.com')
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ node.post)
+
+ @mock.patch('pylxd.client.requests.Session')
+ def test_post_missing_type(self, Session):
+ """A missing response type raises an exception."""
+ response = mock.Mock(**{
+ 'status_code': 200,
+ 'json.return_value': {},
+ })
+ session = mock.Mock(**{'post.return_value': response})
+ Session.return_value = session
+
+ node = client._APINode('http://test.com')
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ node.post)
+
+ @mock.patch('pylxd.client.requests.Session')
def test_put(self, Session):
"""Perform a session put."""
response = mock.Mock(**{
From 3dacb2bee30053722c4cfd5958678bb896a51176 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 15:12:57 -0600
Subject: [PATCH 05/10] Add test coverage (to satisfy codecov)
---
pylxd/tests/mock_lxd.py | 31 ++++++++++++++++++++++++
pylxd/tests/test_image.py | 30 ++++++++++++++++++++++-
pylxd/tests/test_profile.py | 58 ++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 117 insertions(+), 2 deletions(-)
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 6df21c9..0bd12aa 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -22,6 +22,13 @@ def images_POST(request, context):
'metadata': {}})
+def image_DELETE(request, context):
+ context.status_code = 202
+ return json.dumps({
+ 'type': 'async',
+ 'operation': 'operation-abc'})
+
+
def profiles_POST(request, context):
context.status_code = 200
return json.dumps({
@@ -29,6 +36,13 @@ def profiles_POST(request, context):
'metadata': {}})
+def profile_DELETE(request, context):
+ context.status_code = 200
+ return json.dumps({
+ 'type': 'sync',
+ 'operation': 'operation-abc'})
+
+
def snapshot_DELETE(request, context):
context.status_code = 202
return json.dumps({
@@ -213,6 +227,12 @@ def profile_GET(request, context):
'method': 'GET',
'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
},
+ {
+ 'text': image_DELETE,
+ 'method': 'DELETE',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
+ },
+
# Profiles
{
@@ -234,6 +254,17 @@ def profile_GET(request, context):
'method': 'GET',
'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$',
},
+ {
+ 'text': json.dumps({'type': 'sync'}),
+ 'method': 'PUT',
+ 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$',
+ },
+ {
+ 'text': profile_DELETE,
+ 'method': 'DELETE',
+ 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$',
+ },
+
# Operations
{
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index 37f9f30..b97485f 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -44,7 +44,7 @@ def test_all(self):
def test_create(self):
"""An image is created."""
fingerprint = hashlib.sha256(b'').hexdigest()
- a_image = image.Image.create(self.client, b'')
+ a_image = image.Image.create(self.client, b'', public=True)
self.assertIsInstance(a_image, image.Image)
self.assertEqual(fingerprint, a_image.fingerprint)
@@ -101,3 +101,31 @@ def not_found(request, context):
a_image = image.Image(fingerprint=fingerprint, _client=self.client)
self.assertRaises(exceptions.NotFound, a_image.fetch)
+
+ def test_fetch_error(self):
+ """A 500 error raises LXDAPIException."""
+ def not_found(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': not_found,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
+ })
+ fingerprint = hashlib.sha256(b'').hexdigest()
+
+ a_image = image.Image(fingerprint=fingerprint, _client=self.client)
+
+ self.assertRaises(exceptions.LXDAPIException, a_image.fetch)
+
+ def test_delete(self):
+ """An image is deleted."""
+ # XXX: rockstar (03 Jun 2016) - This just executes
+ # a code path. There should be an assertion here, but
+ # it's not clear how to assert that, just yet.
+ a_image = self.client.images.all()[0]
+
+ a_image.delete(wait=True)
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
index 9abc487..d964ec7 100644
--- a/pylxd/tests/test_profile.py
+++ b/pylxd/tests/test_profile.py
@@ -32,6 +32,24 @@ def not_found(request, context):
exceptions.NotFound,
profile.Profile.get, self.client, 'an-profile')
+ def test_get_error(self):
+ """LXDAPIException is raised on get error."""
+ def error(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': error,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/profiles/an-profile$',
+ })
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ profile.Profile.get, self.client, 'an-profile')
+
def test_all(self):
"""A list of all profiles is returned."""
profiles = profile.Profile.all(self.client)
@@ -72,7 +90,18 @@ def error(request, context):
profile.Profile.create, self.client,
name='an-new-profile', config={})
- def test_partial_objects(self):
+ def test_update(self):
+ """A profile is updated."""
+ # XXX: rockstar (03 Jun 2016) - This just executes
+ # a code path. There should be an assertion here, but
+ # it's not clear how to assert that, just yet.
+ an_profile = profile.Profile.get(self.client, 'an-profile')
+
+ an_profile.update()
+
+ self.assertEqual({}, an_profile.config)
+
+ def test_update_partial_objects(self):
"""A partially fetched profile can't be pushed."""
an_profile = self.client.profiles.all()[0]
@@ -105,3 +134,30 @@ def not_found(request, context):
an_profile = profile.Profile(name='an-profile', _client=self.client)
self.assertRaises(exceptions.NotFound, an_profile.fetch)
+
+ def test_fetch_error(self):
+ """LXDAPIException is raised on fetch error."""
+ def error(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': error,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/profiles/an-profile$',
+ })
+
+ an_profile = profile.Profile(name='an-profile', _client=self.client)
+
+ self.assertRaises(exceptions.LXDAPIException, an_profile.fetch)
+
+ def test_delete(self):
+ """A profile is deleted."""
+ # XXX: rockstar (03 Jun 2016) - This just executes
+ # a code path. There should be an assertion here, but
+ # it's not clear how to assert that, just yet.
+ an_profile = self.client.profiles.all()[0]
+
+ an_profile.delete()
From 116dd0a2c4f94b63678bcde3903bf9b598c9f2c1 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 15:30:23 -0600
Subject: [PATCH 06/10] More test coverage, specific to the patch
---
pylxd/container.py | 2 --
pylxd/tests/test_container.py | 55 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/pylxd/container.py b/pylxd/container.py
index 6df712c..3408f52 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -318,7 +318,5 @@ def delete(self, wait=False):
response = self._client.api.containers[
self._container.name].snapshots[self.name].delete()
- if response.status_code != 202:
- raise RuntimeError('Error deleting snapshot {}'.format(self.name))
if wait:
self.wait_for_operation(response.json()['operation'])
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 8a4d6b3..38cb0b6 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -22,7 +22,7 @@ def test_get(self):
self.assertEqual(name, an_container.name)
def test_get_not_found(self):
- """NameError is raised when the container doesn't exist."""
+ """NotFound is raised when the container doesn't exist."""
def not_found(request, context):
context.status_code = 404
return json.dumps({
@@ -41,6 +41,26 @@ def not_found(request, context):
exceptions.NotFound,
container.Container.get, self.client, name)
+ def test_get_error(self):
+ """LXDAPIException is raised when the LXD API errors."""
+ def not_found(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': not_found,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA
+ })
+
+ name = 'an-missing-container'
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ container.Container.get, self.client, name)
+
def test_create(self):
"""A new container is created."""
config = {'name': 'an-new-container'}
@@ -97,6 +117,25 @@ def not_found(request, context):
self.assertRaises(exceptions.NotFound, an_container.fetch)
+ def test_fetch_error(self):
+ """LXDAPIException is raised on error."""
+ def not_found(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'An bad error',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': not_found,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA
+ })
+
+ an_container = container.Container(
+ name='an-missing-container', _client=self.client)
+
+ self.assertRaises(exceptions.LXDAPIException, an_container.fetch)
+
def test_update(self):
"""A container is updated."""
an_container = container.Container(
@@ -268,3 +307,17 @@ def not_found(request, context):
self.assertRaises(
exceptions.NotFound, self.container.files.get, '/tmp/getted')
+
+ def test_get_error(self):
+ """LXDAPIException is raised on error."""
+ def not_found(request, context):
+ context.status_code = 503
+ rule = {
+ 'text': not_found,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fgetted$', # NOQA
+ }
+ self.add_rule(rule)
+
+ self.assertRaises(
+ exceptions.LXDAPIException, self.container.files.get, '/tmp/getted')
From 8e00ee37b51844dcdef0cbf107359391a2ca7368 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 15:33:52 -0600
Subject: [PATCH 07/10] Fix lint
---
pylxd/tests/test_container.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 38cb0b6..49b7c40 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -320,4 +320,5 @@ def not_found(request, context):
self.add_rule(rule)
self.assertRaises(
- exceptions.LXDAPIException, self.container.files.get, '/tmp/getted')
+ exceptions.LXDAPIException,
+ self.container.files.get, '/tmp/getted')
From 2acb18b43120280eb819141fbaf4b0d86bbf345d Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 15:39:24 -0600
Subject: [PATCH 08/10] Update client back to 100% coverage
---
pylxd/client.py | 4 ++--
pylxd/tests/test_client.py | 18 +++++++++++++++++-
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/pylxd/client.py b/pylxd/client.py
index b8ea444..f25755c 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -13,9 +13,9 @@
# under the License.
import os
-try:
+try: # pragma: no cover
from urllib.parse import quote
-except ImportError:
+except ImportError: # pragma: no cover
from urllib import quote
import requests
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index aaa70d1..bf74bff 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -179,7 +179,7 @@ def test_post_202_sync(self, Session):
node.post)
@mock.patch('pylxd.client.requests.Session')
- def test_post_missing_type(self, Session):
+ def test_post_missing_type_200(self, Session):
"""A missing response type raises an exception."""
response = mock.Mock(**{
'status_code': 200,
@@ -195,6 +195,22 @@ def test_post_missing_type(self, Session):
node.post)
@mock.patch('pylxd.client.requests.Session')
+ def test_post_missing_type_202(self, Session):
+ """A missing response type raises an exception."""
+ response = mock.Mock(**{
+ 'status_code': 202,
+ 'json.return_value': {},
+ })
+ session = mock.Mock(**{'post.return_value': response})
+ Session.return_value = session
+
+ node = client._APINode('http://test.com')
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ node.post)
+
+ @mock.patch('pylxd.client.requests.Session')
def test_put(self, Session):
"""Perform a session put."""
response = mock.Mock(**{
From be69e4c090004d54ce500c3741e6378e2230c1f5 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 17:40:53 -0600
Subject: [PATCH 09/10] Add image export support with accompanying tests
---
integration/test_images.py | 8 ++++++++
pylxd/image.py | 13 ++++++++++++-
pylxd/tests/mock_lxd.py | 5 +++++
pylxd/tests/test_image.py | 43 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/integration/test_images.py b/integration/test_images.py
index 4e2d94f..a6ee5a9 100644
--- a/integration/test_images.py
+++ b/integration/test_images.py
@@ -11,6 +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 hashlib
import time
from pylxd import exceptions
@@ -82,3 +83,10 @@ def test_delete(self):
self.assertRaises(
exceptions.NotFound,
self.client.images.get, self.image.fingerprint)
+
+ def test_export(self):
+ """The imerage is successfully exported."""
+ data = self.image.export()
+ data_sha = hashlib.sha256(data).hexdigest()
+
+ self.assertEqual(self.image.fingerprint, data_sha)
diff --git a/pylxd/image.py b/pylxd/image.py
index 82b965d..a42ada9 100644
--- a/pylxd/image.py
+++ b/pylxd/image.py
@@ -25,7 +25,7 @@ class Image(mixin.Waitable, mixin.Marshallable):
'_client',
'aliases', 'architecture', 'created_at', 'expires_at', 'filename',
'fingerprint', 'properties', 'public', 'size', 'uploaded_at'
- ]
+ ]
@classmethod
def get(cls, client, fingerprint):
@@ -101,3 +101,14 @@ def fetch(self):
for key, val in six.iteritems(response.json()['metadata']):
setattr(self, key, val)
+
+ def export(self):
+ """Export the image."""
+ try:
+ response = self._client.api.images[self.fingerprint].export.get()
+ except exceptions.LXDAPIException as e:
+ if e.response.status_code == 404:
+ raise exceptions.NotFound()
+ raise
+
+ return response.content
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 0bd12aa..cafb71e 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -228,6 +228,11 @@ def profile_GET(request, context):
'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
},
{
+ 'text': '',
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA
+ },
+ {
'text': image_DELETE,
'method': 'DELETE',
'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index b97485f..cc9e6d2 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -129,3 +129,46 @@ def test_delete(self):
a_image = self.client.images.all()[0]
a_image.delete(wait=True)
+
+ def test_export(self):
+ """An image is exported."""
+ a_image = self.client.images.all()[0]
+
+ data = a_image.export()
+ data_sha = hashlib.sha256(data).hexdigest()
+
+ self.assertEqual(a_image.fingerprint, data_sha)
+
+ def test_export_not_found(self):
+ """NotFound is raised on export of bogus image."""
+ def not_found(request, context):
+ context.status_code = 404
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 404})
+ self.add_rule({
+ 'text': not_found,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA
+ })
+ a_image = self.client.images.all()[0]
+
+ self.assertRaises(exceptions.NotFound, a_image.export)
+
+ def test_export_error(self):
+ """LXDAPIException is raised on API error."""
+ def error(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'LOLOLOLOL',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': error,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA
+ })
+ a_image = self.client.images.all()[0]
+
+ self.assertRaises(exceptions.LXDAPIException, a_image.export)
From 2cfb966dde1842b6740b2d0fff9c2ec70856a037 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Fri, 3 Jun 2016 17:50:04 -0600
Subject: [PATCH 10/10] Increase test coverage for pylxd.image
---
pylxd/tests/mock_lxd.py | 8 +++++++-
pylxd/tests/test_image.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index cafb71e..38f4930 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -19,7 +19,7 @@ def images_POST(request, context):
context.status_code = 202
return json.dumps({
'type': 'async',
- 'metadata': {}})
+ 'operation': 'operation-abc'})
def image_DELETE(request, context):
@@ -214,6 +214,7 @@ def profile_GET(request, context):
'cached': False,
'filename': 'a_image.tar.bz2',
'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA
+ 'public': False,
'properties': {},
'size': 1,
'auto_update': False,
@@ -228,6 +229,11 @@ def profile_GET(request, context):
'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
},
{
+ 'text': json.dumps({'type': 'sync'}), # should be async
+ 'method': 'PUT',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
+ },
+ {
'text': '',
'method': 'GET',
'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index cc9e6d2..fa3995a 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -35,6 +35,26 @@ def not_found(request, context):
exceptions.NotFound,
image.Image.get, self.client, fingerprint)
+ def test_get_error(self):
+ """LXDAPIException is raised on error."""
+ def error(request, context):
+ context.status_code = 500
+ return json.dumps({
+ 'type': 'error',
+ 'error': 'Not found',
+ 'error_code': 500})
+ self.add_rule({
+ 'text': error,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
+ })
+
+ fingerprint = hashlib.sha256(b'').hexdigest()
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ image.Image.get, self.client, fingerprint)
+
def test_all(self):
"""A list of all images is returned."""
images = image.Image.all(self.client)
@@ -44,7 +64,7 @@ def test_all(self):
def test_create(self):
"""An image is created."""
fingerprint = hashlib.sha256(b'').hexdigest()
- a_image = image.Image.create(self.client, b'', public=True)
+ a_image = image.Image.create(self.client, b'', public=True, wait=True)
self.assertIsInstance(a_image, image.Image)
self.assertEqual(fingerprint, a_image.fingerprint)
@@ -67,6 +87,13 @@ def create_fail(request, context):
exceptions.CreateFailed,
image.Image.create, self.client, b'')
+ def test_update(self):
+ """An image is updated."""
+ a_image = self.client.images.all()[0]
+ a_image.fetch()
+
+ a_image.update()
+
def test_update_partial_objects(self):
"""A partially fetched image can't be pushed."""
a_image = self.client.images.all()[0]
More information about the lxc-devel
mailing list