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

rockstar on Github lxc-bot at linuxcontainers.org
Tue May 31 03:29:35 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 524 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160531/d5316358/attachment.bin>
-------------- next part --------------
From e87f17fff48b5e47da490f5e511fe306ade2471b Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at 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 <paul at 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')


More information about the lxc-devel mailing list