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