[lxc-devel] [pylxd/master] Events websocket

rockstar on Github lxc-bot at linuxcontainers.org
Sun Jun 5 04:47:46 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 661 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160605/fa9371b7/attachment.bin>
-------------- next part --------------
From b4f2f18cf928939a88693e31985bdc9a73e62231 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sat, 4 Jun 2016 22:28:19 -0600
Subject: [PATCH 1/3] Add websocket events support

---
 pylxd/client.py            | 57 +++++++++++++++++++++++++++++++++++++++++-----
 pylxd/tests/test_client.py | 23 +++++++++++++++++++
 2 files changed, 74 insertions(+), 6 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index f25755c..0c3727e 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -11,15 +11,13 @@
 #    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 json
 import os
 
-try:  # pragma: no cover
-    from urllib.parse import quote
-except ImportError:  # pragma: no cover
-    from urllib import quote
-
 import requests
 import requests_unixsocket
+from six.moves.urllib import parse
+from ws4py.client import WebSocketBaseClient
 
 from pylxd import exceptions, managers
 
@@ -109,6 +107,22 @@ def delete(self, *args, **kwargs):
         return response
 
 
+class _WebsocketClient(WebSocketBaseClient):
+    """A basic websocket client for the LXD API.
+
+    This client is intentionally barebones, and serves
+    as a simple default. It simply connects and saves
+    all json messages to a messages attribute, which can
+    then be read are parsed.
+    """
+    def handshake_ok(self):
+        self.messages = []
+
+    def received_message(self, message):
+        json_message = json.loads(message.data.decode('utf-8'))
+        self.messages.append(json_message)
+
+
 class Client(object):
     """
     Client class for LXD REST API.
@@ -161,7 +175,7 @@ def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
             else:
                 path = '/var/lib/lxd/unix.socket'
             self.api = _APINode('http+unix://{}'.format(
-                quote(path, safe='')))
+                parse.quote(path, safe='')))
         self.api = self.api[version]
 
         # Verify the connection is valid.
@@ -182,3 +196,34 @@ def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
         self.images = managers.ImageManager(self)
         self.operations = managers.OperationManager(self)
         self.profiles = managers.ProfileManager(self)
+
+    def events(self, websocket_client=None):
+        """Get a websocket client for getting events.
+
+        /events is a websocket url, and so must be handled differently than
+        most other LXD API endpoints. This method returns
+        a client that can be interacted with like any
+        regular python socket.
+
+        An optional `websocket_client` parameter can be
+        specified for implementation-specific handling
+        of events as they occur.
+        """
+        if websocket_client is None:
+            websocket_client = _WebsocketClient
+
+        parsed = parse.urlparse(self.api.events._api_endpoint)
+        if parsed.scheme == 'http+unix':
+            scheme = 'ws+unix'
+            host = parse.unquote(parsed.netloc)
+        elif parsed.scheme in ('http', 'https'):
+            host = parsed.netloc
+            if parsed.scheme == 'http':
+                scheme = 'ws'
+            elif parsed.scheme == 'https':
+                scheme = 'wss'
+        url = parse.urlunparse((scheme, host, '', '', '', ''))
+        client = websocket_client(url)
+        client.resource = parsed.path
+
+        return client
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index bf74bff..5cf0e3e 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -1,3 +1,4 @@
+import json
 import os
 import unittest
 
@@ -241,3 +242,25 @@ def test_delete(self, Session):
         node.delete()
 
         session.delete.assert_called_once_with('http://test.com')
+
+
+class TestWebsocketClient(unittest.TestCase):
+    """Tests for pylxd.client.WebsocketClient."""
+
+    def test_handshake_ok(self):
+        """A `message` attribute of an empty list is created."""
+        ws_client = client._WebsocketClient('ws://an/fake/path')
+
+        ws_client.handshake_ok()
+
+        self.assertEqual([], ws_client.messages)
+
+    def test_received_message(self):
+        """A json dict is added to the messages attribute."""
+        message = mock.Mock(data=json.dumps({'test': 'data'}).encode('utf-8'))
+        ws_client = client._WebsocketClient('ws://an/fake/path')
+        ws_client.handshake_ok()
+
+        ws_client.received_message(message)
+
+        self.assertEqual({'test': 'data'}, ws_client.messages[0])

From 029b8d15778f9d1d9ada2d8a6a54a6d2f930286d Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sat, 4 Jun 2016 22:28:29 -0600
Subject: [PATCH 2/3] Add documentation for websocket events support

---
 doc/source/usage.rst | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 3d8e106..8fa80e6 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -197,3 +197,24 @@ and `devices` config dictionaries.
     >>> profile = client.profiles.create(
     ...     'an-profile', config={'security.nesting': 'true'},
     ...     devices={'root': {'path': '/', 'size': '10GB', 'type': 'disk'}})
+
+
+Events
+======
+
+LXD provides an `/events` endpoint that is upgraded to a streaming websocket
+for getting LXD events in real-time. The :class:`~pylxd.Client`'s `events`
+method will return a websocket client that can interact with the
+web socket messages.
+
+.. code-block:: python
+
+    >>> ws_client = client.events()
+    >>> ws_client.connect()
+    >>> ws_client.run()
+
+A default client class is provided, which will block indefinitely, and
+collect all json messages in a `messages` attribute. An optional 
+`websocket_client` parameter can be provided when more functionality is
+needed. The `ws4py` library is used to establish the connection; please
+see the `ws4py` documentation for more information.

From 2ea0f180b5a8c4ad921702080c4469bf49d94c77 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sat, 4 Jun 2016 22:47:18 -0600
Subject: [PATCH 3/3] Add tests for websocket events

---
 pylxd/client.py            | 10 +++++-----
 pylxd/tests/test_client.py | 41 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 5 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index 0c3727e..8f95a52 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -213,15 +213,15 @@ def events(self, websocket_client=None):
             websocket_client = _WebsocketClient
 
         parsed = parse.urlparse(self.api.events._api_endpoint)
-        if parsed.scheme == 'http+unix':
-            scheme = 'ws+unix'
-            host = parse.unquote(parsed.netloc)
-        elif parsed.scheme in ('http', 'https'):
+        if parsed.scheme in ('http', 'https'):
             host = parsed.netloc
             if parsed.scheme == 'http':
                 scheme = 'ws'
-            elif parsed.scheme == 'https':
+            else:
                 scheme = 'wss'
+        else:
+            scheme = 'ws+unix'
+            host = parse.unquote(parsed.netloc)
         url = parse.urlunparse((scheme, host, '', '', '', ''))
         client = websocket_client(url)
         client.resource = parsed.path
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index 5cf0e3e..f4e4294 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -81,6 +81,47 @@ def test_host_info(self):
         an_client = client.Client()
         self.assertEqual('zfs', an_client.host_info['environment']['storage'])
 
+    def test_events(self):
+        """The default websocket client is returned."""
+        an_client = client.Client()
+
+        ws_client = an_client.events()
+
+        self.assertEqual('/1.0/events', ws_client.resource)
+
+    def test_events_unix_socket(self):
+        """A unix socket compatible websocket client is returned."""
+        websocket_client = mock.Mock(resource=None)
+        WebsocketClient = mock.Mock()
+        WebsocketClient.return_value = websocket_client
+        an_client = client.Client()
+
+        an_client.events(websocket_client=WebsocketClient)
+
+        WebsocketClient.assert_called_once_with('ws+unix:///lxd/unix.socket')
+
+    def test_events_htt(self):
+        """An http compatible websocket client is returned."""
+        websocket_client = mock.Mock(resource=None)
+        WebsocketClient = mock.Mock()
+        WebsocketClient.return_value = websocket_client
+        an_client = client.Client('http://lxd.local')
+
+        an_client.events(websocket_client=WebsocketClient)
+
+        WebsocketClient.assert_called_once_with('ws://lxd.local')
+
+    def test_events_https(self):
+        """An https compatible websocket client is returned."""
+        websocket_client = mock.Mock(resource=None)
+        WebsocketClient = mock.Mock()
+        WebsocketClient.return_value = websocket_client
+        an_client = client.Client('https://lxd.local')
+
+        an_client.events(websocket_client=WebsocketClient)
+
+        WebsocketClient.assert_called_once_with('wss://lxd.local')
+
 
 class TestAPINode(unittest.TestCase):
     """Tests for pylxd.client._APINode."""


More information about the lxc-devel mailing list