Bug 2572: Docs and tutorial for WebSockets notifications 59/23159/6
authorJan Hajnar <jhajnar@cisco.com>
Tue, 16 Jun 2015 09:18:24 +0000 (11:18 +0200)
committerColin Dixon <colin@colindixon.com>
Wed, 1 Jul 2015 12:58:31 +0000 (12:58 +0000)
* added tutorial for websocket notifications subscription

Change-Id: I76313ee220e7aea7a71a4353c521a7966371c67c
Signed-off-by: Jan Hajnar <jhajnar@cisco.com>
(cherry picked from commit 50f1db5232c05866fc0f8397933edf9d0794d41a)

manuals/developer-guide/src/main/asciidoc/controller/controller.adoc
manuals/developer-guide/src/main/asciidoc/controller/websocket-notifications.adoc [new file with mode: 0644]

index 30155e94d41cd5e7ebc3e12034dfbf53573c3323..903c4c5a7340471476feb41f226f3bc1af5c20d6 100644 (file)
@@ -49,4 +49,6 @@ include::md-sal-rpc-routing.adoc[MD-SAL Rpc Routing]
 
 include::restconf.adoc[RESTCONF]
 
+include::websocket-notifications.adoc[Websocket Notifications]
+
 include::config.adoc[Config Subsystem]
diff --git a/manuals/developer-guide/src/main/asciidoc/controller/websocket-notifications.adoc b/manuals/developer-guide/src/main/asciidoc/controller/websocket-notifications.adoc
new file mode 100644 (file)
index 0000000..fb1a8ca
--- /dev/null
@@ -0,0 +1,273 @@
+=== Websocket change event notification subscription tutorial
+
+Subscribing to data change notifications makes it possible to obtain
+notifications about data manipulation (insert, change, delete) which are
+done on any specified *path* of any specified *datastore* with specific
+*scope*. In following examples _\{odlAddress}_ is address of server
+where ODL is running and _\{odlPort}_ is port on which OpenDaylight is
+running.
+
+==== Websocket notifications subscription process
+
+In this section we will learn what steps need to be taken in order to
+successfully subscribe to data change event notifications.
+
+===== Create stream
+
+In order to use event notifications you first need to call RPC that
+creates notification stream that you can later listen to. You need to
+provide three parameters to this RPC:
+
+* *path*: data store path that you plan to listen to. You can register
+  listener on containers, lists and leaves.
+* *datastore*: data store type. _OPERATIONAL_ or _CONFIGURATION_.
+* *scope*: Represents scope of data change. Possible options are:
+** BASE: only changes directly to the data tree node specified in the
+   path will be reported
+** ONE: changes to the node and to direct child nodes will be reported
+** SUBTREE: changes anywhere in the subtree starting at the node will
+   be reported
+
+The RPC to create the stream can be invoked via RESCONF like this:
+
+* URI:
+\http://\{odlAddress}:\{odlPort}/restconf/operations/sal-remote:create-data-change-event-subscription
+* HEADER: Content-Type=application/json
+* OPERATION: POST
+* DATA:
++
+[source,json]
+----
+{
+    "input": {
+        "path": "/toaster:toaster/toaster:toasterStatus",
+        "sal-remote-augment:datastore": "OPERATIONAL",
+        "sal-remote-augment:scope": "ONE"
+    }
+}
+----
+
+The response should look something like this:
+
+[source,json]
+----
+{
+    "output": {
+        "stream-name": "toaster:toaster/toaster:toasterStatus/datastore=CONFIGURATION/scope=SUBTREE"
+    }
+}
+----
+
+*stream-name* is important because you will need to use it when you
+subscribe to the stream in the next step.
+
+NOTE: Internally, this will create a new listener  for _stream-name_
+      if it did not already exist.
+
+===== Subscribe to stream
+
+In order to subscribe to stream and obtain WebSocket location you need
+to call _GET_ on your stream path. The URI should generally be
+\http://\{odlAddress}:\{odlPort}/restconf/streams/stream/\{streamName},
+where _\{streamName}_ is the _stream-name_ parameter contained in
+response from _create-data-change-event-subscription_ RPC from the
+previous step.
+
+* URI:
+\http://\{odlAddress}:\{odlPort}/restconf/streams/stream/toaster:toaster/datastore=CONFIGURATION/scope=SUBTREE
+* OPERATION: GET
+
+The expected response status is 200 OK and response body should be empty.
+You will get your WebSocket location from *Location* header of response.
+For example in our particular toaster example location header would have
+this value:
+_ws://\{odlAddress}:8185/toaster:toaster/datastore=CONFIGURATION/scope=SUBTREE_
+
+NOTE: During this phase there is an internal check for to see if a
+      listener for the _stream-name_ from the URI exists. If not, new
+      a new listener is registered with the DOM data broker.
+
+===== Receive notifications
+
+You should now have a data change notification stream created and have
+location of a WebSocket. You can use this WebSocket to listen to data
+change notifications. To listen to notifications you can use a
+JavaScript client or if you are using chrome browser you can use the
+https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo[Simple
+WebSocket Client].
+
+Also, for testing purposes, there is simple Java application named
+WebSocketClient. The application is placed in the
+_-sal-rest-connector-classes.class_ project. It accepts a WebSocket URI
+as and input parameter. After starting the utility (WebSocketClient
+class directly in Eclipse/InteliJ Idea) received notifications should be
+displayed in console.
+
+Notifications are always in XML format and look like this:
+
+[source,xml]
+----
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2014-09-11T09:58:23+02:00</eventTime>
+    <data-changed-notification xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote">
+        <data-change-event>
+            <path xmlns:meae="http://netconfcentral.org/ns/toaster">/meae:toaster</path>
+            <operation>updated</operation>
+        </data-change-event>
+    </data-changed-notification>
+</notification>
+----
+
+NOTE: Notifications never contain data. They only contain information
+      about the path that was changed and what kind of operation
+      executed on that path. If you need data you should execute a
+      RESTCONF GET request on the path that was changed.
+
+==== Example use case
+
+The typical use case is listening to data change events to update web
+page data in real-time. In this tutorial we will be using toaster as
+the base.
+// TODO: link to toaster tutorial?
+
+When you call _make-toast_ RPC, it sets _toasterStatus_ to "down" to
+reflect that the toaster is busy making toast. When it finishes,
+_toasterStatus_ is set to "up" again. We will listen to this toaster
+status changes in data store and will reflect it on our web page in
+real-time thanks to WebSocket data change notification.
+
+==== Simple javascript client implementation
+
+We will create simple JavaScript web application that will listen
+updates on _toasterStatus_ leaf and update some element of our web page
+according to new toaster status state.
+
+===== Create stream
+
+First you need to create stream that you are planing to subscribe to.
+This can be achieved by invoking "create-data-change-event-subscription"
+RPC on RESTCONF via AJAX request. You need to provide data store *path*
+that you plan to listen on, *data store type* and *scope*. If the
+request is successful you can extract the *stream-name* from the
+response and use that to subscribe to the newly created stream. The
+_\{username}_ and _\{password}_ fields represent your credentials that
+you use to connect to OpenDaylight via RESTCONF:
+
+NOTE: The default user name and password are "admin".
+
+[source,javascript]
+----
+function createStream() {
+    $.ajax(
+        {
+            url: 'http://{odlAddress}:{odlPort}/restconf/operations/sal-remote:create-data-change-event-subscription',
+            type: 'POST',
+            headers: {
+              'Authorization': 'Basic ' + btoa('{username}:{password}'),
+              'Content-Type': 'application/json'
+            },
+            data: JSON.stringify(
+                {
+                    'input': {
+                        'path': '/toaster:toaster/toaster:toasterStatus',
+                        'sal-remote-augment:datastore': 'OPERATIONAL',
+                        'sal-remote-augment:scope': 'ONE'
+                    }
+                }
+            )
+        }).done(function (data) {
+            // this function will be called when ajax call is executed successfully
+            subscribeToStream(data.output['stream-name']);
+        }).fail(function (data) {
+            // this function will be called when ajax call fails
+            console.log("Create stream call unsuccessful");
+        })
+}
+----
+
+===== Subscribe to stream
+
+The Next step is to subscribe to the stream. To subscribe to the stream
+you need to call _GET_ on
+_\http://\{odlAddress}:\{odlPort}/restconf/streams/stream/\{stream-name}_.
+If the call is successful, you get WebSocket address for this stream in
+*Location* parameter inside response header. You can get response header
+by calling _getResponseHeader('Location')_ on HttpRequest object inside
+_done()_ function call:
+
+[source,javascript]
+----
+function subscribeToStream(streamName) {
+    $.ajax(
+        {
+            url: 'http://{odlAddress}:{odlPort}/restconf/streams/stream/' + streamName;
+            type: 'GET',
+            headers: {
+              'Authorization': 'Basic ' + btoa('{username}:{password}'),
+            }
+        }
+    ).done(function (data, textStatus, httpReq) {
+        // we need function that has http request object parameter in order to access response headers.
+        listenToNotifications(httpReq.getResponseHeader('Location'));
+    }).fail(function (data) {
+        console.log("Subscribe to stream call unsuccessful");
+    });
+}
+----
+
+===== Receive notifications
+
+Once you got WebSocket server location you can now connect to it and
+start receiving data change events. You need to define functions that
+will handle events on WebSocket. In order to process incoming events
+from OpenDaylight you need to provide a function that will handle
+_onmessage_ events. The function must have one parameter that represents
+the received event object. The event data will be stored in _event.data_.
+The data will be in an XML format that you can then easily parse using
+jQuery.
+
+[source,javascript]
+----
+function listenToNotifications(socketLocation) {
+    try {
+        var notificatinSocket = new WebSocket(socketLocation);
+
+        notificatinSocket.onmessage = function (event) {
+            // we process our received event here
+            console.log('Received toaster data change event.');
+            $($.parseXML(event.data)).find('data-change-event').each(
+                function (index) {
+                    var operation = $(this).find('operation').text();
+                    if (operation == 'updated') {
+                        // toaster status was updated so we call function that gets the value of toasterStatus leaf
+                        updateToasterStatus();
+                        return false;
+                    }
+                }
+            );
+        }
+        notificatinSocket.onerror = function (error) {
+            console.log("Socket error: " + error);
+        }
+        notificatinSocket.onopen = function (event) {
+            console.log("Socket connection opened.");
+        }
+        notificatinSocket.onclose = function (event) {
+            console.log("Socket connection closed.");
+        }
+        // if there is a problem on socket creation we get exception (i.e. when socket address is incorrect)
+    } catch(e) {
+        alert("Error when creating WebSocket" + e );
+    }
+}
+----
+
+The _updateToasterStatus()_ function represents function that calls
+_GET_ on the path that was modified and sets toaster status in some web
+page element according to received data. After the WebSocket connection
+has been established you can test events by calling make-toast RPC via
+RESTCONF.
+
+NOTE: for more information about WebSockets in JavaScript visit
+https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_client_applications[Writing
+WebSocket client applications]