The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/132
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) === Add support for the events websocket. This was a little...special...in that we've mostly had this consistent API for working with a HTTP stateless api, and now we have what is essentially a socket client. I tried to document this as clearly as possible, but it's entirely possible that we'll have to iterate on this API a few times before it "feels right".
From b4f2f18cf928939a88693e31985bdc9a73e62231 Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@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.hum...@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.hum...@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."""
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel