The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/115
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This patch adds a new api to `Container` that is similar to the `Client` `container`, `image`, and `profile` manager api. It also adds the use of these new apis to the old snapshot methods on `Container`, which should no longer be used.
From 05a3908734c1d32d711a2cc04f02ed669d376d6e Mon Sep 17 00:00:00 2001 From: Paul Hummer <p...@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 <p...@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
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel