[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