[lxc-devel] [pylxd/master] Partial objects and updating
rockstar on Github
lxc-bot at linuxcontainers.org
Sun May 29 05:28:35 UTC 2016
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 882 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160529/2eb17df8/attachment.bin>
-------------- next part --------------
From df572faa8f0d9a53a510c1f24b3f87bddce0c180 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sat, 28 May 2016 15:36:25 -0600
Subject: [PATCH 1/4] Add test for issue #110
---
pylxd/tests/test_profile.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
index 689e5e4..c454c7c 100644
--- a/pylxd/tests/test_profile.py
+++ b/pylxd/tests/test_profile.py
@@ -64,3 +64,9 @@ def error(request, context):
exceptions.CreateFailed,
profile.Profile.create, self.client,
name='an-new-profile', config={})
+
+ def test_partial_objects(self):
+ """A partially fetched profile can't be pushed."""
+ profile = self.client.profiles.all()[0]
+
+ profile.update()
From ddf09fe5b14960fd77007b0db0cafdc4f02afd25 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sat, 28 May 2016 16:28:58 -0600
Subject: [PATCH 2/4] Implement fetch for Profile
---
pylxd/exceptions.py | 12 ++++++++++++
pylxd/profile.py | 15 ++++++++++++++-
pylxd/tests/mock_lxd.py | 3 +++
pylxd/tests/test_profile.py | 32 ++++++++++++++++++++++++++++++--
4 files changed, 59 insertions(+), 3 deletions(-)
diff --git a/pylxd/exceptions.py b/pylxd/exceptions.py
index e30ba39..e910cbc 100644
--- a/pylxd/exceptions.py
+++ b/pylxd/exceptions.py
@@ -27,3 +27,15 @@ class NotFound(_LXDAPIException):
class CreateFailed(_LXDAPIException):
"""Generic create failure exception."""
+
+
+class ObjectIncomplete(Exception):
+ """An exception raised when an object isn't completely populated.
+
+ When an object is fetched via `all`, it isn't a complete object
+ just yet. It requires a call to `fetch` to populate the object before
+ it can be pushed back up to the server.
+ """
+
+ def __str__(self):
+ return 'Object incomplete. Please call `fetch` before updating.'
diff --git a/pylxd/profile.py b/pylxd/profile.py
index ede9c17..934dff4 100644
--- a/pylxd/profile.py
+++ b/pylxd/profile.py
@@ -65,7 +65,10 @@ def __init__(self, **kwargs):
def update(self):
"""Update the profile in LXD based on local changes."""
- marshalled = self.marshall()
+ try:
+ marshalled = self.marshall()
+ except AttributeError:
+ raise exceptions.ObjectIncomplete()
# The name property cannot be updated.
del marshalled['name']
@@ -81,3 +84,13 @@ def rename(self, new):
def delete(self):
"""Delete a profile."""
self._client.api.profiles[self.name].delete()
+
+ 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())
+
+ for key, val in six.iteritems(response.json()['metadata']):
+ setattr(self, key, val)
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 5b249ac..4f83a71 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -27,6 +27,9 @@ def profile_GET(request, context):
return json.dumps({
'metadata': {
'name': name,
+ 'description': 'An description',
+ 'config': {},
+ 'devices': {},
},
})
else:
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
index c454c7c..8839f0d 100644
--- a/pylxd/tests/test_profile.py
+++ b/pylxd/tests/test_profile.py
@@ -67,6 +67,34 @@ def error(request, context):
def test_partial_objects(self):
"""A partially fetched profile can't be pushed."""
- profile = self.client.profiles.all()[0]
+ an_profile = self.client.profiles.all()[0]
- profile.update()
+ self.assertRaises(
+ exceptions.ObjectIncomplete,
+ an_profile.update)
+
+ def test_fetch(self):
+ """A partially fetched profile is made complete."""
+ an_profile = self.client.profiles.all()[0]
+
+ an_profile.fetch()
+
+ self.assertEqual('An description', an_profile.description)
+
+ def test_fetch_notfound(self):
+ """NotFound is raised on bogus profile fetches."""
+ 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/profiles/(?P<container_name>.*)$',
+ })
+
+ an_profile = profile.Profile(name='an-profile', _client=self.client)
+
+ self.assertRaises(exceptions.NotFound, an_profile.fetch)
From 493236aecc3d704bd84d376b2c57ca3345d2fc4b Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sat, 28 May 2016 16:42:18 -0600
Subject: [PATCH 3/4] Implement ObjectIncomplete exception on
`Container.update`
---
pylxd/container.py | 10 ++++++++--
pylxd/tests/test_container.py | 8 ++++++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/pylxd/container.py b/pylxd/container.py
index 61f8c99..f330401 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -80,7 +80,7 @@ def __init__(self, **kwargs):
for key, value in six.iteritems(kwargs):
setattr(self, key, value)
- def reload(self):
+ def fetch(self):
"""Reload the container information."""
response = self._client.api.containers[self.name].get()
if response.status_code == 404:
@@ -88,10 +88,16 @@ def reload(self):
'Container named "{}" has gone away'.format(self.name))
for key, value in six.iteritems(response.json()['metadata']):
setattr(self, key, value)
+ # XXX: rockstar (28 Mar 2016) - This method was named improperly
+ # originally. It's being kept here for backwards compatibility.
+ reload = fetch
def update(self, wait=False):
"""Update the container in lxd from local changes."""
- marshalled = self.marshall()
+ try:
+ marshalled = self.marshall()
+ except AttributeError:
+ raise exceptions.ObjectIncomplete()
# These two properties are explicitly not allowed.
del marshalled['name']
del marshalled['status']
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 4756d40..b6620ba 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -115,6 +115,14 @@ def test_update(self):
self.assertTrue(an_container.ephemeral)
+ def test_update_partial_objects(self):
+ """A partially fetched profile can't be pushed."""
+ an_container = self.client.containers.all()[0]
+
+ self.assertRaises(
+ exceptions.ObjectIncomplete,
+ an_container.update)
+
def test_rename(self):
an_container = container.Container(
name='an-container', _client=self.client)
From b6206e7d2bcaf4a6633662abf978b16998a2deb8 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sat, 28 May 2016 23:22:07 -0600
Subject: [PATCH 4/4] Add Image.fetch and impartial image state knowledge to
Image
---
pylxd/image.py | 16 +++++++++++++++-
pylxd/tests/mock_lxd.py | 19 ++++++++++++++++++-
pylxd/tests/test_image.py | 35 +++++++++++++++++++++++++++++++++++
3 files changed, 68 insertions(+), 2 deletions(-)
diff --git a/pylxd/image.py b/pylxd/image.py
index 1927d2c..535c083 100644
--- a/pylxd/image.py
+++ b/pylxd/image.py
@@ -73,8 +73,12 @@ def __init__(self, **kwargs):
def update(self):
"""Update LXD based on changes to this image."""
+ try:
+ marshalled = self.marshall()
+ except AttributeError:
+ raise exceptions.ObjectIncomplete()
self._client.api.images[self.fingerprint].put(
- json=self.marshall())
+ json=marshalled)
def delete(self, wait=False):
"""Delete the image."""
@@ -82,3 +86,13 @@ def delete(self, wait=False):
if wait:
self.wait_for_operation(response.json()['operation'])
+
+ 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())
+
+ for key, val in six.iteritems(response.json()['metadata']):
+ setattr(self, key, val)
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 4f83a71..e4f1600 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -92,7 +92,7 @@ def profile_GET(request, context):
# Images
{
'text': json.dumps({'metadata': [
- 'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA
+ 'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA
]}),
'method': 'GET',
'url': r'^http://pylxd.test/1.0/images$',
@@ -105,7 +105,24 @@ def profile_GET(request, context):
{
'text': json.dumps({
'metadata': {
+ 'aliases': [
+ {
+ 'name': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA
+ 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA
+ }
+ ],
+ 'architecture': 'x86_64',
+ 'cached': False,
+ 'filename': 'a_image.tar.bz2',
'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA
+ 'properties': {},
+ 'size': 1,
+ 'auto_update': False,
+ 'created_at': '1983-06-16T02:42:00Z',
+ 'expires_at': '1983-06-16T02:42:00Z',
+ 'last_used_at': '1983-06-16T02:42:00Z',
+ 'uploaded_at': '1983-06-16T02:42:00Z',
+
},
}),
'method': 'GET',
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index 626c191..37f9f30 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -66,3 +66,38 @@ def create_fail(request, context):
self.assertRaises(
exceptions.CreateFailed,
image.Image.create, self.client, b'')
+
+ def test_update_partial_objects(self):
+ """A partially fetched image can't be pushed."""
+ a_image = self.client.images.all()[0]
+
+ self.assertRaises(
+ exceptions.ObjectIncomplete,
+ a_image.update)
+
+ def test_fetch(self):
+ """A partial object is fetched and populated."""
+ a_image = self.client.images.all()[0]
+
+ a_image.fetch()
+
+ self.assertEqual(1, a_image.size)
+
+ def test_fetch_notfound(self):
+ """A bogus image fetch raises NotFound."""
+ 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$', # NOQA
+ })
+ fingerprint = hashlib.sha256(b'').hexdigest()
+
+ a_image = image.Image(fingerprint=fingerprint, _client=self.client)
+
+ self.assertRaises(exceptions.NotFound, a_image.fetch)
More information about the lxc-devel
mailing list