The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/121
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 branch adds a manager-esque `FilesManager` to handle files, with accompanying tests. Please review #120 first (I merged that into this branch; writing these tests were how I realized how loose the mock service was)
From e87f17fff48b5e47da490f5e511fe306ade2471b Mon Sep 17 00:00:00 2001 From: Paul Hummer <p...@eventuallyanyway.com> Date: Mon, 30 May 2016 19:52:23 -0600 Subject: [PATCH 1/2] Update the mock service to be more strict --- pylxd/tests/mock_lxd.py | 55 ++++++++++++++++++++++++------------------- pylxd/tests/test_container.py | 6 ++--- pylxd/tests/test_profile.py | 4 ++-- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py index 30ddb0c..5e2aea4 100644 --- a/pylxd/tests/mock_lxd.py +++ b/pylxd/tests/mock_lxd.py @@ -21,19 +21,21 @@ def profiles_POST(request, context): return json.dumps({'metadata': {}}) +def snapshot_DELETE(request, context): + context.status_code = 202 + return json.dumps({'operation': 'operation-abc'}) + + def profile_GET(request, context): name = request.path.split('/')[-1] - if name in ('an-profile', 'an-new-profile'): - return json.dumps({ - 'metadata': { - 'name': name, - 'description': 'An description', - 'config': {}, - 'devices': {}, - }, - }) - else: - context.status_code = 404 + return json.dumps({ + 'metadata': { + 'name': name, + 'description': 'An description', + 'config': {}, + 'devices': {}, + }, + }) RULES = [ @@ -63,7 +65,7 @@ def profile_GET(request, context): 'ephemeral': True, }}), 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'text': json.dumps({'metadata': { @@ -71,19 +73,19 @@ def profile_GET(request, context): 'status_code': 103, }}), 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)/state$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/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 + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { 'text': json.dumps({'operation': 'operation-abc'}), 'method': 'POST', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)/snapshots$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { 'text': json.dumps({'metadata': { @@ -91,32 +93,32 @@ def profile_GET(request, context): 'stateful': False, }}), 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { 'text': json.dumps({'operation': 'operation-abc'}), 'method': 'POST', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { - 'text': json.dumps({'operation': 'operation-abc'}), + 'text': snapshot_DELETE, 'method': 'DELETE', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { 'text': json.dumps({'operation': 'operation-abc'}), 'method': 'POST', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'text': json.dumps({'operation': 'operation-abc'}), 'method': 'PUT', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'text': container_DELETE, 'method': 'DELETE', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, # Images @@ -175,13 +177,18 @@ def profile_GET(request, context): { 'text': profile_GET, 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/profiles/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$', }, # Operations { 'text': '{"metadata": {"id": "operation-abc"}}', 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/operations/(?P<operation_id>.*)$', + 'url': r'^http://pylxd.test/1.0/operations/operation-abc$', + }, + { + 'text': '{"metadata": {}', + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/operations/operation-abc/wait$', }, ] diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py index ca324ae..785b67b 100644 --- a/pylxd/tests/test_container.py +++ b/pylxd/tests/test_container.py @@ -32,7 +32,7 @@ def not_found(request, context): self.add_rule({ 'text': not_found, 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) name = 'an-missing-container' @@ -89,7 +89,7 @@ def not_found(request, context): self.add_rule({ 'text': not_found, 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) an_container = container.Container( @@ -224,7 +224,7 @@ def not_found(request, context): self.add_rule({ 'text': not_found, 'method': 'DELETE', - 'url': r'^http://pylxd.test/1.0/containers/(?P<container>.*)/snapshots/(?P<snapshot>.*)$', # NOQA + 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }) snapshot = container.Snapshot( diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py index 72e0692..9abc487 100644 --- a/pylxd/tests/test_profile.py +++ b/pylxd/tests/test_profile.py @@ -25,7 +25,7 @@ def not_found(request, context): self.add_rule({ 'text': not_found, 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/profiles/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) self.assertRaises( @@ -99,7 +99,7 @@ def not_found(request, context): self.add_rule({ 'text': not_found, 'method': 'GET', - 'url': r'^http://pylxd.test/1.0/profiles/(?P<container_name>.*)$', + 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) an_profile = profile.Profile(name='an-profile', _client=self.client) From 9d191119e46f31e2b68d8facc6f81c84f7bfdf61 Mon Sep 17 00:00:00 2001 From: Paul Hummer <p...@eventuallyanyway.com> Date: Mon, 30 May 2016 21:14:59 -0600 Subject: [PATCH 2/2] Add a manager-esque FilesManager to namespace files. --- pylxd/container.py | 46 +++++++++++++++++++++++++++++++------------ pylxd/exceptions.py | 8 ++++++-- pylxd/tests/mock_lxd.py | 40 ++++++++++++++++++++++++++----------- pylxd/tests/test_container.py | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 26 deletions(-) diff --git a/pylxd/container.py b/pylxd/container.py index 66a751b..96785f9 100644 --- a/pylxd/container.py +++ b/pylxd/container.py @@ -32,6 +32,31 @@ class Container(mixin.Waitable, mixin.Marshallable): This class is not intended to be used directly, but rather to be used via `Client.containers.create`. """ + class FilesManager(object): + """A pseudo-manager for namespacing file operations.""" + + def __init__(self, client, container): + self._client = client + self._container = container + + def put(self, filepath, data): + response = self._client.api.containers[ + self._container.name].files.post( + params={'path': filepath}, data=data) + return response.status_code == 200 + + def get(self, filepath): + response = self._client.api.containers[ + self._container.name].files.get( + params={'path': filepath}) + if response.status_code == 500: + # XXX: rockstar (15 Feb 2016) - This should really + # return a 404. I blame LXD. + raise exceptions.NotFound({ + 'error': '{} not found in container {}'.format( + filepath, self._container.name + )}) + return response.content class Snapshots(object): def __init__(self, client, container): @@ -89,6 +114,7 @@ def __init__(self, **kwargs): setattr(self, key, value) self.snapshots = self.Snapshots(self._client, self) + self.files = self.FilesManager(self._client, self) def fetch(self): """Reload the container information.""" @@ -210,21 +236,15 @@ def delete_snapshot(self, name, wait=False): # pragma: no cover snapshot = self.snapshots.get(name) snapshot.delete() - def get_file(self, filepath): + @deprecated('Container.get_file is deprecated. Please use Container.files.get') # NOQA + def get_file(self, filepath): # pragma: no cover """Get a file from the container.""" - response = self._client.api.containers[self.name].files.get( - params={'path': filepath}) - if response.status_code == 500: - # XXX: rockstar (15 Feb 2016) - This should really return a 404. - # I blame LXD. :) - raise IOError('Error reading "{}"'.format(filepath)) - return response.content - - def put_file(self, filepath, data): + return self.files.get(filepath) + + @deprecated('Container.put_file is deprecated. Please use Container.files.put') # NOQA + def put_file(self, filepath, data): # pragma: no cover """Put a file on the container.""" - response = self._client.api.containers[self.name].files.post( - params={'path': filepath}, data=data) - return response.status_code == 200 + return self.files.put(filepath, data) def execute(self, commands, environment={}): """Execute a command on the container.""" diff --git a/pylxd/exceptions.py b/pylxd/exceptions.py index e910cbc..9e243dd 100644 --- a/pylxd/exceptions.py +++ b/pylxd/exceptions.py @@ -4,7 +4,9 @@ class ClientConnectionFailed(Exception): class ClientAuthenticationFailed(Exception): """The LXD client's certificates are not trusted.""" - message = "LXD client certificates are not trusted.""" + + def __str__(self): + return "LXD client certificates are not trusted.""" class _LXDAPIException(Exception): @@ -18,7 +20,9 @@ class _LXDAPIException(Exception): def __init__(self, data): self.data = data - self.message = self.data.get('error') + + def __str__(self): + return self.data.get('error') class NotFound(_LXDAPIException): diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py index 5e2aea4..5150503 100644 --- a/pylxd/tests/mock_lxd.py +++ b/pylxd/tests/mock_lxd.py @@ -46,6 +46,7 @@ def profile_GET(request, context): 'url': r'^http://pylxd.test/1.0$', }, + # Containers { 'text': json.dumps({'metadata': [ @@ -76,6 +77,24 @@ def profile_GET(request, context): 'url': r'^http://pylxd.test/1.0/containers/an-container/state$', # NOQA }, { + 'text': json.dumps({'operation': 'operation-abc'}), + 'method': 'POST', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', + }, + { + 'text': json.dumps({'operation': 'operation-abc'}), + 'method': 'PUT', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', + }, + { + 'text': container_DELETE, + 'method': 'DELETE', + 'url': r'^http://pylxd.test/1.0/containers/an-container$', + }, + + + # Container Snapshots + { 'text': json.dumps({'metadata': [ '/1.0/containers/an_container/snapshots/an-snapshot', ]}), @@ -105,22 +124,21 @@ def profile_GET(request, context): 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, + + + # Container files { - 'text': json.dumps({'operation': 'operation-abc'}), - 'method': 'POST', - 'url': r'^http://pylxd.test/1.0/containers/an-container$', - }, - { - 'text': json.dumps({'operation': 'operation-abc'}), - 'method': 'PUT', - 'url': r'^http://pylxd.test/1.0/containers/an-container$', + 'text': 'This is a getted file', + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fgetted$', # NOQA }, { - 'text': container_DELETE, - 'method': 'DELETE', - 'url': r'^http://pylxd.test/1.0/containers/an-container$', + 'method': 'POST', + 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fputted$', # NOQA }, + + # Images { 'text': json.dumps({'metadata': [ diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py index 785b67b..932a38e 100644 --- a/pylxd/tests/test_container.py +++ b/pylxd/tests/test_container.py @@ -232,3 +232,39 @@ def not_found(request, context): name='an-snapshot') self.assertRaises(RuntimeError, snapshot.delete) + + +class TestFiles(testing.PyLXDTestCase): + """Tests for pylxd.container.Container.files.""" + + def setUp(self): + super(TestFiles, self).setUp() + self.container = container.Container.get(self.client, 'an-container') + + def test_put(self): + """A file is put on the container.""" + data = 'The quick brown fox' + + self.container.files.put('/tmp/putted', data) + + # TODO: Add an assertion here + + def test_get(self): + """A file is retrieved from the container.""" + data = self.container.files.get('/tmp/getted') + + self.assertEqual(b'This is a getted file', data) + + def test_get_not_found(self): + """NotFound is raised on bogus filenames.""" + def not_found(request, context): + context.status_code = 500 + rule = { + 'text': not_found, + 'method': 'GET', + 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fgetted$', # NOQA + } + self.add_rule(rule) + + self.assertRaises( + exceptions.NotFound, self.container.files.get, '/tmp/getted')
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel