[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