[lxc-devel] [pylxd/master] Snapshot manager

rockstar on Github lxc-bot at linuxcontainers.org
Mon May 30 02:22:02 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 538 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160530/c5c1c78e/attachment.bin>
-------------- next part --------------
From 05a3908734c1d32d711a2cc04f02ed669d376d6e Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sun, 29 May 2016 20:13:41 -0600
Subject: [PATCH 1/2] Add Container.snapshots manager API

This should replace the old snapshots API that was rather
awkward to work with. It also sets in a common API pattern
for nesting objects.
---
 pylxd/container.py            | 102 ++++++++++++++++++++++++++++++++++--------
 pylxd/tests/mock_lxd.py       |  30 +++++++++++++
 pylxd/tests/test_container.py |  78 ++++++++++++++++++++++++++++++++
 3 files changed, 191 insertions(+), 19 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index f330401..8aadcca 100644
--- a/pylxd/container.py
+++ b/pylxd/container.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 functools
 
 import six
 
@@ -31,6 +32,12 @@ class Container(mixin.Waitable, mixin.Marshallable):
     via `Client.containers.create`.
     """
 
+    class Snapshots(object):
+        def __init__(self, client, container):
+            self.get = functools.partial(Snapshot.get, client, container)
+            self.all = functools.partial(Snapshot.all, client, container)
+            self.create = functools.partial(Snapshot.create, client, container)
+
     __slots__ = [
         '_client',
         'architecture', 'config', 'created_at', 'devices', 'ephemeral',
@@ -80,6 +87,8 @@ def __init__(self, **kwargs):
         for key, value in six.iteritems(kwargs):
             setattr(self, key, value)
 
+        self.snapshots = self.Snapshots(self._client, self)
+
     def fetch(self):
         """Reload the container information."""
         response = self._client.api.containers[self.name].get()
@@ -176,32 +185,25 @@ def unfreeze(self, timeout=30, force=True, wait=False):
                                force=force,
                                wait=wait)
 
-    def snapshot(self, name, stateful=False, wait=False):
+    # The next four methods are left for backwards compatibility,
+    # but are deprecated for the snapshots manager API.
+    def snapshot(self, name, stateful=False, wait=False):  # pragma: no cover
         """Take a snapshot of the container."""
-        response = self._client.api.containers[self.name].snapshots.post(json={
-            'name': name, 'stateful': stateful})
-        if wait:
-            self.wait_for_operation(response.json()['operation'])
+        self.snapshots.create(name, stateful, wait)
 
-    def list_snapshots(self):
+    def list_snapshots(self):  # pragma: no cover
         """List all container snapshots."""
-        response = self._client.api.containers[self.name].snapshots.get()
-        return [snapshot.split('/')[-1]
-                for snapshot in response.json()['metadata']]
+        return [s.name for s in self.snapshots.all()]
 
-    def rename_snapshot(self, old, new, wait=False):
+    def rename_snapshot(self, old, new, wait=False):  # pragma: no cover
         """Rename a snapshot."""
-        response = self._client.api.containers[
-            self.name].snapshots[old].post(json={'name': new})
-        if wait:
-            self.wait_for_operation(response.json()['operation'])
+        snapshot = self.snapshots.get(old)
+        snapshot.rename(new, wait=wait)
 
-    def delete_snapshot(self, name, wait=False):
+    def delete_snapshot(self, name, wait=False):  # pragma: no cover
         """Delete a snapshot."""
-        response = self._client.api.containers[
-            self.name].snapshots[name].delete()
-        if wait:
-            self.wait_for_operation(response.json()['operation'])
+        snapshot = self.snapshots.get(name)
+        snapshot.delete()
 
     def get_file(self, filepath):
         """Get a file from the container."""
@@ -234,3 +236,65 @@ def execute(self, commands, environment={}):
             })
         operation_id = response.json()['operation']
         self.wait_for_operation(operation_id)
+
+
+class Snapshot(mixin.Waitable, mixin.Marshallable):
+    """A container snapshot."""
+
+    @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())
+
+        snapshot = cls(
+            _client=client, _container=container,
+            **response.json()['metadata'])
+        # Snapshot names are namespaced in LXD, as container-name/snapshot-name.
+        # We hide that implementation detail.
+        snapshot.name = snapshot.name.split('/')[-1]
+        return snapshot
+
+    @classmethod
+    def all(cls, client, container):
+        response = client.api.containers[container.name].snapshots.get()
+
+        return [cls(
+                name=snapshot.split('/')[-1], _client=client,
+                _container=container)
+                for snapshot in response.json()['metadata']]
+
+    @classmethod
+    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)
+        if wait:
+            snapshot.wait_for_operation(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})
+        if wait:
+            self.wait_for_operation(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 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/mock_lxd.py b/pylxd/tests/mock_lxd.py
index e4f1600..30ddb0c 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -74,6 +74,36 @@ def profile_GET(request, context):
         'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)/state$',  # NOQA
     },
     {
+        'text': json.dumps({'metadata': [
+            '/1.0/containers/an_container/snapshots/an-snapshot',
+        ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)/snapshots$',  # NOQA
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)/snapshots$',  # NOQA
+    },
+    {
+        'text': json.dumps({'metadata': {
+            'name': 'an_container/an-snapshot',
+            'stateful': False,
+        }}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$',  # NOQA
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$',  # NOQA
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'DELETE',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$',  # NOQA
+    },
+    {
         'text': json.dumps({'operation': 'operation-abc'}),
         'method': 'POST',
         'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$',
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index b6620ba..05a094f 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -154,3 +154,81 @@ def test_get(self):
 
         self.assertEqual('Running', state.status)
         self.assertEqual(103, state.status_code)
+
+
+class TestContainerSnapshots(testing.PyLXDTestCase):
+    """Tests for pylxd.container.Container.snapshots."""
+
+    def setUp(self):
+        super(TestContainerSnapshots, self).setUp()
+        self.container = container.Container.get(self.client, 'an-container')
+
+    def test_get(self):
+        """Return a specific snapshot."""
+        snapshot = self.container.snapshots.get('an-snapshot')
+
+        self.assertEqual('an-snapshot', snapshot.name)
+
+    def test_all(self):
+        """Return all snapshots."""
+        snapshots = self.container.snapshots.all()
+
+        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)
+
+    def test_create(self):
+        """Create a snapshot."""
+        snapshot = self.container.snapshots.create(
+            'an-snapshot', stateful=True, wait=True)
+
+        self.assertEqual('an-snapshot', snapshot.name)
+
+
+class TestSnapshot(testing.PyLXDTestCase):
+    """Tests for pylxd.container.Snapshot."""
+
+    def setUp(self):
+        super(TestSnapshot, self).setUp()
+        self.container = container.Container.get(self.client, 'an-container')
+
+    def test_rename(self):
+        """A snapshot is renamed."""
+        snapshot = container.Snapshot(
+            _client=self.client, _container=self.container,
+            name='an-snapshot')
+
+        snapshot.rename('an-renamed-snapshot', wait=True)
+
+        self.assertEqual('an-renamed-snapshot', snapshot.name)
+
+    def test_delete(self):
+        """A snapshot is deleted."""
+        snapshot = container.Snapshot(
+            _client=self.client, _container=self.container,
+            name='an-snapshot')
+
+        snapshot.delete(wait=True)
+
+        # TODO: add an assertion here
+
+    def test_delete_failure(self):
+        """If the response indicates delete failure, raise RuntimeError."""
+        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': 'DELETE',
+            'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$',  # NOQA
+        })
+
+        snapshot = container.Snapshot(
+            _client=self.client, _container=self.container,
+            name='an-snapshot')
+
+        self.assertRaises(RuntimeError, snapshot.delete)

From f22bd2448a6ee43c4df39335757f49ace8b8990c Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sun, 29 May 2016 20:19:02 -0600
Subject: [PATCH 2/2] Fix pep8

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

diff --git a/pylxd/container.py b/pylxd/container.py
index 8aadcca..1b89fe5 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -251,8 +251,9 @@ def get(cls, client, container, name):
         snapshot = cls(
             _client=client, _container=container,
             **response.json()['metadata'])
-        # Snapshot names are namespaced in LXD, as container-name/snapshot-name.
-        # We hide that implementation detail.
+        # Snapshot names are namespaced in LXD, as
+        # container-name/snapshot-name. We hide that implementation
+        # detail.
         snapshot.name = snapshot.name.split('/')[-1]
         return snapshot
 


More information about the lxc-devel mailing list