[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