[lxc-devel] [pylxd/master] Add support for container and snapshot publish.

rockstar on Github lxc-bot at linuxcontainers.org
Wed Jul 13 03:30:33 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 434 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160713/9afc1932/attachment.bin>
-------------- next part --------------
From 592646263f05d1a2bfe70ced05eae08d4e74930e Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 12 Jul 2016 21:29:37 -0600
Subject: [PATCH] Add support for container and snapshot publish.

Snapshot publish is broken in LXD, but this is a guess for how
it is supposed to work, based on how it works in container
publish.
---
 pylxd/container.py            | 49 +++++++++++++++++++++++++++++++++++++++++
 pylxd/exceptions.py           |  3 +++
 pylxd/operation.py            | 15 +++++++++----
 pylxd/tests/test_container.py | 51 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 114 insertions(+), 4 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 3f49b32..7495572 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -289,6 +289,30 @@ def migrate(self, new_client, wait=False):
         }
         return new_client.containers.create(config, wait=wait)
 
+    def publish(self, public=False, wait=False):
+        """Publish a container as an image.
+
+        The container must be stopped in order publish it as an image. This
+        method does not enforce that constraint, so a LXDAPIException may be
+        raised if this method is called on a running container.
+
+        If wait=True, an Image is returned.
+        """
+        data = {
+            'public': public,
+            'source': {
+                'type': 'container',
+                'name': self.name,
+            }
+        }
+
+        response = self.client.api.images.post(json=data)
+        if wait:
+            operation = Operation.wait_for_operation(
+                self.client, response.json()['operation'])
+
+            return self.client.images.get(operation.metadata['fingerprint'])
+
 
 class _CommandWebsocketClient(WebSocketBaseClient):  # pragma: no cover
     def __init__(self, manager, *args, **kwargs):
@@ -374,3 +398,28 @@ def rename(self, new_name, wait=False):
             Operation.wait_for_operation(
                 self.client, response.json()['operation'])
         self.name = new_name
+
+    def publish(self, public=False, wait=False):
+        """Publish a snapshot as an image.
+
+        If wait=True, an Image is returned.
+
+        This functionality is currently broken in LXD. Please see
+        https://github.com/lxc/lxd/issues/2201 - The implementation
+        here is mostly a guess. Once that bug is fixed, we can verify
+        that this works, or file a bug to fix it appropriately.
+        """
+        data = {
+            'public': public,
+            'source': {
+                'type': 'snapshot',
+                'name': '{}/{}'.format(self.container.name, self.name),
+                'alias': 'lololol',
+            }
+        }
+
+        response = self.client.api.images.post(json=data)
+        if wait:
+            operation = Operation.wait_for_operation(
+                self.client, response.json()['operation'])
+            return self.client.images.get(operation.metadata['fingerprint'])
diff --git a/pylxd/exceptions.py b/pylxd/exceptions.py
index 36d8086..d5b6b5e 100644
--- a/pylxd/exceptions.py
+++ b/pylxd/exceptions.py
@@ -15,6 +15,9 @@ def __init__(self, response):
         self.response = response
 
     def __str__(self):
+        if self.response.status_code == 200:  # Operation failure
+            return self.response.json()['metadata']['err']
+
         try:
             data = self.response.json()
             return data['error']
diff --git a/pylxd/operation.py b/pylxd/operation.py
index 09c9d24..1594680 100644
--- a/pylxd/operation.py
+++ b/pylxd/operation.py
@@ -11,7 +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 six
+from pylxd import exceptions
 
 
 class Operation(object):
@@ -27,7 +27,7 @@ def wait_for_operation(cls, client, operation_id):
         """Get an operation and wait for it to complete."""
         operation = cls.get(client, operation_id)
         operation.wait()
-        return operation
+        return cls.get(client, operation.id)
 
     @classmethod
     def get(cls, client, operation_id):
@@ -39,9 +39,16 @@ def get(cls, client, operation_id):
 
     def __init__(self, **kwargs):
         super(Operation, self).__init__()
-        for key, value in six.iteritems(kwargs):
+        for key, value in kwargs.items():
             setattr(self, key, value)
 
     def wait(self):
         """Wait for the operation to complete and return."""
-        self._client.api.operations[self.id].wait.get()
+        response = self._client.api.operations[self.id].wait.get()
+
+        try:
+            if response.json()['metadata']['status'] == 'Failure':
+                raise exceptions.LXDAPIException(response)
+        except KeyError:
+            # Support for legacy LXD
+            pass
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index d483cba..0178cb7 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -191,6 +191,31 @@ def test_migrate(self):
         self.assertEqual('an-container', an_migrated_container.name)
         self.assertEqual(client2, an_migrated_container.client)
 
+    def test_publish(self):
+        """Containers can be published."""
+        self.add_rule({
+            'text': json.dumps({
+                'type': 'sync',
+                'metadata': {
+                    'id': 'operation-abc',
+                    'metadata': {
+                        'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'  # NOQA
+                        }
+                    }
+                }),
+            'method': 'GET',
+            'url': r'^http://pylxd.test/1.0/operations/operation-abc$',
+        })
+
+        an_container = container.Container(
+            self.client, name='an-container')
+
+        image = an_container.publish(wait=True)
+
+        self.assertEqual(
+            'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
+            image.fingerprint)
+
 
 class TestContainerState(testing.PyLXDTestCase):
     """Tests for pylxd.container.ContainerState."""
@@ -313,6 +338,32 @@ def not_found(request, context):
 
         self.assertRaises(exceptions.LXDAPIException, snapshot.delete)
 
+    def test_publish(self):
+        """Snapshots can be published."""
+        self.add_rule({
+            'text': json.dumps({
+                'type': 'sync',
+                'metadata': {
+                    'id': 'operation-abc',
+                    'metadata': {
+                        'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'  # NOQA
+                        }
+                    }
+                }),
+            'method': 'GET',
+            'url': r'^http://pylxd.test/1.0/operations/operation-abc$',
+        })
+
+        snapshot = container.Snapshot(
+            self.client, container=self.container,
+            name='an-snapshot')
+
+        image = snapshot.publish(wait=True)
+
+        self.assertEqual(
+            'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
+            image.fingerprint)
+
 
 class TestFiles(testing.PyLXDTestCase):
     """Tests for pylxd.container.Container.files."""


More information about the lxc-devel mailing list