1"""
2A Websockets add-on to saltnado
3===============================
4
5.. py:currentmodule:: salt.netapi.rest_tornado.saltnado
6
7:depends:   - tornado Python module
8
9In order to enable saltnado_websockets you must add websockets: True to your
10saltnado config block.
11
12.. code-block:: yaml
13
14    rest_tornado:
15        # can be any port
16        port: 8000
17        ssl_crt: /etc/pki/api/certs/server.crt
18        # no need to specify ssl_key if cert and key
19        # are in one single file
20        ssl_key: /etc/pki/api/certs/server.key
21        debug: False
22        disable_ssl: False
23        websockets: True
24
25All Events
26----------
27
28Exposes ``all`` "real-time" events from Salt's event bus on a websocket connection.
29It should be noted that "Real-time" here means these events are made available
30to the server as soon as any salt related action (changes to minions, new jobs etc) happens.
31Clients are however assumed to be able to tolerate any network transport related latencies.
32Functionality provided by this endpoint is similar to the ``/events`` end point.
33
34The event bus on the Salt master exposes a large variety of things, notably
35when executions are started on the master and also when minions ultimately
36return their results. This URL provides a real-time window into a running
37Salt infrastructure. Uses websocket as the transport mechanism.
38
39Exposes GET method to return websocket connections.
40All requests should include an auth token.
41A way to obtain obtain authentication tokens is shown below.
42
43.. code-block:: bash
44
45    % curl -si localhost:8000/login \\
46        -H "Accept: application/json" \\
47        -d username='salt' \\
48        -d password='salt' \\
49        -d eauth='pam'
50
51Which results in the response
52
53.. code-block:: json
54
55    {
56        "return": [{
57            "perms": [".*", "@runner", "@wheel"],
58            "start": 1400556492.277421,
59            "token": "d0ce6c1a37e99dcc0374392f272fe19c0090cca7",
60            "expire": 1400599692.277422,
61            "user": "salt",
62            "eauth": "pam"
63        }]
64    }
65
66In this example the ``token`` returned is ``d0ce6c1a37e99dcc0374392f272fe19c0090cca7`` and can be included
67in subsequent websocket requests (as part of the URL).
68
69The event stream can be easily consumed via JavaScript:
70
71.. code-block:: javascript
72
73    // Note, you must be authenticated!
74
75    // Get the Websocket connection to Salt
76    var source = new Websocket('wss://localhost:8000/all_events/d0ce6c1a37e99dcc0374392f272fe19c0090cca7');
77
78    // Get Salt's "real time" event stream.
79    source.onopen = function() { source.send('websocket client ready'); };
80
81    // Other handlers
82    source.onerror = function(e) { console.debug('error!', e); };
83
84    // e.data represents Salt's "real time" event data as serialized JSON.
85    source.onmessage = function(e) { console.debug(e.data); };
86
87    // Terminates websocket connection and Salt's "real time" event stream on the server.
88    source.close();
89
90Or via Python, using the Python module
91`websocket-client <https://pypi.python.org/pypi/websocket-client/>`_ for example.
92Or the tornado
93`client <https://tornado.readthedocs.io/en/latest/websocket.html#client-side-support>`_.
94
95.. code-block:: python
96
97    # Note, you must be authenticated!
98
99    from websocket import create_connection
100
101    # Get the Websocket connection to Salt
102    ws = create_connection('wss://localhost:8000/all_events/d0ce6c1a37e99dcc0374392f272fe19c0090cca7')
103
104    # Get Salt's "real time" event stream.
105    ws.send('websocket client ready')
106
107
108    # Simple listener to print results of Salt's "real time" event stream.
109    # Look at https://pypi.python.org/pypi/websocket-client/ for more examples.
110    while listening_to_events:
111        print ws.recv()       #  Salt's "real time" event data as serialized JSON.
112
113    # Terminates websocket connection and Salt's "real time" event stream on the server.
114    ws.close()
115
116    # Please refer to https://github.com/liris/websocket-client/issues/81 when using a self signed cert
117
118Above examples show how to establish a websocket connection to Salt and activating
119real time updates from Salt's event stream by signaling ``websocket client ready``.
120
121
122Formatted Events
123-----------------
124
125Exposes ``formatted`` "real-time" events from Salt's event bus on a websocket connection.
126It should be noted that "Real-time" here means these events are made available
127to the server as soon as any salt related action (changes to minions, new jobs etc) happens.
128Clients are however assumed to be able to tolerate any network transport related latencies.
129Functionality provided by this endpoint is similar to the ``/events`` end point.
130
131The event bus on the Salt master exposes a large variety of things, notably
132when executions are started on the master and also when minions ultimately
133return their results. This URL provides a real-time window into a running
134Salt infrastructure. Uses websocket as the transport mechanism.
135
136Formatted events parses the raw "real time" event stream and maintains
137a current view of the following:
138
139- minions
140- jobs
141
142A change to the minions (such as addition, removal of keys or connection drops)
143or jobs is processed and clients are updated.
144Since we use salt's presence events to track minions,
145please enable ``presence_events``
146and set a small value for the ``loop_interval``
147in the salt master config file.
148
149Exposes GET method to return websocket connections.
150All requests should include an auth token.
151A way to obtain obtain authentication tokens is shown below.
152
153.. code-block:: bash
154
155    % curl -si localhost:8000/login \\
156        -H "Accept: application/json" \\
157        -d username='salt' \\
158        -d password='salt' \\
159        -d eauth='pam'
160
161Which results in the response
162
163.. code-block:: json
164
165    {
166        "return": [{
167            "perms": [".*", "@runner", "@wheel"],
168            "start": 1400556492.277421,
169            "token": "d0ce6c1a37e99dcc0374392f272fe19c0090cca7",
170            "expire": 1400599692.277422,
171            "user": "salt",
172            "eauth": "pam"
173        }]
174    }
175
176In this example the ``token`` returned is ``d0ce6c1a37e99dcc0374392f272fe19c0090cca7`` and can be included
177in subsequent websocket requests (as part of the URL).
178
179The event stream can be easily consumed via JavaScript:
180
181.. code-block:: javascript
182
183    // Note, you must be authenticated!
184
185    // Get the Websocket connection to Salt
186    var source = new Websocket('wss://localhost:8000/formatted_events/d0ce6c1a37e99dcc0374392f272fe19c0090cca7');
187
188    // Get Salt's "real time" event stream.
189    source.onopen = function() { source.send('websocket client ready'); };
190
191    // Other handlers
192    source.onerror = function(e) { console.debug('error!', e); };
193
194    // e.data represents Salt's "real time" event data as serialized JSON.
195    source.onmessage = function(e) { console.debug(e.data); };
196
197    // Terminates websocket connection and Salt's "real time" event stream on the server.
198    source.close();
199
200Or via Python, using the Python module
201`websocket-client <https://pypi.python.org/pypi/websocket-client/>`_ for example.
202Or the tornado
203`client <https://tornado.readthedocs.io/en/latest/websocket.html#client-side-support>`_.
204
205.. code-block:: python
206
207    # Note, you must be authenticated!
208
209    from websocket import create_connection
210
211    # Get the Websocket connection to Salt
212    ws = create_connection('wss://localhost:8000/formatted_events/d0ce6c1a37e99dcc0374392f272fe19c0090cca7')
213
214    # Get Salt's "real time" event stream.
215    ws.send('websocket client ready')
216
217
218    # Simple listener to print results of Salt's "real time" event stream.
219    # Look at https://pypi.python.org/pypi/websocket-client/ for more examples.
220    while listening_to_events:
221        print ws.recv()       #  Salt's "real time" event data as serialized JSON.
222
223    # Terminates websocket connection and Salt's "real time" event stream on the server.
224    ws.close()
225
226    # Please refer to https://github.com/liris/websocket-client/issues/81 when using a self signed cert
227
228Above examples show how to establish a websocket connection to Salt and activating
229real time updates from Salt's event stream by signaling ``websocket client ready``.
230
231Example responses
232-----------------
233
234``Minion information`` is a dictionary keyed by each connected minion's ``id`` (``mid``),
235grains information for each minion is also included.
236
237Minion information is sent in response to the following minion events:
238
239- connection drops
240    - requires running ``manage.present`` periodically every ``loop_interval`` seconds
241- minion addition
242- minion removal
243
244.. code-block:: python
245
246    # Not all grains are shown
247    data: {
248        "minions": {
249            "minion1": {
250                "id": "minion1",
251                "grains": {
252                    "kernel": "Darwin",
253                    "domain": "local",
254                    "zmqversion": "4.0.3",
255                    "kernelrelease": "13.2.0"
256                }
257            }
258        }
259    }
260
261``Job information`` is also tracked and delivered.
262
263Job information is also a dictionary
264in which each job's information is keyed by salt's ``jid``.
265
266.. code-block:: python
267
268    data: {
269        "jobs": {
270            "20140609153646699137": {
271                "tgt_type": "glob",
272                "jid": "20140609153646699137",
273                "tgt": "*",
274                "start_time": "2014-06-09T15:36:46.700315",
275                "state": "complete",
276                "fun": "test.ping",
277                "minions": {
278                    "minion1": {
279                        "return": true,
280                        "retcode": 0,
281                        "success": true
282                    }
283                }
284            }
285        }
286    }
287
288Setup
289=====
290"""
291
292import logging
293
294import salt.ext.tornado.gen
295import salt.ext.tornado.websocket
296import salt.netapi
297import salt.utils.json
298
299from . import event_processor
300from .saltnado import _check_cors_origin
301
302_json = salt.utils.json.import_json()
303
304
305log = logging.getLogger(__name__)
306
307
308class AllEventsHandler(
309    salt.ext.tornado.websocket.WebSocketHandler
310):  # pylint: disable=W0223,W0232
311    """
312    Server side websocket handler.
313    """
314
315    # pylint: disable=W0221
316    def get(self, token):
317        """
318        Check the token, returns a 401 if the token is invalid.
319        Else open the websocket connection
320        """
321        log.debug("In the websocket get method")
322
323        self.token = token
324        # close the connection, if not authenticated
325        if not self.application.auth.get_tok(token):
326            log.debug("Refusing websocket connection, bad token!")
327            self.send_error(401)
328            return
329        super().get(token)
330
331    def open(self, token):  # pylint: disable=W0221
332        """
333        Return a websocket connection to Salt
334        representing Salt's "real time" event stream.
335        """
336        self.connected = False
337
338    @salt.ext.tornado.gen.coroutine
339    def on_message(self, message):
340        """Listens for a "websocket client ready" message.
341        Once that message is received an asynchronous job
342        is stated that yields messages to the client.
343        These messages make up salt's
344        "real time" event stream.
345        """
346        log.debug("Got websocket message %s", message)
347        if message == "websocket client ready":
348            if self.connected:
349                # TBD: Add ability to run commands in this branch
350                log.debug("Websocket already connected, returning")
351                return
352
353            self.connected = True
354
355            while True:
356                try:
357                    event = yield self.application.event_listener.get_event(self)
358                    self.write_message(salt.utils.json.dumps(event, _json_module=_json))
359                except Exception as err:  # pylint: disable=broad-except
360                    log.info(
361                        "Error! Ending server side websocket connection. Reason = %s",
362                        err,
363                    )
364                    break
365
366            self.close()
367        else:
368            # TBD: Add logic to run salt commands here
369            pass
370
371    def on_close(self, *args, **kwargs):
372        """Cleanup."""
373        log.debug("In the websocket close method")
374        self.close()
375
376    def check_origin(self, origin):
377        """
378        If cors is enabled, check that the origin is allowed
379        """
380
381        mod_opts = self.application.mod_opts
382
383        if mod_opts.get("cors_origin"):
384            return bool(_check_cors_origin(origin, mod_opts["cors_origin"]))
385        else:
386            return super().check_origin(origin)
387
388
389class FormattedEventsHandler(AllEventsHandler):  # pylint: disable=W0223,W0232
390    @salt.ext.tornado.gen.coroutine
391    def on_message(self, message):
392        """Listens for a "websocket client ready" message.
393        Once that message is received an asynchronous job
394        is stated that yields messages to the client.
395        These messages make up salt's
396        "real time" event stream.
397        """
398        log.debug("Got websocket message %s", message)
399        if message == "websocket client ready":
400            if self.connected:
401                # TBD: Add ability to run commands in this branch
402                log.debug("Websocket already connected, returning")
403                return
404
405            self.connected = True
406
407            evt_processor = event_processor.SaltInfo(self)
408            client = salt.netapi.NetapiClient(self.application.opts)
409            client.run(
410                {
411                    "fun": "grains.items",
412                    "tgt": "*",
413                    "token": self.token,
414                    "mode": "client",
415                    "asynchronous": "local_async",
416                    "client": "local",
417                }
418            )
419            while True:
420                try:
421                    event = yield self.application.event_listener.get_event(self)
422                    evt_processor.process(event, self.token, self.application.opts)
423                    # self.write_message('data: {0}\n\n'.format(salt.utils.json.dumps(event, _json_module=_json)))
424                except Exception as err:  # pylint: disable=broad-except
425                    log.debug(
426                        "Error! Ending server side websocket connection. Reason = %s",
427                        err,
428                    )
429                    break
430
431            self.close()
432        else:
433            # TBD: Add logic to run salt commands here
434            pass
435