[lxc-devel] [pylxd/master] Update all objects to the model API

rockstar on Github lxc-bot at linuxcontainers.org
Mon Jun 27 22:56:54 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 444 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160627/a913988a/attachment.bin>
-------------- next part --------------
From 2422bbb4120594b854be143d095e174824d5690c Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sat, 25 Jun 2016 22:55:19 -0600
Subject: [PATCH 1/9] Update Certificate to use the model code

---
 pylxd/certificate.py            | 34 +++++++++++-----------------------
 pylxd/model.py                  |  5 ++++-
 pylxd/network.py                |  4 ++++
 pylxd/tests/test_certificate.py |  4 ++--
 4 files changed, 21 insertions(+), 26 deletions(-)

diff --git a/pylxd/certificate.py b/pylxd/certificate.py
index 25b0087..7f4fc1e 100644
--- a/pylxd/certificate.py
+++ b/pylxd/certificate.py
@@ -17,23 +17,23 @@
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.serialization import Encoding
-import six
 
+from pylxd import model
 
-class Certificate(object):
+
+class Certificate(model.Model):
     """A LXD certificate."""
 
-    __slots__ = [
-        '_client',
-        'certificate', 'fingerprint', 'type',
-    ]
+    certificate = model.Attribute()
+    fingerprint = model.Attribute()
+    type = model.Attribute()
 
     @classmethod
     def get(cls, client, fingerprint):
         """Get a certificate by fingerprint."""
         response = client.api.certificates[fingerprint].get()
 
-        return cls(_client=client, **response.json()['metadata'])
+        return cls(client, **response.json()['metadata'])
 
     @classmethod
     def all(cls, client):
@@ -43,7 +43,7 @@ def all(cls, client):
         certs = []
         for cert in response.json()['metadata']:
             fingerprint = cert.split('/')[-1]
-            certs.append(cls(_client=client, fingerprint=fingerprint))
+            certs.append(cls(client, fingerprint=fingerprint))
         return certs
 
     @classmethod
@@ -66,18 +66,6 @@ def create(cls, client, password, cert_data):
             cert.fingerprint(hashes.SHA256())).decode('utf-8')
         return cls.get(client, fingerprint)
 
-    def __init__(self, **kwargs):
-        super(Certificate, self).__init__()
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
-
-    def delete(self):
-        """Delete the certificate."""
-        self._client.api.certificates[self.fingerprint].delete()
-
-    def fetch(self):
-        """Fetch an updated representation of the certificate."""
-        response = self._client.api.certificates[self.fingerprint].get()
-
-        for key, value in six.iteritems(response.json()['metadata']):
-            setattr(self, key, value)
+    @property
+    def api(self):
+        return self.client.api.certificates[self.fingerprint]
diff --git a/pylxd/model.py b/pylxd/model.py
index 58ec560..778e4ef 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -13,6 +13,8 @@
 #    under the License.
 import six
 
+from pylxd.deprecation import deprecated
+
 
 class Attribute(object):
     """A metadata class for model attributes."""
@@ -115,6 +117,7 @@ def sync(self):
         response = self.api.get()
         for key, val in response.json()['metadata'].items():
             setattr(self, key, val)
+    fetch = deprecated("fetch is deprecated; please use sync")(sync)
 
     def save(self):
         """Save data to the server.
@@ -127,4 +130,4 @@ def save(self):
 
     def delete(self):
         """Delete an object from the server."""
-        raise NotImplementedError('delete is not implemented')
+        self.api.delete()
diff --git a/pylxd/network.py b/pylxd/network.py
index 9609172..5c24165 100644
--- a/pylxd/network.py
+++ b/pylxd/network.py
@@ -42,3 +42,7 @@ def all(cls, client):
     @property
     def api(self):
         return self.client.api.networks[self.name]
+
+    def delete(self):
+        """Delete an object from the server."""
+        raise NotImplementedError('delete is not implemented')
diff --git a/pylxd/tests/test_certificate.py b/pylxd/tests/test_certificate.py
index 34a85eb..e3a7f86 100644
--- a/pylxd/tests/test_certificate.py
+++ b/pylxd/tests/test_certificate.py
@@ -46,7 +46,7 @@ def test_create(self):
     def test_fetch(self):
         """A partial object is fully fetched."""
         an_certificate = certificate.Certificate(
-            _client=self.client, fingerprint='an-certificate')
+            self.client, fingerprint='an-certificate')
 
         an_certificate.fetch()
 
@@ -57,6 +57,6 @@ def test_delete(self):
         # XXX: rockstar (08 Jun 2016) - This just executes a code path. An
         # assertion should be added.
         an_certificate = certificate.Certificate(
-            _client=self.client, fingerprint='an-certificate')
+            self.client, fingerprint='an-certificate')
 
         an_certificate.delete()

From 6c6e66026b4decb6a4db603b8ab2cecc6ced58d3 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sun, 26 Jun 2016 10:32:02 -0600
Subject: [PATCH 2/9] Convert the Profile to the model api

---
 pylxd/model.py              | 15 ++++++++++++++-
 pylxd/profile.py            | 42 ++++++++++++------------------------------
 pylxd/tests/test_profile.py | 12 ++----------
 3 files changed, 28 insertions(+), 41 deletions(-)

diff --git a/pylxd/model.py b/pylxd/model.py
index 778e4ef..e84200b 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -13,6 +13,7 @@
 #    under the License.
 import six
 
+from pylxd import exceptions
 from pylxd.deprecation import deprecated
 
 
@@ -114,7 +115,12 @@ def sync(self):
         """
         # XXX: rockstar (25 Jun 2016) - This has the potential to step
         # on existing attributes.
-        response = self.api.get()
+        try:
+            response = self.api.get()
+        except exceptions.LXDAPIException as e:
+            if e.response.status_code == 404:
+                raise exceptions.NotFound()
+            raise
         for key, val in response.json()['metadata'].items():
             setattr(self, key, val)
     fetch = deprecated("fetch is deprecated; please use sync")(sync)
@@ -131,3 +137,10 @@ def save(self):
     def delete(self):
         """Delete an object from the server."""
         self.api.delete()
+
+    def marshall(self):
+        """Marshall the object in preparation for updating to the server."""
+        marshalled = {}
+        for key, val in self.__attributes__.items():
+            marshalled[key] = getattr(self, key)
+        return marshalled
diff --git a/pylxd/profile.py b/pylxd/profile.py
index 1bcc0e0..fe68c9d 100644
--- a/pylxd/profile.py
+++ b/pylxd/profile.py
@@ -11,17 +11,16 @@
 #    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 six
-from pylxd import exceptions, mixin
+from pylxd import exceptions, model
 
 
-class Profile(mixin.Marshallable):
+class Profile(model.Model):
     """A LXD profile."""
 
-    __slots__ = [
-        '_client',
-        'config', 'devices', 'name'
-        ]
+    name = model.Attribute()
+    description = model.Attribute()
+    config = model.Attribute()
+    devices = model.Attribute()
 
     @classmethod
     def get(cls, client, name):
@@ -33,7 +32,7 @@ def get(cls, client, name):
                 raise exceptions.NotFound()
             raise
 
-        return cls(_client=client, **response.json()['metadata'])
+        return cls(client, **response.json()['metadata'])
 
     @classmethod
     def all(cls, client):
@@ -43,7 +42,7 @@ def all(cls, client):
         profiles = []
         for url in response.json()['metadata']:
             name = url.split('/')[-1]
-            profiles.append(cls(_client=client, name=name))
+            profiles.append(cls(client, name=name))
         return profiles
 
     @classmethod
@@ -61,10 +60,9 @@ def create(cls, client, name, config=None, devices=None):
 
         return cls.get(client, name)
 
-    def __init__(self, **kwargs):
-        super(Profile, self).__init__()
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
+    @property
+    def api(self):
+        return self.client.api.profiles[self.name]
 
     def update(self):
         """Update the profile in LXD based on local changes."""
@@ -75,25 +73,9 @@ def update(self):
         # The name property cannot be updated.
         del marshalled['name']
 
-        self._client.api.profiles[self.name].put(json=marshalled)
+        self.api.put(json=marshalled)
 
     def rename(self, new):
         """Rename the profile."""
         raise NotImplementedError(
             'LXD does not currently support renaming profiles')
-
-    def delete(self):
-        """Delete a profile."""
-        self._client.api.profiles[self.name].delete()
-
-    def fetch(self):
-        """Fetch the object from LXD, populating attributes."""
-        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)
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
index d964ec7..2d7b80a 100644
--- a/pylxd/tests/test_profile.py
+++ b/pylxd/tests/test_profile.py
@@ -101,14 +101,6 @@ def test_update(self):
 
         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]
-
-        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]
@@ -131,7 +123,7 @@ def not_found(request, context):
             'url': r'^http://pylxd.test/1.0/profiles/an-profile$',
         })
 
-        an_profile = profile.Profile(name='an-profile', _client=self.client)
+        an_profile = profile.Profile(self.client, name='an-profile')
 
         self.assertRaises(exceptions.NotFound, an_profile.fetch)
 
@@ -149,7 +141,7 @@ def error(request, context):
             'url': r'^http://pylxd.test/1.0/profiles/an-profile$',
         })
 
-        an_profile = profile.Profile(name='an-profile', _client=self.client)
+        an_profile = profile.Profile(self.client, name='an-profile')
 
         self.assertRaises(exceptions.LXDAPIException, an_profile.fetch)
 

From fe2f5212f8a3e7e7d5c3cb70a8c63b153752d851 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sun, 26 Jun 2016 11:02:10 -0600
Subject: [PATCH 3/9] Update Image to the model api

---
 pylxd/image.py            | 65 +++++++++++++++++------------------------------
 pylxd/model.py            |  9 +++++--
 pylxd/operation.py        |  1 -
 pylxd/tests/test_image.py | 12 ++-------
 4 files changed, 33 insertions(+), 54 deletions(-)

diff --git a/pylxd/image.py b/pylxd/image.py
index ccc1f9d..e4d02c3 100644
--- a/pylxd/image.py
+++ b/pylxd/image.py
@@ -13,20 +13,29 @@
 #    under the License.
 import hashlib
 
-import six
-
-from pylxd import exceptions, mixin
+from pylxd import exceptions, model
 from pylxd.operation import Operation
 
 
-class Image(mixin.Marshallable):
+class Image(model.Model):
     """A LXD Image."""
-
-    __slots__ = [
-        '_client',
-        'aliases', 'architecture', 'created_at', 'expires_at', 'filename',
-        'fingerprint', 'properties', 'public', 'size', 'uploaded_at'
-    ]
+    aliases = model.Attribute()
+    auto_update = model.Attribute()
+    architecture = model.Attribute()
+    cached = model.Attribute()
+    created_at = model.Attribute()
+    expires_at = model.Attribute()
+    filename = model.Attribute()
+    fingerprint = model.Attribute()
+    last_used_at = model.Attribute()
+    properties = model.Attribute()
+    public = model.Attribute()
+    size = model.Attribute()
+    uploaded_at = model.Attribute()
+
+    @property
+    def api(self):
+        return self.client.api.images[self.fingerprint]
 
     @classmethod
     def get(cls, client, fingerprint):
@@ -38,7 +47,7 @@ def get(cls, client, fingerprint):
                 raise exceptions.NotFound()
             raise
 
-        image = cls(_client=client, **response.json()['metadata'])
+        image = cls(client, **response.json()['metadata'])
         return image
 
     @classmethod
@@ -49,7 +58,7 @@ def all(cls, client):
         images = []
         for url in response.json()['metadata']:
             fingerprint = url.split('/')[-1]
-            images.append(cls(_client=client, fingerprint=fingerprint))
+            images.append(cls(client, fingerprint=fingerprint))
         return images
 
     @classmethod
@@ -68,12 +77,7 @@ def create(cls, client, image_data, public=False, wait=False):
 
         if wait:
             Operation.wait_for_operation(client, response.json()['operation'])
-        return cls(_client=client, fingerprint=fingerprint)
-
-    def __init__(self, **kwargs):
-        super(Image, self).__init__()
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
+        return cls(client, fingerprint=fingerprint)
 
     def update(self):
         """Update LXD based on changes to this image."""
@@ -81,33 +85,12 @@ def update(self):
             marshalled = self.marshall()
         except AttributeError:
             raise exceptions.ObjectIncomplete()
-        self._client.api.images[self.fingerprint].put(
-            json=marshalled)
-
-    def delete(self, wait=False):
-        """Delete the image."""
-        response = self._client.api.images[self.fingerprint].delete()
-
-        if wait:
-            Operation.wait_for_operation(
-                self._client, response.json()['operation'])
-
-    def fetch(self):
-        """Fetch the object from LXD, populating attributes."""
-        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)
+        self.api.put(json=marshalled)
 
     def export(self):
         """Export the image."""
         try:
-            response = self._client.api.images[self.fingerprint].export.get()
+            response = self.api.export.get()
         except exceptions.LXDAPIException as e:
             if e.response.status_code == 404:
                 raise exceptions.NotFound()
diff --git a/pylxd/model.py b/pylxd/model.py
index e84200b..3a5f601 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -15,6 +15,7 @@
 
 from pylxd import exceptions
 from pylxd.deprecation import deprecated
+from pylxd.operation import Operation
 
 
 class Attribute(object):
@@ -134,9 +135,13 @@ def save(self):
         """
         raise NotImplementedError('save is not implemented')
 
-    def delete(self):
+    def delete(self, wait=False):
         """Delete an object from the server."""
-        self.api.delete()
+        response = self.api.delete()
+
+        if response.json()['type'] == 'async' and wait:
+            Operation.wait_for_operation(
+                self.client, response.json()['operation'])
 
     def marshall(self):
         """Marshall the object in preparation for updating to the server."""
diff --git a/pylxd/operation.py b/pylxd/operation.py
index 5ec9ea0..09c9d24 100644
--- a/pylxd/operation.py
+++ b/pylxd/operation.py
@@ -11,7 +11,6 @@
 #    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 six
 
 
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index fa3995a..bac6a5e 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -94,14 +94,6 @@ def test_update(self):
 
         a_image.update()
 
-    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]
@@ -125,7 +117,7 @@ def not_found(request, context):
         })
         fingerprint = hashlib.sha256(b'').hexdigest()
 
-        a_image = image.Image(fingerprint=fingerprint, _client=self.client)
+        a_image = image.Image(self.client, fingerprint=fingerprint)
 
         self.assertRaises(exceptions.NotFound, a_image.fetch)
 
@@ -144,7 +136,7 @@ def not_found(request, context):
         })
         fingerprint = hashlib.sha256(b'').hexdigest()
 
-        a_image = image.Image(fingerprint=fingerprint, _client=self.client)
+        a_image = image.Image(self.client, fingerprint=fingerprint)
 
         self.assertRaises(exceptions.LXDAPIException, a_image.fetch)
 

From dfb21a5ea5bc402854c32d3b013403a3d1e4d3f2 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sun, 26 Jun 2016 17:25:07 -0600
Subject: [PATCH 4/9] Update Container to the model api

---
 pylxd/client.py               |  8 ----
 pylxd/container.py            | 91 +++++++++++++++++++------------------------
 pylxd/model.py                | 14 +++++++
 pylxd/tests/mock_lxd.py       |  4 ++
 pylxd/tests/test_client.py    | 32 ---------------
 pylxd/tests/test_container.py | 16 ++++----
 6 files changed, 66 insertions(+), 99 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index 2890b43..81a278c 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -74,14 +74,6 @@ def _assert_response(self, response, allowed_status_codes=(200,)):
                 # 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."""
         response = self.session.get(self._api_endpoint, *args, **kwargs)
diff --git a/pylxd/container.py b/pylxd/container.py
index 164601b..4286894 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -18,7 +18,7 @@
 from ws4py.client import WebSocketBaseClient
 from ws4py.manager import WebSocketManager
 
-from pylxd import exceptions, managers, mixin
+from pylxd import exceptions, managers, mixin, model
 from pylxd.deprecation import deprecated
 from pylxd.operation import Operation
 
@@ -31,12 +31,29 @@ def __init__(self, **kwargs):
             setattr(self, key, value)
 
 
-class Container(mixin.Marshallable):
+class Container(model.Model):
     """An LXD Container.
 
     This class is not intended to be used directly, but rather to be used
     via `Client.containers.create`.
     """
+    architecture = model.Attribute()
+    config = model.Attribute()
+    created_at = model.Attribute()
+    devices = model.Attribute()
+    ephemeral = model.Attribute()
+    expanded_config = model.Attribute()
+    expanded_devices = model.Attribute()
+    name = model.Attribute()
+    profiles = model.Attribute()
+    status = model.Attribute()
+
+    snapshots = model.Manager()
+    files = model.Manager()
+
+    @property
+    def api(self):
+        return self.client.api.containers[self.name]
 
     class FilesManager(object):
         """A pseudo-manager for namespacing file operations."""
@@ -63,12 +80,6 @@ def get(self, filepath):
                 raise
             return response.content
 
-    __slots__ = [
-        '_client',
-        'architecture', 'config', 'created_at', 'devices', 'ephemeral',
-        'expanded_config', 'expanded_devices', 'name', 'profiles', 'status'
-    ]
-
     @classmethod
     def get(cls, client, name):
         """Get a container by name."""
@@ -79,7 +90,7 @@ def get(cls, client, name):
                 raise exceptions.NotFound()
             raise
 
-        container = cls(_client=client, **response.json()['metadata'])
+        container = cls(client, **response.json()['metadata'])
         return container
 
     @classmethod
@@ -96,7 +107,7 @@ def all(cls, client):
         containers = []
         for url in response.json()['metadata']:
             name = url.split('/')[-1]
-            containers.append(cls(_client=client, name=name))
+            containers.append(cls(client, name=name))
         return containers
 
     @classmethod
@@ -109,31 +120,19 @@ def create(cls, client, config, wait=False):
 
         if wait:
             Operation.wait_for_operation(client, response.json()['operation'])
-        return cls(name=config['name'], _client=client)
+        return cls(client, name=config['name'])
 
-    def __init__(self, **kwargs):
-        super(Container, self).__init__()
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
+    def __init__(self, *args, **kwargs):
+        super(Container, self).__init__(*args, **kwargs)
 
-        self.snapshots = managers.SnapshotManager(self._client, self)
-        self.files = self.FilesManager(self._client, self)
+        self.snapshots = managers.SnapshotManager(self.client, self)
+        self.files = self.FilesManager(self.client, self)
 
-    def fetch(self):
-        """Reload the container information."""
-        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
     # originally. It's being kept here for backwards compatibility.
     reload = deprecated(
-        "Container.reload is deprecated. Please use Container.fetch")(
-        fetch)
+        "Container.reload is deprecated. Please use Container.sync")(
+        model.Model.sync)
 
     def update(self, wait=False):
         """Update the container in lxd from local changes."""
@@ -145,44 +144,34 @@ def update(self, wait=False):
         del marshalled['name']
         del marshalled['status']
 
-        response = self._client.api.containers[self.name].put(
-            json=marshalled)
+        response = self.api.put(json=marshalled)
 
         if wait:
             Operation.wait_for_operation(
-                self._client, response.json()['operation'])
+                self.client, response.json()['operation'])
 
     def rename(self, name, wait=False):
         """Rename a container."""
-        response = self._client.api.containers[
-            self.name].post(json={'name': name})
+        response = self.api.post(json={'name': name})
 
         if wait:
             Operation.wait_for_operation(
-                self._client, response.json()['operation'])
+                self.client, response.json()['operation'])
         self.name = name
 
-    def delete(self, wait=False):
-        """Delete the container."""
-        response = self._client.api.containers[self.name].delete()
-
-        if wait:
-            Operation.wait_for_operation(
-                self._client, response.json()['operation'])
-
     def _set_state(self, state, timeout=30, force=True, wait=False):
-        response = self._client.api.containers[self.name].state.put(json={
+        response = self.api.state.put(json={
             'action': state,
             'timeout': timeout,
             'force': force
         })
         if wait:
             Operation.wait_for_operation(
-                self._client, response.json()['operation'])
+                self.client, response.json()['operation'])
             self.fetch()
 
     def state(self):
-        response = self._client.api.containers[self.name].state.get()
+        response = self.api.state.get()
         state = ContainerState(**response.json()['metadata'])
         return state
 
@@ -257,7 +246,7 @@ def execute(self, commands, environment={}):
         """Execute a command on the container."""
         if isinstance(commands, six.string_types):
             raise TypeError("First argument must be a list.")
-        response = self._client.api.containers[self.name]['exec'].post(json={
+        response = self.api['exec'].post(json={
             'command': commands,
             'environment': environment,
             'wait-for-websocket': True,
@@ -267,17 +256,17 @@ def execute(self, commands, environment={}):
         fds = response.json()['metadata']['metadata']['fds']
         operation_id = response.json()['operation'].split('/')[-1]
         parsed = parse.urlparse(
-            self._client.api.operations[operation_id].websocket._api_endpoint)
+            self.client.api.operations[operation_id].websocket._api_endpoint)
 
         manager = WebSocketManager()
 
-        stdin = _StdinWebsocket(manager, self._client.websocket_url)
+        stdin = _StdinWebsocket(manager, self.client.websocket_url)
         stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
         stdin.connect()
-        stdout = _CommandWebsocketClient(manager, self._client.websocket_url)
+        stdout = _CommandWebsocketClient(manager, self.client.websocket_url)
         stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
         stdout.connect()
-        stderr = _CommandWebsocketClient(manager, self._client.websocket_url)
+        stderr = _CommandWebsocketClient(manager, self.client.websocket_url)
         stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
         stderr.connect()
 
diff --git a/pylxd/model.py b/pylxd/model.py
index 3a5f601..012f6ea 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -25,6 +25,14 @@ def __init__(self, validator=None):
         self.validator = validator
 
 
+class Manager(object):
+    """A manager declaration.
+
+    This class signals to the model that it will have a Manager
+    attribute.
+    """
+
+
 class ModelType(type):
     """A Model metaclass.
 
@@ -37,11 +45,15 @@ def __new__(cls, name, bases, attrs):
             raise TypeError('__slots__ should not be specified.')
         attributes = {}
         for_removal = []
+        managers = []
 
         for key, val in attrs.items():
             if type(val) == Attribute:
                 attributes[key] = val
                 for_removal.append(key)
+            if type(val) == Manager:
+                managers.append(key)
+                for_removal.append(key)
         for key in for_removal:
             del attrs[key]
 
@@ -51,6 +63,8 @@ def __new__(cls, name, bases, attrs):
         for base in bases:
             if '__slots__' in dir(base):
                 slots = slots + base.__slots__
+        if len(managers) > 0:
+            slots = slots + managers
         attrs['__slots__'] = slots
         attrs['__attributes__'] = attributes
 
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 8f5d531..8affaa1 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -112,6 +112,10 @@ def profile_GET(request, context):
         'url': r'^http://pylxd.test/1.0/certificates/an-certificate$',
     },
     {
+        'json': {
+            'type': 'sync',
+            'metadata': {},
+        },
         'status_code': 202,
         'method': 'DELETE',
         'url': r'^http://pylxd.test/1.0/certificates/an-certificate$',
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index 3dbc65e..b0a0030 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -275,22 +275,6 @@ def test_post_200_not_sync(self, Session):
             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_200(self, Session):
         """A missing response type raises an exception."""
         response = mock.Mock(**{
@@ -307,22 +291,6 @@ def test_post_missing_type_200(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(**{
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 35b17e7..bb3ff6f 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -94,7 +94,7 @@ def create_fail(request, context):
     def test_fetch(self):
         """A fetch updates the properties of a container."""
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
 
         an_container.fetch()
 
@@ -115,7 +115,7 @@ def not_found(request, context):
         })
 
         an_container = container.Container(
-            name='an-missing-container', _client=self.client)
+            self.client, name='an-missing-container')
 
         self.assertRaises(exceptions.NotFound, an_container.fetch)
 
@@ -134,14 +134,14 @@ def not_found(request, context):
         })
 
         an_container = container.Container(
-            name='an-missing-container', _client=self.client)
+            self.client, name='an-missing-container')
 
         self.assertRaises(exceptions.LXDAPIException, an_container.fetch)
 
     def test_update(self):
         """A container is updated."""
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
         an_container.architecture = 1
         an_container.config = {}
         an_container.created_at = 1
@@ -166,7 +166,7 @@ def test_update_partial_objects(self):
 
     def test_rename(self):
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
 
         an_container.rename('an-renamed-container', wait=True)
 
@@ -178,7 +178,7 @@ def test_delete(self):
         # a code path. There should be an assertion here, but
         # it's not clear how to assert that, just yet.
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
 
         an_container.delete(wait=True)
 
@@ -192,7 +192,7 @@ def test_execute(self, _CommandWebsocketClient, _StdinWebsocket):
         _CommandWebsocketClient.return_value = fake_websocket
 
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
 
         stdout, _ = an_container.execute(['echo', 'test'])
 
@@ -201,7 +201,7 @@ def test_execute(self, _CommandWebsocketClient, _StdinWebsocket):
     def test_execute_string(self):
         """A command passed as string raises a TypeError."""
         an_container = container.Container(
-            name='an-container', _client=self.client)
+            self.client, name='an-container')
 
         self.assertRaises(TypeError, an_container.execute, 'apt-get update')
 

From e26ab48771ec12ad9a8455ac66921ad721ef4756 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 09:13:30 -0600
Subject: [PATCH 5/9] Convert Snapshot to the model API

---
 pylxd/container.py            | 41 +++++++++++++++++------------------------
 pylxd/model.py                |  9 ++++++++-
 pylxd/tests/test_container.py | 10 +++++-----
 3 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 4286894..0b86b9c 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -18,7 +18,7 @@
 from ws4py.client import WebSocketBaseClient
 from ws4py.manager import WebSocketManager
 
-from pylxd import exceptions, managers, mixin, model
+from pylxd import exceptions, managers, model
 from pylxd.deprecation import deprecated
 from pylxd.operation import Operation
 
@@ -314,8 +314,17 @@ def handshake_ok(self):
         self.close()
 
 
-class Snapshot(mixin.Marshallable):
+class Snapshot(model.Model):
     """A container snapshot."""
+    name = model.Attribute()
+    stateful = model.Attribute()
+
+    container = model.Parent()
+
+    @property
+    def api(self):
+        return self.client.api.containers[
+            self.container.name].snapshots[self.name]
 
     @classmethod
     def get(cls, client, container, name):
@@ -328,7 +337,7 @@ def get(cls, client, container, name):
             raise
 
         snapshot = cls(
-            _client=client, _container=container,
+            client, container=container,
             **response.json()['metadata'])
         # Snapshot names are namespaced in LXD, as
         # container-name/snapshot-name. We hide that implementation
@@ -341,8 +350,8 @@ def all(cls, client, container):
         response = client.api.containers[container.name].snapshots.get()
 
         return [cls(
-                name=snapshot.split('/')[-1], _client=client,
-                _container=container)
+                client, name=snapshot.split('/')[-1],
+                container=container)
                 for snapshot in response.json()['metadata']]
 
     @classmethod
@@ -350,31 +359,15 @@ def create(cls, client, container, name, stateful=False, wait=False):
         response = client.api.containers[container.name].snapshots.post(json={
             'name': name, 'stateful': stateful})
 
-        snapshot = cls(_client=client, _container=container, name=name)
+        snapshot = cls(client, container=container, name=name)
         if wait:
             Operation.wait_for_operation(client, response.json()['operation'])
         return snapshot
 
-    def __init__(self, **kwargs):
-        super(Snapshot, self).__init__()
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
-
     def rename(self, new_name, wait=False):
         """Rename a snapshot."""
-        response = self._client.api.containers[
-            self._container.name].snapshots[self.name].post(
-            json={'name': new_name})
+        response = self.api.post(json={'name': new_name})
         if wait:
             Operation.wait_for_operation(
-                self._client, response.json()['operation'])
+                self.client, response.json()['operation'])
         self.name = new_name
-
-    def delete(self, wait=False):
-        """Delete a snapshot."""
-        response = self._client.api.containers[
-            self._container.name].snapshots[self.name].delete()
-
-        if wait:
-            Operation.wait_for_operation(
-                self._client, response.json()['operation'])
diff --git a/pylxd/model.py b/pylxd/model.py
index 012f6ea..bcfdd43 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -33,6 +33,13 @@ class Manager(object):
     """
 
 
+class Parent(object):
+    """A parent declaration.
+
+    Child managers must keep a reference to their parent.
+    """
+
+
 class ModelType(type):
     """A Model metaclass.
 
@@ -51,7 +58,7 @@ def __new__(cls, name, bases, attrs):
             if type(val) == Attribute:
                 attributes[key] = val
                 for_removal.append(key)
-            if type(val) == Manager:
+            if type(val) in (Manager, Parent):
                 managers.append(key)
                 for_removal.append(key)
         for key in for_removal:
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index bb3ff6f..d1b2b86 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -269,8 +269,8 @@ def test_all(self):
 
         self.assertEqual(1, len(snapshots))
         self.assertEqual('an-snapshot', snapshots[0].name)
-        self.assertEqual(self.client, snapshots[0]._client)
-        self.assertEqual(self.container, snapshots[0]._container)
+        self.assertEqual(self.client, snapshots[0].client)
+        self.assertEqual(self.container, snapshots[0].container)
 
     def test_create(self):
         """Create a snapshot."""
@@ -290,7 +290,7 @@ def setUp(self):
     def test_rename(self):
         """A snapshot is renamed."""
         snapshot = container.Snapshot(
-            _client=self.client, _container=self.container,
+            self.client, container=self.container,
             name='an-snapshot')
 
         snapshot.rename('an-renamed-snapshot', wait=True)
@@ -300,7 +300,7 @@ def test_rename(self):
     def test_delete(self):
         """A snapshot is deleted."""
         snapshot = container.Snapshot(
-            _client=self.client, _container=self.container,
+            self.client, container=self.container,
             name='an-snapshot')
 
         snapshot.delete(wait=True)
@@ -322,7 +322,7 @@ def not_found(request, context):
         })
 
         snapshot = container.Snapshot(
-            _client=self.client, _container=self.container,
+            self.client, container=self.container,
             name='an-snapshot')
 
         self.assertRaises(exceptions.LXDAPIException, snapshot.delete)

From 20c133b5f12ab937b5463699ad8c895895eb6707 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 12:34:05 -0600
Subject: [PATCH 6/9] Remove calls to `update`, opting for `save`

---
 pylxd/container.py | 20 ++------------------
 pylxd/image.py     |  8 --------
 pylxd/model.py     | 20 ++++++++++++++++----
 pylxd/network.py   |  6 +++++-
 pylxd/profile.py   | 13 +------------
 5 files changed, 24 insertions(+), 43 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 0b86b9c..5b5ec07 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -44,9 +44,9 @@ class Container(model.Model):
     ephemeral = model.Attribute()
     expanded_config = model.Attribute()
     expanded_devices = model.Attribute()
-    name = model.Attribute()
+    name = model.Attribute(readonly=True)
     profiles = model.Attribute()
-    status = model.Attribute()
+    status = model.Attribute(readonly=True)
 
     snapshots = model.Manager()
     files = model.Manager()
@@ -134,22 +134,6 @@ def __init__(self, *args, **kwargs):
         "Container.reload is deprecated. Please use Container.sync")(
         model.Model.sync)
 
-    def update(self, wait=False):
-        """Update the container in lxd from local changes."""
-        try:
-            marshalled = self.marshall()
-        except AttributeError:
-            raise exceptions.ObjectIncomplete()
-        # These two properties are explicitly not allowed.
-        del marshalled['name']
-        del marshalled['status']
-
-        response = self.api.put(json=marshalled)
-
-        if wait:
-            Operation.wait_for_operation(
-                self.client, response.json()['operation'])
-
     def rename(self, name, wait=False):
         """Rename a container."""
         response = self.api.post(json={'name': name})
diff --git a/pylxd/image.py b/pylxd/image.py
index e4d02c3..795db3e 100644
--- a/pylxd/image.py
+++ b/pylxd/image.py
@@ -79,14 +79,6 @@ def create(cls, client, image_data, public=False, wait=False):
             Operation.wait_for_operation(client, response.json()['operation'])
         return cls(client, fingerprint=fingerprint)
 
-    def update(self):
-        """Update LXD based on changes to this image."""
-        try:
-            marshalled = self.marshall()
-        except AttributeError:
-            raise exceptions.ObjectIncomplete()
-        self.api.put(json=marshalled)
-
     def export(self):
         """Export the image."""
         try:
diff --git a/pylxd/model.py b/pylxd/model.py
index bcfdd43..99538bc 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -21,8 +21,9 @@
 class Attribute(object):
     """A metadata class for model attributes."""
 
-    def __init__(self, validator=None):
+    def __init__(self, validator=None, readonly=False):
         self.validator = validator
+        self.readonly = False
 
 
 class Manager(object):
@@ -147,14 +148,24 @@ def sync(self):
             setattr(self, key, val)
     fetch = deprecated("fetch is deprecated; please use sync")(sync)
 
-    def save(self):
+    def save(self, wait=False):
         """Save data to the server.
 
         This method should write the new data to the server via marshalling.
         It should be a no-op when the object is not dirty, to prevent needless
         I/O.
         """
-        raise NotImplementedError('save is not implemented')
+        try:
+            marshalled = self.marshall()
+        except AttributeError:
+            raise exceptions.ObjectIncomplete()
+
+        response = self.api.put(json=marshalled)
+
+        if wait:
+            Operation.wait_for_operation(
+                self.client, response.json()['operation'])
+    update = deprecated('update is deprecated; please use save')(save)
 
     def delete(self, wait=False):
         """Delete an object from the server."""
@@ -168,5 +179,6 @@ def marshall(self):
         """Marshall the object in preparation for updating to the server."""
         marshalled = {}
         for key, val in self.__attributes__.items():
-            marshalled[key] = getattr(self, key)
+            if not val.readonly:
+                marshalled[key] = getattr(self, key)
         return marshalled
diff --git a/pylxd/network.py b/pylxd/network.py
index 5c24165..5b2c5e9 100644
--- a/pylxd/network.py
+++ b/pylxd/network.py
@@ -43,6 +43,10 @@ def all(cls, client):
     def api(self):
         return self.client.api.networks[self.name]
 
+    def save(self, wait=False):
+        """Save is not available for networks."""
+        raise NotImplementedError('save is not implemented')
+
     def delete(self):
-        """Delete an object from the server."""
+        """Delete is not available for networks."""
         raise NotImplementedError('delete is not implemented')
diff --git a/pylxd/profile.py b/pylxd/profile.py
index fe68c9d..58c2106 100644
--- a/pylxd/profile.py
+++ b/pylxd/profile.py
@@ -17,7 +17,7 @@
 class Profile(model.Model):
     """A LXD profile."""
 
-    name = model.Attribute()
+    name = model.Attribute(readonly=True)
     description = model.Attribute()
     config = model.Attribute()
     devices = model.Attribute()
@@ -64,17 +64,6 @@ def create(cls, client, name, config=None, devices=None):
     def api(self):
         return self.client.api.profiles[self.name]
 
-    def update(self):
-        """Update the profile in LXD based on local changes."""
-        try:
-            marshalled = self.marshall()
-        except AttributeError:
-            raise exceptions.ObjectIncomplete()
-        # The name property cannot be updated.
-        del marshalled['name']
-
-        self.api.put(json=marshalled)
-
     def rename(self, new):
         """Rename the profile."""
         raise NotImplementedError(

From c58f42dcd3b17bd895c1b9c8d582e6944941cb89 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 13:06:54 -0600
Subject: [PATCH 7/9] Fix integration test issues

---
 pylxd/container.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pylxd/container.py b/pylxd/container.py
index 5b5ec07..130052f 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -48,6 +48,9 @@ class Container(model.Model):
     profiles = model.Attribute()
     status = model.Attribute(readonly=True)
 
+    status_code = model.Attribute()
+    stateful = model.Attribute()
+
     snapshots = model.Manager()
     files = model.Manager()
 

From 11155f4246f1bc4971c4b81b77d8ede7d3a60003 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 16:11:12 -0600
Subject: [PATCH 8/9] Fix unit and integration tests

---
 pylxd/container.py            |  4 ++--
 pylxd/model.py                |  6 +-----
 pylxd/tests/mock_lxd.py       | 41 +++++++++++++++++++++++++++++++++++++----
 pylxd/tests/test_container.py |  8 --------
 4 files changed, 40 insertions(+), 19 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 130052f..e66f0dc 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -48,8 +48,8 @@ class Container(model.Model):
     profiles = model.Attribute()
     status = model.Attribute(readonly=True)
 
-    status_code = model.Attribute()
-    stateful = model.Attribute()
+    status_code = model.Attribute(readonly=True)
+    stateful = model.Attribute(readonly=True)
 
     snapshots = model.Manager()
     files = model.Manager()
diff --git a/pylxd/model.py b/pylxd/model.py
index 99538bc..2d185be 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -155,11 +155,7 @@ def save(self, wait=False):
         It should be a no-op when the object is not dirty, to prevent needless
         I/O.
         """
-        try:
-            marshalled = self.marshall()
-        except AttributeError:
-            raise exceptions.ObjectIncomplete()
-
+        marshalled = self.marshall()
         response = self.api.put(json=marshalled)
 
         if wait:
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 8affaa1..e544aff 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -138,17 +138,50 @@ def profile_GET(request, context):
         'url': r'^http://pylxd.test/1.0/containers$',
     },
     {
-        'text': json.dumps({
+        'json': {
             'type': 'sync',
             'metadata': {
                 'name': 'an-container',
+
+                'architecture': "x86_64",
+                'config': {
+                    'security.privileged': "true",
+                },
+                'created_at': "1983-06-16T00:00:00-00:00",
+                'devices': {
+                    'root': {
+                        'path': "/",
+                        'type': "disk"
+                    }
+                },
                 'ephemeral': True,
-            }}),
+                'expanded_config': {
+                    'security.privileged': "true",
+                },
+                'expanded_devices': {
+                    'eth0': {
+                        'name': "eth0",
+                        'nictype': "bridged",
+                        'parent': "lxdbr0",
+                        'type': "nic"
+                    },
+                    'root': {
+                        'path': "/",
+                        'type': "disk"
+                    }
+                },
+                'profiles': [
+                    "default"
+                ],
+                'stateful': False,
+                'status': "Running",
+                'status_code': 103
+            }},
         'method': 'GET',
         'url': r'^http://pylxd.test/1.0/containers/an-container$',
     },
     {
-        'text': json.dumps({
+        'json': {
             'type': 'sync',
             'metadata': {
                 'status': 'Running',
@@ -176,7 +209,7 @@ def profile_GET(request, context):
                 },
                 'pid': 69,
                 'processes': 100,
-            }}),
+            }},
         'method': 'GET',
         'url': r'^http://pylxd.test/1.0/containers/an-container/state$',  # NOQA
     },
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index d1b2b86..167a1a3 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -156,14 +156,6 @@ 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(
             self.client, name='an-container')

From 91bb82661e49b83339b59abf53fc36b24ec2548b Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 16:49:21 -0600
Subject: [PATCH 9/9] Update deprecated API calls in tests

---
 pylxd/container.py              |  4 ++--
 pylxd/tests/test_certificate.py |  2 +-
 pylxd/tests/test_container.py   | 10 +++++-----
 pylxd/tests/test_image.py       | 10 +++++-----
 pylxd/tests/test_profile.py     |  8 ++++----
 5 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index e66f0dc..199c629 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -102,7 +102,7 @@ def all(cls, client):
 
         Containers returned from this method will only have the name
         set, as that is the only property returned from LXD. If more
-        information is needed, `Container.fetch` is the method call
+        information is needed, `Container.sync` is the method call
         that should be used.
         """
         response = client.api.containers.get()
@@ -155,7 +155,7 @@ def _set_state(self, state, timeout=30, force=True, wait=False):
         if wait:
             Operation.wait_for_operation(
                 self.client, response.json()['operation'])
-            self.fetch()
+            self.sync()
 
     def state(self):
         response = self.api.state.get()
diff --git a/pylxd/tests/test_certificate.py b/pylxd/tests/test_certificate.py
index e3a7f86..ef5a57f 100644
--- a/pylxd/tests/test_certificate.py
+++ b/pylxd/tests/test_certificate.py
@@ -48,7 +48,7 @@ def test_fetch(self):
         an_certificate = certificate.Certificate(
             self.client, fingerprint='an-certificate')
 
-        an_certificate.fetch()
+        an_certificate.sync()
 
         self.assertEqual('certificate-content', an_certificate.certificate)
 
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 167a1a3..e9c5968 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -92,11 +92,11 @@ def create_fail(request, context):
             container.Container.create, self.client, config)
 
     def test_fetch(self):
-        """A fetch updates the properties of a container."""
+        """A sync updates the properties of a container."""
         an_container = container.Container(
             self.client, name='an-container')
 
-        an_container.fetch()
+        an_container.sync()
 
         self.assertTrue(an_container.ephemeral)
 
@@ -117,7 +117,7 @@ def not_found(request, context):
         an_container = container.Container(
             self.client, name='an-missing-container')
 
-        self.assertRaises(exceptions.NotFound, an_container.fetch)
+        self.assertRaises(exceptions.NotFound, an_container.sync)
 
     def test_fetch_error(self):
         """LXDAPIException is raised on error."""
@@ -136,7 +136,7 @@ def not_found(request, context):
         an_container = container.Container(
             self.client, name='an-missing-container')
 
-        self.assertRaises(exceptions.LXDAPIException, an_container.fetch)
+        self.assertRaises(exceptions.LXDAPIException, an_container.sync)
 
     def test_update(self):
         """A container is updated."""
@@ -152,7 +152,7 @@ def test_update(self):
         an_container.profiles = 1
         an_container.status = 1
 
-        an_container.update(wait=True)
+        an_container.save(wait=True)
 
         self.assertTrue(an_container.ephemeral)
 
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
index bac6a5e..d889553 100644
--- a/pylxd/tests/test_image.py
+++ b/pylxd/tests/test_image.py
@@ -90,15 +90,15 @@ def create_fail(request, context):
     def test_update(self):
         """An image is updated."""
         a_image = self.client.images.all()[0]
-        a_image.fetch()
+        a_image.sync()
 
-        a_image.update()
+        a_image.save()
 
     def test_fetch(self):
         """A partial object is fetched and populated."""
         a_image = self.client.images.all()[0]
 
-        a_image.fetch()
+        a_image.sync()
 
         self.assertEqual(1, a_image.size)
 
@@ -119,7 +119,7 @@ def not_found(request, context):
 
         a_image = image.Image(self.client, fingerprint=fingerprint)
 
-        self.assertRaises(exceptions.NotFound, a_image.fetch)
+        self.assertRaises(exceptions.NotFound, a_image.sync)
 
     def test_fetch_error(self):
         """A 500 error raises LXDAPIException."""
@@ -138,7 +138,7 @@ def not_found(request, context):
 
         a_image = image.Image(self.client, fingerprint=fingerprint)
 
-        self.assertRaises(exceptions.LXDAPIException, a_image.fetch)
+        self.assertRaises(exceptions.LXDAPIException, a_image.sync)
 
     def test_delete(self):
         """An image is deleted."""
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
index 2d7b80a..1cb79f6 100644
--- a/pylxd/tests/test_profile.py
+++ b/pylxd/tests/test_profile.py
@@ -97,7 +97,7 @@ def test_update(self):
         # it's not clear how to assert that, just yet.
         an_profile = profile.Profile.get(self.client, 'an-profile')
 
-        an_profile.update()
+        an_profile.save()
 
         self.assertEqual({}, an_profile.config)
 
@@ -105,7 +105,7 @@ def test_fetch(self):
         """A partially fetched profile is made complete."""
         an_profile = self.client.profiles.all()[0]
 
-        an_profile.fetch()
+        an_profile.sync()
 
         self.assertEqual('An description', an_profile.description)
 
@@ -125,7 +125,7 @@ def not_found(request, context):
 
         an_profile = profile.Profile(self.client, name='an-profile')
 
-        self.assertRaises(exceptions.NotFound, an_profile.fetch)
+        self.assertRaises(exceptions.NotFound, an_profile.sync)
 
     def test_fetch_error(self):
         """LXDAPIException is raised on fetch error."""
@@ -143,7 +143,7 @@ def error(request, context):
 
         an_profile = profile.Profile(self.client, name='an-profile')
 
-        self.assertRaises(exceptions.LXDAPIException, an_profile.fetch)
+        self.assertRaises(exceptions.LXDAPIException, an_profile.sync)
 
     def test_delete(self):
         """A profile is deleted."""


More information about the lxc-devel mailing list