Browse code

remove old versions

Gandolf authored on 10/03/2020 22:32:22
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,701 +0,0 @@
1
-#!/usr/bin/python -u
2
-# The -u option above turns off block buffering of python output. This 
3
-# assures that each error message gets individually printed to the log file.
4
-#
5
-# Module: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#
37
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
-
39
-import os
40
-import urllib2
41
-import sys
42
-import signal
43
-import subprocess
44
-import multiprocessing
45
-import time
46
-import json
47
-
48
-_USER = os.environ['USER']
49
-_HOSTNAME = os.uname()[1]
50
-
51
-   ### DEFAULT AREDN NODE URL ###
52
-
53
-# set url of the aredn node
54
-
55
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
-
57
-    ### FILE AND FOLDER LOCATIONS ###
58
-
59
-# folder for containing dynamic data objects
60
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
-# folder for charts and output data file
62
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
-# location of data output file
64
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
-# dummy output data file
66
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
-# database that stores node data
68
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
-
70
-    ### GLOBAL CONSTANTS ###
71
-
72
-# max number of failed data requests allowed
73
-_MAX_FAILED_DATA_REQUESTS = 0
74
-# interval in minutes between data requests to the aredn node
75
-_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
-# number seconds to wait for a response to HTTP request
77
-_HTTP_REQUEST_TIMEOUT = 10
78
-# standard chart width in pixels
79
-_CHART_WIDTH = 600
80
-# standard chart height in pixels
81
-_CHART_HEIGHT = 150
82
-# Set this to True only if this server is intended to relay raw
83
-# node data to a mirror server.
84
-_RELAY_SERVER = False
85
-
86
-   ### GLOBAL VARIABLES ###
87
-
88
-# turn on or off of verbose debugging information
89
-debugOption = False
90
-verboseDebug = False
91
-
92
-# The following two items are used for detecting system faults
93
-# and aredn node online or offline status.
94
-
95
-# count of failed attempts to get data from aredn node
96
-failedUpdateCount = 0
97
-# detected status of aredn node device
98
-nodeOnline = True
99
-
100
-# ip address of aredn node
101
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
-# frequency of data requests to aredn node
103
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
-# last node request time
105
-lastDataPointTime = -1
106
-
107
-  ###  PRIVATE METHODS  ###
108
-
109
-def getTimeStamp():
110
-    """
111
-    Set the error message time stamp to the local system time.
112
-    Parameters: none
113
-    Returns: string containing the time stamp
114
-    """
115
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
-##end def
117
-
118
-def getLastDataPointTime():
119
-    """
120
-    Get the timestamp of the most recent update to the round robin
121
-    database.
122
-    Parameters: none
123
-    Returns: string epoch time stamp of the last rrd update
124
-    """
125
-    strCmd = "rrdtool lastupdate %s" % \
126
-             (_RRD_FILE)
127
-
128
-    # Run the command as a subprocess.
129
-    try:
130
-        result = subprocess.check_output(strCmd, shell=True,  \
131
-                             stderr=subprocess.STDOUT)
132
-    except subprocess.CalledProcessError, exError:
133
-        print "%s: rrdtool update failed: %s" % \
134
-                    (getTimeStamp(), exError.output)
135
-        return None
136
-
137
-    # Return just the epoch time stamp of the last rrd update
138
-    return int((result.split('\n')[-2]).split(':')[0])
139
-##end def
140
-
141
-def setStatusToOffline():
142
-    """Set the detected status of the aredn node to
143
-       "offline" and inform downstream clients by removing input
144
-       and output data files.
145
-       Parameters: none
146
-       Returns: nothing
147
-    """
148
-    global nodeOnline
149
-
150
-    # Inform downstream clients by removing output data file.
151
-    if os.path.exists(_OUTPUT_DATA_FILE):
152
-       os.remove(_OUTPUT_DATA_FILE)
153
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
-       os.remove(_DUMMY_OUTPUT_FILE)
155
-    # If the aredn node was previously online, then send
156
-    # a message that we are now offline.
157
-    if nodeOnline:
158
-        print '%s aredn node offline' % getTimeStamp()
159
-    nodeOnline = False
160
-##end def
161
-
162
-def terminateAgentProcess(signal, frame):
163
-    """Send a message to log when the agent process gets killed
164
-       by the operating system.  Inform downstream clients
165
-       by removing input and output data files.
166
-       Parameters:
167
-           signal, frame - dummy parameters
168
-       Returns: nothing
169
-    """
170
-    # Inform downstream clients by removing output data file.
171
-    if os.path.exists(_OUTPUT_DATA_FILE):
172
-       os.remove(_OUTPUT_DATA_FILE)
173
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
-       os.remove(_DUMMY_OUTPUT_FILE)
175
-    print '%s terminating arednsig agent process' % \
176
-              (getTimeStamp())
177
-    sys.exit(0)
178
-##end def
179
-
180
-  ###  PUBLIC METHODS  ###
181
-
182
-def getArednNodeData():
183
-    """Send http request to aredn node.  The response from the
184
-       node contains the node signal data as unformatted ascii text.
185
-       Parameters: none
186
-       Returns: a string containing the node signal data if successful,
187
-                or None if not successful
188
-    """
189
-    try:
190
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
-
192
-        # Format received data into a single string.
193
-        content = ""
194
-        for line in conn:
195
-            content += line.strip()
196
-        del conn
197
-
198
-    except Exception, exError:
199
-        # If no response is received from the device, then assume that
200
-        # the device is down or unavailable over the network.  In
201
-        # that case return None to the calling function.
202
-        print "%s http error: %s" % (getTimeStamp(), exError)
203
-        return None
204
-
205
-    if verboseDebug:
206
-        print "http request successful: %d bytes" % len(content)
207
-
208
-    return content
209
-##end def
210
-
211
-def parseNodeData(sData, ldData):
212
-    """Parse the node  signal data JSON string from the aredn node
213
-       into its component parts.  
214
-       Parameters:
215
-           sData - the string containing the data to be parsed
216
-           dData - a dictionary object to contain the parsed data items
217
-       Returns: True if successful, False otherwise
218
-    """
219
-    iTrail = int(dataRequestInterval)
220
-
221
-    try:
222
-        ldTmp = json.loads(sData[1:-1])
223
-        ldTmp = ldTmp[-iTrail:]
224
-        if len(ldTmp) != iTrail:
225
-            #raise Exception("truncated list")
226
-            pass
227
-    except Exception, exError:
228
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
-        return False
230
-    
231
-    del ldData[:]
232
-    for item in ldTmp:
233
-        ldData.append(item)
234
-
235
-    if verboseDebug:
236
-        print "parse successful: %d data points" % len(ldData)
237
-    return True
238
-##end def
239
-
240
-def convertData(ldData):
241
-    """Convert individual node signal data items as necessary.
242
-       Parameters:
243
-           dData - a dictionary object containing the node signal data
244
-       Returns: True if successful, False otherwise
245
-    """
246
-    # parse example string
247
-    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
-    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
-    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
-    #
251
-    index = 0
252
-    while index < len(ldData):
253
-        item = ldData[index]
254
-        try:
255
-            item['time'] = int(item.pop('x')) / 1000
256
-            item['signal'] = int(item['y'][0])
257
-            item['noise'] = int(item['y'][1])
258
-            item['snr'] = int(item.pop('m'))
259
-            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
-            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
-            item['rx_rate'] = int(item.pop('rx_rate'))
262
-            item['tx_rate'] = int(item.pop('tx_rate'))
263
-            item.pop('y')
264
-            item.pop('label')
265
-        except Exception, exError:
266
-            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
-            print "discarding %s" % item
268
-            #return False
269
-            ldData.pop(index)
270
-        else:
271
-            index += 1
272
-    ##end for
273
-
274
-    if len(ldData) > 0:
275
-        if verboseDebug:
276
-            print "convert data successful"
277
-        return True
278
-    else:
279
-        print "convert data failed"
280
-        return False
281
-##end def
282
-
283
-def updateDatabase(ldData):
284
-    """
285
-    Update the rrdtool database by executing an rrdtool system command.
286
-    Format the command using the data extracted from the aredn node
287
-    response.   
288
-    Parameters: dData - dictionary object containing data items to be
289
-                        written to the rr database file
290
-    Returns: True if successful, False otherwise
291
-    """
292
-    updateCount = 0
293
-    lastDataPointTime = getLastDataPointTime()
294
-
295
-    if verboseDebug:
296
-         print "updating database..."
297
-
298
-    for item in ldData:
299
-
300
-        if item['time'] <= lastDataPointTime:
301
-            if verboseDebug:
302
-                print "%s invalid timestamp: discarding data" % \
303
-                      (getTimeStamp())
304
-            continue
305
-
306
-        # Format the rrdtool update command.
307
-        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
-        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
-                 item['noise'], item['snr'], item['rx_mcs'], \
310
-                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
-
312
-        if verboseDebug:
313
-            print "%s" % strCmd # DEBUG
314
-
315
-        # Run the command as a subprocess.
316
-        try:
317
-            subprocess.check_output(strCmd, shell=True,  \
318
-                                 stderr=subprocess.STDOUT)
319
-        except subprocess.CalledProcessError, exError:
320
-            print "%s: rrdtool update failed: %s" % \
321
-                        (getTimeStamp(), exError.output)
322
-            return False
323
-        updateCount += 1
324
-    ##end for
325
-
326
-    if debugOption:
327
-        print '%s added %d data points to database' % \
328
-              (getTimeStamp(), updateCount)
329
-    return True
330
-##end def
331
-
332
-def writeOutputDataFile(sData, ldData):
333
-    """Write node data items to the output data file, formatted as 
334
-       a Javascript file.  This file may then be accessed and used by
335
-       by downstream clients, for instance, in HTML documents.
336
-       Parameters:
337
-           sData - a string object containing the data to be written
338
-                   to the output data file
339
-       Returns: True if successful, False otherwise
340
-    """
341
-    # Write file for use by html clients.  The following two
342
-    # data items are sent to the client file.
343
-    #    * The last database update date and time
344
-    #    * The data request interval
345
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
-                                time.localtime(ldData[-1]['time']) )
347
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
-           (lastUpdate, dataRequestInterval)
349
-    try:
350
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
-        fc.write(sDate)
352
-        fc.close()
353
-    except Exception, exError:
354
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
-        return False
356
-
357
-    if _RELAY_SERVER:
358
-        # Write the entire node data response to the output data file.
359
-        try:
360
-            fc = open(_OUTPUT_DATA_FILE, "w")
361
-            fc.write(sData)
362
-            fc.close()
363
-        except Exception, exError:
364
-            print "%s write output file failed: %s" % \
365
-                  (getTimeStamp(), exError)
366
-            return False
367
-        if verboseDebug:
368
-            print "write output data file: %d bytes" % len(sData)
369
-
370
-    return True
371
-## end def
372
-
373
-def setNodeStatus(updateSuccess):
374
-    """Detect if aredn node is offline or not available on
375
-       the network. After a set number of attempts to get data
376
-       from the node set a flag that the node is offline.
377
-       Parameters:
378
-           updateSuccess - a boolean that is True if data request
379
-                           successful, False otherwise
380
-       Returns: nothing
381
-    """
382
-    global failedUpdateCount, nodeOnline
383
-
384
-    if updateSuccess:
385
-        failedUpdateCount = 0
386
-        # Set status and send a message to the log if the node was
387
-        # previously offline and is now online.
388
-        if not nodeOnline:
389
-            print '%s aredn node online' % getTimeStamp()
390
-            nodeOnline = True
391
-    else:
392
-        # The last attempt failed, so update the failed attempts
393
-        # count.
394
-        failedUpdateCount += 1
395
-
396
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
-        # Max number of failed data requests, so set
398
-        # node status to offline.
399
-        setStatusToOffline()
400
-##end def
401
-
402
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
-                lower, upper, addTrend, autoScale):
404
-    """Uses rrdtool to create a graph of specified node data item.
405
-       Parameters:
406
-           fileName - name of file containing the graph
407
-           dataItem - data item to be graphed
408
-           gLabel - string containing a graph label for the data item
409
-           gTitle - string containing a title for the graph
410
-           gStart - beginning time of the graphed data
411
-           lower - lower bound for graph ordinate #NOT USED
412
-           upper - upper bound for graph ordinate #NOT USED
413
-           addTrend - 0, show only graph data
414
-                      1, show only a trend line
415
-                      2, show a trend line and the graph data
416
-           autoScale - if True, then use vertical axis auto scaling
417
-               (lower and upper parameters are ignored), otherwise use
418
-               lower and upper parameters to set vertical axis scale
419
-       Returns: True if successful, False otherwise
420
-    """
421
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
-    trendWindow = { 'end-1day': 7200,
423
-                    'end-4weeks': 172800,
424
-                    'end-12months': 604800 }
425
- 
426
-    # Format the rrdtool graph command.
427
-
428
-    # Set chart start time, height, and width.
429
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
-   
432
-    # Set the range and scaling of the chart y-axis.
433
-    if lower < upper:
434
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
-    elif autoScale:
436
-        strCmd += "-A "
437
-    strCmd += "-Y "
438
-
439
-    # Set the chart ordinate label and chart title. 
440
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
- 
442
-    # Show the data, or a moving average trend line over
443
-    # the data, or both.
444
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
-    if addTrend == 0:
446
-        strCmd += "LINE1:dSeries#0400ff "
447
-    elif addTrend == 1:
448
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
-                  % trendWindow[gStart]
450
-    elif addTrend == 2:
451
-        strCmd += "LINE1:dSeries#0400ff "
452
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
-                  % trendWindow[gStart]
454
-     
455
-    if verboseDebug:
456
-        print "%s" % strCmd # DEBUG
457
-    
458
-    # Run the formatted rrdtool command as a subprocess.
459
-    try:
460
-        result = subprocess.check_output(strCmd, \
461
-                     stderr=subprocess.STDOUT,   \
462
-                     shell=True)
463
-    except subprocess.CalledProcessError, exError:
464
-        print "rrdtool graph failed: %s" % (exError.output)
465
-        return False
466
-
467
-    if debugOption:
468
-        print "rrdtool graph: %s\n" % result,
469
-    return True
470
-
471
-##end def
472
-
473
-def generateGraphs():
474
-    """Generate graphs for display in html documents.
475
-       Parameters: none
476
-       Returns: nothing
477
-    """
478
-    autoScale = False
479
-
480
-    # The following will force creation of charts
481
-    # of only signal strength and S/N charts.  Note that the following
482
-    # data items appear constant and do not show variation with time:
483
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
-    # these parameters are demonstrated to vary in time, there is no point
485
-    # in creating the charts for these data items.
486
-    createAllCharts = False
487
-
488
-    # 24 hour stock charts
489
-
490
-    createGraph('24hr_signal', 'S', 'dBm', 
491
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
-    createGraph('24hr_snr', 'SNR', 'dB', 
493
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
-
495
-    if createAllCharts:
496
-        createGraph('24hr_noise', 'N', 'dBm', 
497
-                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
-                    autoScale)
499
-        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
-                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
-                    autoScale)
502
-        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
-                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
-                    autoScale)
505
-        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
-                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
-                     autoScale)
508
-        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
-                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
-                     autoScale)
511
-
512
-    # 4 week stock charts
513
-
514
-    createGraph('4wk_signal', 'S', 'dBm', 
515
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
-    createGraph('4wk_snr', 'SNR', 'dB', 
517
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
-
519
-    if createAllCharts:
520
-        createGraph('4wk_noise', 'N', 'dBm', 
521
-                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
-                    autoScale)
523
-        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
-                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
-                    autoScale)
526
-        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
-                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
-                    autoScale)
529
-        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
-                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
-                    autoScale)
532
-        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
-                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
-                    autoScale)
535
-
536
-    # 12 month stock charts
537
-
538
-    createGraph('12m_signal', 'S', 'dBm', 
539
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
-    createGraph('12m_snr', 'SNR', 'dB', 
541
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
-
543
-    if createAllCharts:
544
-        createGraph('12m_noise', 'N', 'dBm', 
545
-                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
-                    autoScale)
547
-        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
-                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
-                    autoScale)
550
-        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
-                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
-                    autoScale)
553
-        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
-                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
-                    autoScale)
556
-        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
-                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
-                    autoScale)
559
-    if debugOption:
560
-        #print # print a blank line to improve readability when in debug mode
561
-        pass
562
-##end def
563
-
564
-def getCLarguments():
565
-    """Get command line arguments.  There are four possible arguments
566
-          -d turns on debug mode
567
-          -v turns on verbose debug mode
568
-          -t sets the aredn node query interval
569
-          -u sets the url of the aredn nodeing device
570
-       Returns: nothing
571
-    """
572
-    global debugOption, verboseDebug, dataRequestInterval, \
573
-           arednNodeUrl
574
-
575
-    index = 1
576
-    while index < len(sys.argv):
577
-        if sys.argv[index] == '-d':
578
-            debugOption = True
579
-        elif sys.argv[index] == '-v':
580
-            debugOption = True
581
-            verboseDebug = True
582
-        elif sys.argv[index] == '-p':
583
-            try:
584
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
-            except:
586
-                print "invalid polling period"
587
-                exit(-1)
588
-            index += 1
589
-        elif sys.argv[index] == '-u':
590
-            arednNodeUrl = sys.argv[index + 1]
591
-            index += 1
592
-        else:
593
-            cmd_name = sys.argv[0].split('/')
594
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
-            exit(-1)
596
-        index += 1
597
-##end def
598
-
599
-def main():
600
-    """Handles timing of events and acts as executive routine managing
601
-       all other functions.
602
-       Parameters: none
603
-       Returns: nothing
604
-    """
605
-    global dataRequestInterval
606
-
607
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
-
609
-    print '%s starting up arednsig agent process' % \
610
-                  (getTimeStamp())
611
-
612
-    # last time output JSON file updated
613
-    lastDataRequestTime = -1
614
-    # last time charts generated
615
-    lastChartUpdateTime = - 1
616
-    # last time the rrdtool database updated
617
-    lastDatabaseUpdateTime = -1
618
-
619
-    ## Get command line arguments.
620
-    getCLarguments()
621
-
622
-    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
-    chartUpdateInterval = dataRequestInterval # get charts interval
624
-
625
-    ## Exit with error if rrdtool database does not exist.
626
-    if not os.path.exists(_RRD_FILE):
627
-        print 'rrdtool database does not exist\n' \
628
-              'use createArednsigRrd script to ' \
629
-              'create rrdtool database\n'
630
-        exit(1)
631
- 
632
-    ## main loop
633
-    while True:
634
-
635
-        currentTime = time.time() # get current time in seconds
636
-
637
-        # Every web update interval request data from the aredn
638
-        # node and process the received data.
639
-        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
-            lastDataRequestTime = currentTime
641
-            ldData = []
642
-            result = True
643
-
644
-            # Get the data string from the device.
645
-            sData = getArednNodeData()
646
-            # If the first http request fails, try one more time.
647
-            if sData == None:
648
-                time.sleep(5)
649
-                sData = getArednNodeData()
650
-                if sData == None:
651
-                    result = False
652
-
653
-            # If successful parse the data.
654
-            if result:
655
-                result = parseNodeData(sData, ldData)
656
-           
657
-            # If parsing successful, convert the data.
658
-            if result:
659
-                result = convertData(ldData)
660
-
661
-            # If conversion successful, write data to data files.
662
-            if result:
663
-                result = updateDatabase(ldData)
664
-
665
-            if result:
666
-                writeOutputDataFile(sData, ldData)
667
-
668
-            # Set the node status to online or offline depending on the
669
-            # success or failure of the above operations.
670
-            setNodeStatus(result)
671
-
672
-
673
-        # At the chart generation interval, generate charts.
674
-        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
-            lastChartUpdateTime = currentTime
676
-            p = multiprocessing.Process(target=generateGraphs, args=())
677
-            p.start()
678
-
679
-        # Relinquish processing back to the operating system until
680
-        # the next update interval.
681
-
682
-        elapsedTime = time.time() - currentTime
683
-        if debugOption:
684
-            if result:
685
-                print "%s update successful:" % getTimeStamp(),
686
-            else:
687
-                print "%s update failed:" % getTimeStamp(),
688
-            print "%6f seconds processing time\n" % elapsedTime 
689
-        remainingTime = requestIntervalSeconds - elapsedTime
690
-        if remainingTime > 0.0:
691
-            time.sleep(remainingTime)
692
-    ## end while
693
-    return
694
-## end def
695
-
696
-if __name__ == '__main__':
697
-    try:
698
-        main()
699
-    except KeyboardInterrupt:
700
-        print '\n',
701
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

support for Aredn firmware version 3.19.3.0

gandolf authored on 03/31/2020 18:06:12
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,701 @@
1
+#!/usr/bin/python -u
2
+# The -u option above turns off block buffering of python output. This 
3
+# assures that each error message gets individually printed to the log file.
4
+#
5
+# Module: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+import json
47
+
48
+_USER = os.environ['USER']
49
+_HOSTNAME = os.uname()[1]
50
+
51
+   ### DEFAULT AREDN NODE URL ###
52
+
53
+# set url of the aredn node
54
+
55
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
+
57
+    ### FILE AND FOLDER LOCATIONS ###
58
+
59
+# folder for containing dynamic data objects
60
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
+# folder for charts and output data file
62
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
+# location of data output file
64
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
+# dummy output data file
66
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
+# database that stores node data
68
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
+
70
+    ### GLOBAL CONSTANTS ###
71
+
72
+# max number of failed data requests allowed
73
+_MAX_FAILED_DATA_REQUESTS = 0
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# standard chart width in pixels
79
+_CHART_WIDTH = 600
80
+# standard chart height in pixels
81
+_CHART_HEIGHT = 150
82
+# Set this to True only if this server is intended to relay raw
83
+# node data to a mirror server.
84
+_RELAY_SERVER = False
85
+
86
+   ### GLOBAL VARIABLES ###
87
+
88
+# turn on or off of verbose debugging information
89
+debugOption = False
90
+verboseDebug = False
91
+
92
+# The following two items are used for detecting system faults
93
+# and aredn node online or offline status.
94
+
95
+# count of failed attempts to get data from aredn node
96
+failedUpdateCount = 0
97
+# detected status of aredn node device
98
+nodeOnline = True
99
+
100
+# ip address of aredn node
101
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
+# frequency of data requests to aredn node
103
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
+# last node request time
105
+lastDataPointTime = -1
106
+
107
+  ###  PRIVATE METHODS  ###
108
+
109
+def getTimeStamp():
110
+    """
111
+    Set the error message time stamp to the local system time.
112
+    Parameters: none
113
+    Returns: string containing the time stamp
114
+    """
115
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
+##end def
117
+
118
+def getLastDataPointTime():
119
+    """
120
+    Get the timestamp of the most recent update to the round robin
121
+    database.
122
+    Parameters: none
123
+    Returns: string epoch time stamp of the last rrd update
124
+    """
125
+    strCmd = "rrdtool lastupdate %s" % \
126
+             (_RRD_FILE)
127
+
128
+    # Run the command as a subprocess.
129
+    try:
130
+        result = subprocess.check_output(strCmd, shell=True,  \
131
+                             stderr=subprocess.STDOUT)
132
+    except subprocess.CalledProcessError, exError:
133
+        print "%s: rrdtool update failed: %s" % \
134
+                    (getTimeStamp(), exError.output)
135
+        return None
136
+
137
+    # Return just the epoch time stamp of the last rrd update
138
+    return int((result.split('\n')[-2]).split(':')[0])
139
+##end def
140
+
141
+def setStatusToOffline():
142
+    """Set the detected status of the aredn node to
143
+       "offline" and inform downstream clients by removing input
144
+       and output data files.
145
+       Parameters: none
146
+       Returns: nothing
147
+    """
148
+    global nodeOnline
149
+
150
+    # Inform downstream clients by removing output data file.
151
+    if os.path.exists(_OUTPUT_DATA_FILE):
152
+       os.remove(_OUTPUT_DATA_FILE)
153
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
+       os.remove(_DUMMY_OUTPUT_FILE)
155
+    # If the aredn node was previously online, then send
156
+    # a message that we are now offline.
157
+    if nodeOnline:
158
+        print '%s aredn node offline' % getTimeStamp()
159
+    nodeOnline = False
160
+##end def
161
+
162
+def terminateAgentProcess(signal, frame):
163
+    """Send a message to log when the agent process gets killed
164
+       by the operating system.  Inform downstream clients
165
+       by removing input and output data files.
166
+       Parameters:
167
+           signal, frame - dummy parameters
168
+       Returns: nothing
169
+    """
170
+    # Inform downstream clients by removing output data file.
171
+    if os.path.exists(_OUTPUT_DATA_FILE):
172
+       os.remove(_OUTPUT_DATA_FILE)
173
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
+       os.remove(_DUMMY_OUTPUT_FILE)
175
+    print '%s terminating arednsig agent process' % \
176
+              (getTimeStamp())
177
+    sys.exit(0)
178
+##end def
179
+
180
+  ###  PUBLIC METHODS  ###
181
+
182
+def getArednNodeData():
183
+    """Send http request to aredn node.  The response from the
184
+       node contains the node signal data as unformatted ascii text.
185
+       Parameters: none
186
+       Returns: a string containing the node signal data if successful,
187
+                or None if not successful
188
+    """
189
+    try:
190
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
+
192
+        # Format received data into a single string.
193
+        content = ""
194
+        for line in conn:
195
+            content += line.strip()
196
+        del conn
197
+
198
+    except Exception, exError:
199
+        # If no response is received from the device, then assume that
200
+        # the device is down or unavailable over the network.  In
201
+        # that case return None to the calling function.
202
+        print "%s http error: %s" % (getTimeStamp(), exError)
203
+        return None
204
+
205
+    if verboseDebug:
206
+        print "http request successful: %d bytes" % len(content)
207
+
208
+    return content
209
+##end def
210
+
211
+def parseNodeData(sData, ldData):
212
+    """Parse the node  signal data JSON string from the aredn node
213
+       into its component parts.  
214
+       Parameters:
215
+           sData - the string containing the data to be parsed
216
+           dData - a dictionary object to contain the parsed data items
217
+       Returns: True if successful, False otherwise
218
+    """
219
+    iTrail = int(dataRequestInterval)
220
+
221
+    try:
222
+        ldTmp = json.loads(sData[1:-1])
223
+        ldTmp = ldTmp[-iTrail:]
224
+        if len(ldTmp) != iTrail:
225
+            #raise Exception("truncated list")
226
+            pass
227
+    except Exception, exError:
228
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
+        return False
230
+    
231
+    del ldData[:]
232
+    for item in ldTmp:
233
+        ldData.append(item)
234
+
235
+    if verboseDebug:
236
+        print "parse successful: %d data points" % len(ldData)
237
+    return True
238
+##end def
239
+
240
+def convertData(ldData):
241
+    """Convert individual node signal data items as necessary.
242
+       Parameters:
243
+           dData - a dictionary object containing the node signal data
244
+       Returns: True if successful, False otherwise
245
+    """
246
+    # parse example string
247
+    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
+    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
+    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
+    #
251
+    index = 0
252
+    while index < len(ldData):
253
+        item = ldData[index]
254
+        try:
255
+            item['time'] = int(item.pop('x')) / 1000
256
+            item['signal'] = int(item['y'][0])
257
+            item['noise'] = int(item['y'][1])
258
+            item['snr'] = int(item.pop('m'))
259
+            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
+            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
+            item['rx_rate'] = int(item.pop('rx_rate'))
262
+            item['tx_rate'] = int(item.pop('tx_rate'))
263
+            item.pop('y')
264
+            item.pop('label')
265
+        except Exception, exError:
266
+            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
+            print "discarding %s" % item
268
+            #return False
269
+            ldData.pop(index)
270
+        else:
271
+            index += 1
272
+    ##end for
273
+
274
+    if len(ldData) > 0:
275
+        if verboseDebug:
276
+            print "convert data successful"
277
+        return True
278
+    else:
279
+        print "convert data failed"
280
+        return False
281
+##end def
282
+
283
+def updateDatabase(ldData):
284
+    """
285
+    Update the rrdtool database by executing an rrdtool system command.
286
+    Format the command using the data extracted from the aredn node
287
+    response.   
288
+    Parameters: dData - dictionary object containing data items to be
289
+                        written to the rr database file
290
+    Returns: True if successful, False otherwise
291
+    """
292
+    updateCount = 0
293
+    lastDataPointTime = getLastDataPointTime()
294
+
295
+    if verboseDebug:
296
+         print "updating database..."
297
+
298
+    for item in ldData:
299
+
300
+        if item['time'] <= lastDataPointTime:
301
+            if verboseDebug:
302
+                print "%s invalid timestamp: discarding data" % \
303
+                      (getTimeStamp())
304
+            continue
305
+
306
+        # Format the rrdtool update command.
307
+        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
+        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
+                 item['noise'], item['snr'], item['rx_mcs'], \
310
+                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
+
312
+        if verboseDebug:
313
+            print "%s" % strCmd # DEBUG
314
+
315
+        # Run the command as a subprocess.
316
+        try:
317
+            subprocess.check_output(strCmd, shell=True,  \
318
+                                 stderr=subprocess.STDOUT)
319
+        except subprocess.CalledProcessError, exError:
320
+            print "%s: rrdtool update failed: %s" % \
321
+                        (getTimeStamp(), exError.output)
322
+            return False
323
+        updateCount += 1
324
+    ##end for
325
+
326
+    if debugOption:
327
+        print '%s added %d data points to database' % \
328
+              (getTimeStamp(), updateCount)
329
+    return True
330
+##end def
331
+
332
+def writeOutputDataFile(sData, ldData):
333
+    """Write node data items to the output data file, formatted as 
334
+       a Javascript file.  This file may then be accessed and used by
335
+       by downstream clients, for instance, in HTML documents.
336
+       Parameters:
337
+           sData - a string object containing the data to be written
338
+                   to the output data file
339
+       Returns: True if successful, False otherwise
340
+    """
341
+    # Write file for use by html clients.  The following two
342
+    # data items are sent to the client file.
343
+    #    * The last database update date and time
344
+    #    * The data request interval
345
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
+                                time.localtime(ldData[-1]['time']) )
347
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
+           (lastUpdate, dataRequestInterval)
349
+    try:
350
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
+        fc.write(sDate)
352
+        fc.close()
353
+    except Exception, exError:
354
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
+        return False
356
+
357
+    if _RELAY_SERVER:
358
+        # Write the entire node data response to the output data file.
359
+        try:
360
+            fc = open(_OUTPUT_DATA_FILE, "w")
361
+            fc.write(sData)
362
+            fc.close()
363
+        except Exception, exError:
364
+            print "%s write output file failed: %s" % \
365
+                  (getTimeStamp(), exError)
366
+            return False
367
+        if verboseDebug:
368
+            print "write output data file: %d bytes" % len(sData)
369
+
370
+    return True
371
+## end def
372
+
373
+def setNodeStatus(updateSuccess):
374
+    """Detect if aredn node is offline or not available on
375
+       the network. After a set number of attempts to get data
376
+       from the node set a flag that the node is offline.
377
+       Parameters:
378
+           updateSuccess - a boolean that is True if data request
379
+                           successful, False otherwise
380
+       Returns: nothing
381
+    """
382
+    global failedUpdateCount, nodeOnline
383
+
384
+    if updateSuccess:
385
+        failedUpdateCount = 0
386
+        # Set status and send a message to the log if the node was
387
+        # previously offline and is now online.
388
+        if not nodeOnline:
389
+            print '%s aredn node online' % getTimeStamp()
390
+            nodeOnline = True
391
+    else:
392
+        # The last attempt failed, so update the failed attempts
393
+        # count.
394
+        failedUpdateCount += 1
395
+
396
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
+        # Max number of failed data requests, so set
398
+        # node status to offline.
399
+        setStatusToOffline()
400
+##end def
401
+
402
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
+                lower, upper, addTrend, autoScale):
404
+    """Uses rrdtool to create a graph of specified node data item.
405
+       Parameters:
406
+           fileName - name of file containing the graph
407
+           dataItem - data item to be graphed
408
+           gLabel - string containing a graph label for the data item
409
+           gTitle - string containing a title for the graph
410
+           gStart - beginning time of the graphed data
411
+           lower - lower bound for graph ordinate #NOT USED
412
+           upper - upper bound for graph ordinate #NOT USED
413
+           addTrend - 0, show only graph data
414
+                      1, show only a trend line
415
+                      2, show a trend line and the graph data
416
+           autoScale - if True, then use vertical axis auto scaling
417
+               (lower and upper parameters are ignored), otherwise use
418
+               lower and upper parameters to set vertical axis scale
419
+       Returns: True if successful, False otherwise
420
+    """
421
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
+    trendWindow = { 'end-1day': 7200,
423
+                    'end-4weeks': 172800,
424
+                    'end-12months': 604800 }
425
+ 
426
+    # Format the rrdtool graph command.
427
+
428
+    # Set chart start time, height, and width.
429
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
+   
432
+    # Set the range and scaling of the chart y-axis.
433
+    if lower < upper:
434
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
+    elif autoScale:
436
+        strCmd += "-A "
437
+    strCmd += "-Y "
438
+
439
+    # Set the chart ordinate label and chart title. 
440
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
+ 
442
+    # Show the data, or a moving average trend line over
443
+    # the data, or both.
444
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
+    if addTrend == 0:
446
+        strCmd += "LINE1:dSeries#0400ff "
447
+    elif addTrend == 1:
448
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
+                  % trendWindow[gStart]
450
+    elif addTrend == 2:
451
+        strCmd += "LINE1:dSeries#0400ff "
452
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
+                  % trendWindow[gStart]
454
+     
455
+    if verboseDebug:
456
+        print "%s" % strCmd # DEBUG
457
+    
458
+    # Run the formatted rrdtool command as a subprocess.
459
+    try:
460
+        result = subprocess.check_output(strCmd, \
461
+                     stderr=subprocess.STDOUT,   \
462
+                     shell=True)
463
+    except subprocess.CalledProcessError, exError:
464
+        print "rrdtool graph failed: %s" % (exError.output)
465
+        return False
466
+
467
+    if debugOption:
468
+        print "rrdtool graph: %s\n" % result,
469
+    return True
470
+
471
+##end def
472
+
473
+def generateGraphs():
474
+    """Generate graphs for display in html documents.
475
+       Parameters: none
476
+       Returns: nothing
477
+    """
478
+    autoScale = False
479
+
480
+    # The following will force creation of charts
481
+    # of only signal strength and S/N charts.  Note that the following
482
+    # data items appear constant and do not show variation with time:
483
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
+    # these parameters are demonstrated to vary in time, there is no point
485
+    # in creating the charts for these data items.
486
+    createAllCharts = False
487
+
488
+    # 24 hour stock charts
489
+
490
+    createGraph('24hr_signal', 'S', 'dBm', 
491
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
+    createGraph('24hr_snr', 'SNR', 'dB', 
493
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
+
495
+    if createAllCharts:
496
+        createGraph('24hr_noise', 'N', 'dBm', 
497
+                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
+                    autoScale)
499
+        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
+                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
+                    autoScale)
502
+        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
+                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
+                    autoScale)
505
+        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
+                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
+                     autoScale)
508
+        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
+                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
+                     autoScale)
511
+
512
+    # 4 week stock charts
513
+
514
+    createGraph('4wk_signal', 'S', 'dBm', 
515
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
+    createGraph('4wk_snr', 'SNR', 'dB', 
517
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
+
519
+    if createAllCharts:
520
+        createGraph('4wk_noise', 'N', 'dBm', 
521
+                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
+                    autoScale)
523
+        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
+                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
+                    autoScale)
526
+        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
+                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
+                    autoScale)
529
+        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
+                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
+                    autoScale)
532
+        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
+                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
+                    autoScale)
535
+
536
+    # 12 month stock charts
537
+
538
+    createGraph('12m_signal', 'S', 'dBm', 
539
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
+    createGraph('12m_snr', 'SNR', 'dB', 
541
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
+
543
+    if createAllCharts:
544
+        createGraph('12m_noise', 'N', 'dBm', 
545
+                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
+                    autoScale)
547
+        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
+                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
+                    autoScale)
550
+        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
+                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
+                    autoScale)
553
+        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
+                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
+                    autoScale)
556
+        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
+                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
+                    autoScale)
559
+    if debugOption:
560
+        #print # print a blank line to improve readability when in debug mode
561
+        pass
562
+##end def
563
+
564
+def getCLarguments():
565
+    """Get command line arguments.  There are four possible arguments
566
+          -d turns on debug mode
567
+          -v turns on verbose debug mode
568
+          -t sets the aredn node query interval
569
+          -u sets the url of the aredn nodeing device
570
+       Returns: nothing
571
+    """
572
+    global debugOption, verboseDebug, dataRequestInterval, \
573
+           arednNodeUrl
574
+
575
+    index = 1
576
+    while index < len(sys.argv):
577
+        if sys.argv[index] == '-d':
578
+            debugOption = True
579
+        elif sys.argv[index] == '-v':
580
+            debugOption = True
581
+            verboseDebug = True
582
+        elif sys.argv[index] == '-p':
583
+            try:
584
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
+            except:
586
+                print "invalid polling period"
587
+                exit(-1)
588
+            index += 1
589
+        elif sys.argv[index] == '-u':
590
+            arednNodeUrl = sys.argv[index + 1]
591
+            index += 1
592
+        else:
593
+            cmd_name = sys.argv[0].split('/')
594
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
+            exit(-1)
596
+        index += 1
597
+##end def
598
+
599
+def main():
600
+    """Handles timing of events and acts as executive routine managing
601
+       all other functions.
602
+       Parameters: none
603
+       Returns: nothing
604
+    """
605
+    global dataRequestInterval
606
+
607
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
+
609
+    print '%s starting up arednsig agent process' % \
610
+                  (getTimeStamp())
611
+
612
+    # last time output JSON file updated
613
+    lastDataRequestTime = -1
614
+    # last time charts generated
615
+    lastChartUpdateTime = - 1
616
+    # last time the rrdtool database updated
617
+    lastDatabaseUpdateTime = -1
618
+
619
+    ## Get command line arguments.
620
+    getCLarguments()
621
+
622
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
+    chartUpdateInterval = dataRequestInterval # get charts interval
624
+
625
+    ## Exit with error if rrdtool database does not exist.
626
+    if not os.path.exists(_RRD_FILE):
627
+        print 'rrdtool database does not exist\n' \
628
+              'use createArednsigRrd script to ' \
629
+              'create rrdtool database\n'
630
+        exit(1)
631
+ 
632
+    ## main loop
633
+    while True:
634
+
635
+        currentTime = time.time() # get current time in seconds
636
+
637
+        # Every web update interval request data from the aredn
638
+        # node and process the received data.
639
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
+            lastDataRequestTime = currentTime
641
+            ldData = []
642
+            result = True
643
+
644
+            # Get the data string from the device.
645
+            sData = getArednNodeData()
646
+            # If the first http request fails, try one more time.
647
+            if sData == None:
648
+                time.sleep(5)
649
+                sData = getArednNodeData()
650
+                if sData == None:
651
+                    result = False
652
+
653
+            # If successful parse the data.
654
+            if result:
655
+                result = parseNodeData(sData, ldData)
656
+           
657
+            # If parsing successful, convert the data.
658
+            if result:
659
+                result = convertData(ldData)
660
+
661
+            # If conversion successful, write data to data files.
662
+            if result:
663
+                result = updateDatabase(ldData)
664
+
665
+            if result:
666
+                writeOutputDataFile(sData, ldData)
667
+
668
+            # Set the node status to online or offline depending on the
669
+            # success or failure of the above operations.
670
+            setNodeStatus(result)
671
+
672
+
673
+        # At the chart generation interval, generate charts.
674
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
+            lastChartUpdateTime = currentTime
676
+            p = multiprocessing.Process(target=generateGraphs, args=())
677
+            p.start()
678
+
679
+        # Relinquish processing back to the operating system until
680
+        # the next update interval.
681
+
682
+        elapsedTime = time.time() - currentTime
683
+        if debugOption:
684
+            if result:
685
+                print "%s update successful:" % getTimeStamp(),
686
+            else:
687
+                print "%s update failed:" % getTimeStamp(),
688
+            print "%6f seconds processing time\n" % elapsedTime 
689
+        remainingTime = requestIntervalSeconds - elapsedTime
690
+        if remainingTime > 0.0:
691
+            time.sleep(remainingTime)
692
+    ## end while
693
+    return
694
+## end def
695
+
696
+if __name__ == '__main__':
697
+    try:
698
+        main()
699
+    except KeyboardInterrupt:
700
+        print '\n',
701
+        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

fix version numbers

gandolf authored on 03/31/2020 18:04:12
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,701 +0,0 @@
1
-#!/usr/bin/python -u
2
-# The -u option above turns off block buffering of python output. This 
3
-# assures that each error message gets individually printed to the log file.
4
-#
5
-# Module: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#
37
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
-
39
-import os
40
-import urllib2
41
-import sys
42
-import signal
43
-import subprocess
44
-import multiprocessing
45
-import time
46
-import json
47
-
48
-_USER = os.environ['USER']
49
-_HOSTNAME = os.uname()[1]
50
-
51
-   ### DEFAULT AREDN NODE URL ###
52
-
53
-# set url of the aredn node
54
-
55
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
-
57
-    ### FILE AND FOLDER LOCATIONS ###
58
-
59
-# folder for containing dynamic data objects
60
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
-# folder for charts and output data file
62
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
-# location of data output file
64
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
-# dummy output data file
66
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
-# database that stores node data
68
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
-
70
-    ### GLOBAL CONSTANTS ###
71
-
72
-# max number of failed data requests allowed
73
-_MAX_FAILED_DATA_REQUESTS = 0
74
-# interval in minutes between data requests to the aredn node
75
-_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
-# number seconds to wait for a response to HTTP request
77
-_HTTP_REQUEST_TIMEOUT = 10
78
-# standard chart width in pixels
79
-_CHART_WIDTH = 600
80
-# standard chart height in pixels
81
-_CHART_HEIGHT = 150
82
-# Set this to True only if this server is intended to relay raw
83
-# node data to a mirror server.
84
-_RELAY_SERVER = False
85
-
86
-   ### GLOBAL VARIABLES ###
87
-
88
-# turn on or off of verbose debugging information
89
-debugOption = False
90
-verboseDebug = False
91
-
92
-# The following two items are used for detecting system faults
93
-# and aredn node online or offline status.
94
-
95
-# count of failed attempts to get data from aredn node
96
-failedUpdateCount = 0
97
-# detected status of aredn node device
98
-nodeOnline = True
99
-
100
-# ip address of aredn node
101
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
-# frequency of data requests to aredn node
103
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
-# last node request time
105
-lastDataPointTime = -1
106
-
107
-  ###  PRIVATE METHODS  ###
108
-
109
-def getTimeStamp():
110
-    """
111
-    Set the error message time stamp to the local system time.
112
-    Parameters: none
113
-    Returns: string containing the time stamp
114
-    """
115
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
-##end def
117
-
118
-def getLastDataPointTime():
119
-    """
120
-    Get the timestamp of the most recent update to the round robin
121
-    database.
122
-    Parameters: none
123
-    Returns: string epoch time stamp of the last rrd update
124
-    """
125
-    strCmd = "rrdtool lastupdate %s" % \
126
-             (_RRD_FILE)
127
-
128
-    # Run the command as a subprocess.
129
-    try:
130
-        result = subprocess.check_output(strCmd, shell=True,  \
131
-                             stderr=subprocess.STDOUT)
132
-    except subprocess.CalledProcessError, exError:
133
-        print "%s: rrdtool update failed: %s" % \
134
-                    (getTimeStamp(), exError.output)
135
-        return None
136
-
137
-    # Return just the epoch time stamp of the last rrd update
138
-    return int((result.split('\n')[-2]).split(':')[0])
139
-##end def
140
-
141
-def setStatusToOffline():
142
-    """Set the detected status of the aredn node to
143
-       "offline" and inform downstream clients by removing input
144
-       and output data files.
145
-       Parameters: none
146
-       Returns: nothing
147
-    """
148
-    global nodeOnline
149
-
150
-    # Inform downstream clients by removing output data file.
151
-    if os.path.exists(_OUTPUT_DATA_FILE):
152
-       os.remove(_OUTPUT_DATA_FILE)
153
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
-       os.remove(_DUMMY_OUTPUT_FILE)
155
-    # If the aredn node was previously online, then send
156
-    # a message that we are now offline.
157
-    if nodeOnline:
158
-        print '%s aredn node offline' % getTimeStamp()
159
-    nodeOnline = False
160
-##end def
161
-
162
-def terminateAgentProcess(signal, frame):
163
-    """Send a message to log when the agent process gets killed
164
-       by the operating system.  Inform downstream clients
165
-       by removing input and output data files.
166
-       Parameters:
167
-           signal, frame - dummy parameters
168
-       Returns: nothing
169
-    """
170
-    # Inform downstream clients by removing output data file.
171
-    if os.path.exists(_OUTPUT_DATA_FILE):
172
-       os.remove(_OUTPUT_DATA_FILE)
173
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
-       os.remove(_DUMMY_OUTPUT_FILE)
175
-    print '%s terminating arednsig agent process' % \
176
-              (getTimeStamp())
177
-    sys.exit(0)
178
-##end def
179
-
180
-  ###  PUBLIC METHODS  ###
181
-
182
-def getArednNodeData():
183
-    """Send http request to aredn node.  The response from the
184
-       node contains the node signal data as unformatted ascii text.
185
-       Parameters: none
186
-       Returns: a string containing the node signal data if successful,
187
-                or None if not successful
188
-    """
189
-    try:
190
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
-
192
-        # Format received data into a single string.
193
-        content = ""
194
-        for line in conn:
195
-            content += line.strip()
196
-        del conn
197
-
198
-    except Exception, exError:
199
-        # If no response is received from the device, then assume that
200
-        # the device is down or unavailable over the network.  In
201
-        # that case return None to the calling function.
202
-        print "%s http error: %s" % (getTimeStamp(), exError)
203
-        return None
204
-
205
-    if verboseDebug:
206
-        print "http request successful: %d bytes" % len(content)
207
-
208
-    return content
209
-##end def
210
-
211
-def parseNodeData(sData, ldData):
212
-    """Parse the node  signal data JSON string from the aredn node
213
-       into its component parts.  
214
-       Parameters:
215
-           sData - the string containing the data to be parsed
216
-           dData - a dictionary object to contain the parsed data items
217
-       Returns: True if successful, False otherwise
218
-    """
219
-    iTrail = int(dataRequestInterval)
220
-
221
-    try:
222
-        ldTmp = json.loads(sData[1:-1])
223
-        ldTmp = ldTmp[-iTrail:]
224
-        if len(ldTmp) != iTrail:
225
-            #raise Exception("truncated list")
226
-            pass
227
-    except Exception, exError:
228
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
-        return False
230
-    
231
-    del ldData[:]
232
-    for item in ldTmp:
233
-        ldData.append(item)
234
-
235
-    if verboseDebug:
236
-        print "parse successful: %d data points" % len(ldData)
237
-    return True
238
-##end def
239
-
240
-def convertData(ldData):
241
-    """Convert individual node signal data items as necessary.
242
-       Parameters:
243
-           dData - a dictionary object containing the node signal data
244
-       Returns: True if successful, False otherwise
245
-    """
246
-    # parse example string
247
-    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
-    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
-    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
-    #
251
-    index = 0
252
-    while index < len(ldData):
253
-        item = ldData[index]
254
-        try:
255
-            item['time'] = int(item.pop('x')) / 1000
256
-            item['signal'] = int(item['y'][0])
257
-            item['noise'] = int(item['y'][1])
258
-            item['snr'] = int(item.pop('m'))
259
-            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
-            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
-            item['rx_rate'] = int(item.pop('rx_rate'))
262
-            item['tx_rate'] = int(item.pop('tx_rate'))
263
-            item.pop('y')
264
-            item.pop('label')
265
-        except Exception, exError:
266
-            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
-            print "discarding %s" % item
268
-            #return False
269
-            ldData.pop(index)
270
-        else:
271
-            index += 1
272
-    ##end for
273
-
274
-    if len(ldData) > 0:
275
-        if verboseDebug:
276
-            print "convert data successful"
277
-        return True
278
-    else:
279
-        print "convert data failed"
280
-        return False
281
-##end def
282
-
283
-def updateDatabase(ldData):
284
-    """
285
-    Update the rrdtool database by executing an rrdtool system command.
286
-    Format the command using the data extracted from the aredn node
287
-    response.   
288
-    Parameters: dData - dictionary object containing data items to be
289
-                        written to the rr database file
290
-    Returns: True if successful, False otherwise
291
-    """
292
-    updateCount = 0
293
-    lastDataPointTime = getLastDataPointTime()
294
-
295
-    if verboseDebug:
296
-         print "updating database..."
297
-
298
-    for item in ldData:
299
-
300
-        if item['time'] <= lastDataPointTime:
301
-            if verboseDebug:
302
-                print "%s invalid timestamp: discarding data" % \
303
-                      (getTimeStamp())
304
-            continue
305
-
306
-        # Format the rrdtool update command.
307
-        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
-        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
-                 item['noise'], item['snr'], item['rx_mcs'], \
310
-                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
-
312
-        if verboseDebug:
313
-            print "%s" % strCmd # DEBUG
314
-
315
-        # Run the command as a subprocess.
316
-        try:
317
-            subprocess.check_output(strCmd, shell=True,  \
318
-                                 stderr=subprocess.STDOUT)
319
-        except subprocess.CalledProcessError, exError:
320
-            print "%s: rrdtool update failed: %s" % \
321
-                        (getTimeStamp(), exError.output)
322
-            return False
323
-        updateCount += 1
324
-    ##end for
325
-
326
-    if debugOption:
327
-        print '%s added %d data points to database' % \
328
-              (getTimeStamp(), updateCount)
329
-    return True
330
-##end def
331
-
332
-def writeOutputDataFile(sData, ldData):
333
-    """Write node data items to the output data file, formatted as 
334
-       a Javascript file.  This file may then be accessed and used by
335
-       by downstream clients, for instance, in HTML documents.
336
-       Parameters:
337
-           sData - a string object containing the data to be written
338
-                   to the output data file
339
-       Returns: True if successful, False otherwise
340
-    """
341
-    # Write file for use by html clients.  The following two
342
-    # data items are sent to the client file.
343
-    #    * The last database update date and time
344
-    #    * The data request interval
345
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
-                                time.localtime(ldData[-1]['time']) )
347
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
-           (lastUpdate, dataRequestInterval)
349
-    try:
350
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
-        fc.write(sDate)
352
-        fc.close()
353
-    except Exception, exError:
354
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
-        return False
356
-
357
-    if _RELAY_SERVER:
358
-        # Write the entire node data response to the output data file.
359
-        try:
360
-            fc = open(_OUTPUT_DATA_FILE, "w")
361
-            fc.write(sData)
362
-            fc.close()
363
-        except Exception, exError:
364
-            print "%s write output file failed: %s" % \
365
-                  (getTimeStamp(), exError)
366
-            return False
367
-        if verboseDebug:
368
-            print "write output data file: %d bytes" % len(sData)
369
-
370
-    return True
371
-## end def
372
-
373
-def setNodeStatus(updateSuccess):
374
-    """Detect if aredn node is offline or not available on
375
-       the network. After a set number of attempts to get data
376
-       from the node set a flag that the node is offline.
377
-       Parameters:
378
-           updateSuccess - a boolean that is True if data request
379
-                           successful, False otherwise
380
-       Returns: nothing
381
-    """
382
-    global failedUpdateCount, nodeOnline
383
-
384
-    if updateSuccess:
385
-        failedUpdateCount = 0
386
-        # Set status and send a message to the log if the node was
387
-        # previously offline and is now online.
388
-        if not nodeOnline:
389
-            print '%s aredn node online' % getTimeStamp()
390
-            nodeOnline = True
391
-    else:
392
-        # The last attempt failed, so update the failed attempts
393
-        # count.
394
-        failedUpdateCount += 1
395
-
396
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
-        # Max number of failed data requests, so set
398
-        # node status to offline.
399
-        setStatusToOffline()
400
-##end def
401
-
402
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
-                lower, upper, addTrend, autoScale):
404
-    """Uses rrdtool to create a graph of specified node data item.
405
-       Parameters:
406
-           fileName - name of file containing the graph
407
-           dataItem - data item to be graphed
408
-           gLabel - string containing a graph label for the data item
409
-           gTitle - string containing a title for the graph
410
-           gStart - beginning time of the graphed data
411
-           lower - lower bound for graph ordinate #NOT USED
412
-           upper - upper bound for graph ordinate #NOT USED
413
-           addTrend - 0, show only graph data
414
-                      1, show only a trend line
415
-                      2, show a trend line and the graph data
416
-           autoScale - if True, then use vertical axis auto scaling
417
-               (lower and upper parameters are ignored), otherwise use
418
-               lower and upper parameters to set vertical axis scale
419
-       Returns: True if successful, False otherwise
420
-    """
421
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
-    trendWindow = { 'end-1day': 7200,
423
-                    'end-4weeks': 172800,
424
-                    'end-12months': 604800 }
425
- 
426
-    # Format the rrdtool graph command.
427
-
428
-    # Set chart start time, height, and width.
429
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
-   
432
-    # Set the range and scaling of the chart y-axis.
433
-    if lower < upper:
434
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
-    elif autoScale:
436
-        strCmd += "-A "
437
-    strCmd += "-Y "
438
-
439
-    # Set the chart ordinate label and chart title. 
440
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
- 
442
-    # Show the data, or a moving average trend line over
443
-    # the data, or both.
444
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
-    if addTrend == 0:
446
-        strCmd += "LINE1:dSeries#0400ff "
447
-    elif addTrend == 1:
448
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
-                  % trendWindow[gStart]
450
-    elif addTrend == 2:
451
-        strCmd += "LINE1:dSeries#0400ff "
452
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
-                  % trendWindow[gStart]
454
-     
455
-    if verboseDebug:
456
-        print "%s" % strCmd # DEBUG
457
-    
458
-    # Run the formatted rrdtool command as a subprocess.
459
-    try:
460
-        result = subprocess.check_output(strCmd, \
461
-                     stderr=subprocess.STDOUT,   \
462
-                     shell=True)
463
-    except subprocess.CalledProcessError, exError:
464
-        print "rrdtool graph failed: %s" % (exError.output)
465
-        return False
466
-
467
-    if debugOption:
468
-        print "rrdtool graph: %s\n" % result,
469
-    return True
470
-
471
-##end def
472
-
473
-def generateGraphs():
474
-    """Generate graphs for display in html documents.
475
-       Parameters: none
476
-       Returns: nothing
477
-    """
478
-    autoScale = False
479
-
480
-    # The following will force creation of charts
481
-    # of only signal strength and S/N charts.  Note that the following
482
-    # data items appear constant and do not show variation with time:
483
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
-    # these parameters are demonstrated to vary in time, there is no point
485
-    # in creating the charts for these data items.
486
-    createAllCharts = False
487
-
488
-    # 24 hour stock charts
489
-
490
-    createGraph('24hr_signal', 'S', 'dBm', 
491
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
-    createGraph('24hr_snr', 'SNR', 'dB', 
493
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
-
495
-    if createAllCharts:
496
-        createGraph('24hr_noise', 'N', 'dBm', 
497
-                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
-                    autoScale)
499
-        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
-                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
-                    autoScale)
502
-        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
-                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
-                    autoScale)
505
-        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
-                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
-                     autoScale)
508
-        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
-                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
-                     autoScale)
511
-
512
-    # 4 week stock charts
513
-
514
-    createGraph('4wk_signal', 'S', 'dBm', 
515
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
-    createGraph('4wk_snr', 'SNR', 'dB', 
517
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
-
519
-    if createAllCharts:
520
-        createGraph('4wk_noise', 'N', 'dBm', 
521
-                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
-                    autoScale)
523
-        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
-                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
-                    autoScale)
526
-        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
-                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
-                    autoScale)
529
-        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
-                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
-                    autoScale)
532
-        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
-                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
-                    autoScale)
535
-
536
-    # 12 month stock charts
537
-
538
-    createGraph('12m_signal', 'S', 'dBm', 
539
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
-    createGraph('12m_snr', 'SNR', 'dB', 
541
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
-
543
-    if createAllCharts:
544
-        createGraph('12m_noise', 'N', 'dBm', 
545
-                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
-                    autoScale)
547
-        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
-                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
-                    autoScale)
550
-        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
-                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
-                    autoScale)
553
-        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
-                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
-                    autoScale)
556
-        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
-                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
-                    autoScale)
559
-    if debugOption:
560
-        #print # print a blank line to improve readability when in debug mode
561
-        pass
562
-##end def
563
-
564
-def getCLarguments():
565
-    """Get command line arguments.  There are four possible arguments
566
-          -d turns on debug mode
567
-          -v turns on verbose debug mode
568
-          -t sets the aredn node query interval
569
-          -u sets the url of the aredn nodeing device
570
-       Returns: nothing
571
-    """
572
-    global debugOption, verboseDebug, dataRequestInterval, \
573
-           arednNodeUrl
574
-
575
-    index = 1
576
-    while index < len(sys.argv):
577
-        if sys.argv[index] == '-d':
578
-            debugOption = True
579
-        elif sys.argv[index] == '-v':
580
-            debugOption = True
581
-            verboseDebug = True
582
-        elif sys.argv[index] == '-p':
583
-            try:
584
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
-            except:
586
-                print "invalid polling period"
587
-                exit(-1)
588
-            index += 1
589
-        elif sys.argv[index] == '-u':
590
-            arednNodeUrl = sys.argv[index + 1]
591
-            index += 1
592
-        else:
593
-            cmd_name = sys.argv[0].split('/')
594
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
-            exit(-1)
596
-        index += 1
597
-##end def
598
-
599
-def main():
600
-    """Handles timing of events and acts as executive routine managing
601
-       all other functions.
602
-       Parameters: none
603
-       Returns: nothing
604
-    """
605
-    global dataRequestInterval
606
-
607
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
-
609
-    print '%s starting up arednsig agent process' % \
610
-                  (getTimeStamp())
611
-
612
-    # last time output JSON file updated
613
-    lastDataRequestTime = -1
614
-    # last time charts generated
615
-    lastChartUpdateTime = - 1
616
-    # last time the rrdtool database updated
617
-    lastDatabaseUpdateTime = -1
618
-
619
-    ## Get command line arguments.
620
-    getCLarguments()
621
-
622
-    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
-    chartUpdateInterval = dataRequestInterval # get charts interval
624
-
625
-    ## Exit with error if rrdtool database does not exist.
626
-    if not os.path.exists(_RRD_FILE):
627
-        print 'rrdtool database does not exist\n' \
628
-              'use createArednsigRrd script to ' \
629
-              'create rrdtool database\n'
630
-        exit(1)
631
- 
632
-    ## main loop
633
-    while True:
634
-
635
-        currentTime = time.time() # get current time in seconds
636
-
637
-        # Every web update interval request data from the aredn
638
-        # node and process the received data.
639
-        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
-            lastDataRequestTime = currentTime
641
-            ldData = []
642
-            result = True
643
-
644
-            # Get the data string from the device.
645
-            sData = getArednNodeData()
646
-            # If the first http request fails, try one more time.
647
-            if sData == None:
648
-                time.sleep(5)
649
-                sData = getArednNodeData()
650
-                if sData == None:
651
-                    result = False
652
-
653
-            # If successful parse the data.
654
-            if result:
655
-                result = parseNodeData(sData, ldData)
656
-           
657
-            # If parsing successful, convert the data.
658
-            if result:
659
-                result = convertData(ldData)
660
-
661
-            # If conversion successful, write data to data files.
662
-            if result:
663
-                result = updateDatabase(ldData)
664
-
665
-            if result:
666
-                writeOutputDataFile(sData, ldData)
667
-
668
-            # Set the node status to online or offline depending on the
669
-            # success or failure of the above operations.
670
-            setNodeStatus(result)
671
-
672
-
673
-        # At the chart generation interval, generate charts.
674
-        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
-            lastChartUpdateTime = currentTime
676
-            p = multiprocessing.Process(target=generateGraphs, args=())
677
-            p.start()
678
-
679
-        # Relinquish processing back to the operating system until
680
-        # the next update interval.
681
-
682
-        elapsedTime = time.time() - currentTime
683
-        if debugOption:
684
-            if result:
685
-                print "%s update successful:" % getTimeStamp(),
686
-            else:
687
-                print "%s update failed:" % getTimeStamp(),
688
-            print "%6f seconds processing time\n" % elapsedTime 
689
-        remainingTime = requestIntervalSeconds - elapsedTime
690
-        if remainingTime > 0.0:
691
-            time.sleep(remainingTime)
692
-    ## end while
693
-    return
694
-## end def
695
-
696
-if __name__ == '__main__':
697
-    try:
698
-        main()
699
-    except KeyboardInterrupt:
700
-        print '\n',
701
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

support for Aredn firmware version 3.19.3.0

gandolf authored on 03/31/2020 18:00:53
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,701 @@
1
+#!/usr/bin/python -u
2
+# The -u option above turns off block buffering of python output. This 
3
+# assures that each error message gets individually printed to the log file.
4
+#
5
+# Module: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+import json
47
+
48
+_USER = os.environ['USER']
49
+_HOSTNAME = os.uname()[1]
50
+
51
+   ### DEFAULT AREDN NODE URL ###
52
+
53
+# set url of the aredn node
54
+
55
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
+
57
+    ### FILE AND FOLDER LOCATIONS ###
58
+
59
+# folder for containing dynamic data objects
60
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
+# folder for charts and output data file
62
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
+# location of data output file
64
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
+# dummy output data file
66
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
+# database that stores node data
68
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
+
70
+    ### GLOBAL CONSTANTS ###
71
+
72
+# max number of failed data requests allowed
73
+_MAX_FAILED_DATA_REQUESTS = 0
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# standard chart width in pixels
79
+_CHART_WIDTH = 600
80
+# standard chart height in pixels
81
+_CHART_HEIGHT = 150
82
+# Set this to True only if this server is intended to relay raw
83
+# node data to a mirror server.
84
+_RELAY_SERVER = False
85
+
86
+   ### GLOBAL VARIABLES ###
87
+
88
+# turn on or off of verbose debugging information
89
+debugOption = False
90
+verboseDebug = False
91
+
92
+# The following two items are used for detecting system faults
93
+# and aredn node online or offline status.
94
+
95
+# count of failed attempts to get data from aredn node
96
+failedUpdateCount = 0
97
+# detected status of aredn node device
98
+nodeOnline = True
99
+
100
+# ip address of aredn node
101
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
+# frequency of data requests to aredn node
103
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
+# last node request time
105
+lastDataPointTime = -1
106
+
107
+  ###  PRIVATE METHODS  ###
108
+
109
+def getTimeStamp():
110
+    """
111
+    Set the error message time stamp to the local system time.
112
+    Parameters: none
113
+    Returns: string containing the time stamp
114
+    """
115
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
+##end def
117
+
118
+def getLastDataPointTime():
119
+    """
120
+    Get the timestamp of the most recent update to the round robin
121
+    database.
122
+    Parameters: none
123
+    Returns: string epoch time stamp of the last rrd update
124
+    """
125
+    strCmd = "rrdtool lastupdate %s" % \
126
+             (_RRD_FILE)
127
+
128
+    # Run the command as a subprocess.
129
+    try:
130
+        result = subprocess.check_output(strCmd, shell=True,  \
131
+                             stderr=subprocess.STDOUT)
132
+    except subprocess.CalledProcessError, exError:
133
+        print "%s: rrdtool update failed: %s" % \
134
+                    (getTimeStamp(), exError.output)
135
+        return None
136
+
137
+    # Return just the epoch time stamp of the last rrd update
138
+    return int((result.split('\n')[-2]).split(':')[0])
139
+##end def
140
+
141
+def setStatusToOffline():
142
+    """Set the detected status of the aredn node to
143
+       "offline" and inform downstream clients by removing input
144
+       and output data files.
145
+       Parameters: none
146
+       Returns: nothing
147
+    """
148
+    global nodeOnline
149
+
150
+    # Inform downstream clients by removing output data file.
151
+    if os.path.exists(_OUTPUT_DATA_FILE):
152
+       os.remove(_OUTPUT_DATA_FILE)
153
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
+       os.remove(_DUMMY_OUTPUT_FILE)
155
+    # If the aredn node was previously online, then send
156
+    # a message that we are now offline.
157
+    if nodeOnline:
158
+        print '%s aredn node offline' % getTimeStamp()
159
+    nodeOnline = False
160
+##end def
161
+
162
+def terminateAgentProcess(signal, frame):
163
+    """Send a message to log when the agent process gets killed
164
+       by the operating system.  Inform downstream clients
165
+       by removing input and output data files.
166
+       Parameters:
167
+           signal, frame - dummy parameters
168
+       Returns: nothing
169
+    """
170
+    # Inform downstream clients by removing output data file.
171
+    if os.path.exists(_OUTPUT_DATA_FILE):
172
+       os.remove(_OUTPUT_DATA_FILE)
173
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
+       os.remove(_DUMMY_OUTPUT_FILE)
175
+    print '%s terminating arednsig agent process' % \
176
+              (getTimeStamp())
177
+    sys.exit(0)
178
+##end def
179
+
180
+  ###  PUBLIC METHODS  ###
181
+
182
+def getArednNodeData():
183
+    """Send http request to aredn node.  The response from the
184
+       node contains the node signal data as unformatted ascii text.
185
+       Parameters: none
186
+       Returns: a string containing the node signal data if successful,
187
+                or None if not successful
188
+    """
189
+    try:
190
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
+
192
+        # Format received data into a single string.
193
+        content = ""
194
+        for line in conn:
195
+            content += line.strip()
196
+        del conn
197
+
198
+    except Exception, exError:
199
+        # If no response is received from the device, then assume that
200
+        # the device is down or unavailable over the network.  In
201
+        # that case return None to the calling function.
202
+        print "%s http error: %s" % (getTimeStamp(), exError)
203
+        return None
204
+
205
+    if verboseDebug:
206
+        print "http request successful: %d bytes" % len(content)
207
+
208
+    return content
209
+##end def
210
+
211
+def parseNodeData(sData, ldData):
212
+    """Parse the node  signal data JSON string from the aredn node
213
+       into its component parts.  
214
+       Parameters:
215
+           sData - the string containing the data to be parsed
216
+           dData - a dictionary object to contain the parsed data items
217
+       Returns: True if successful, False otherwise
218
+    """
219
+    iTrail = int(dataRequestInterval)
220
+
221
+    try:
222
+        ldTmp = json.loads(sData[1:-1])
223
+        ldTmp = ldTmp[-iTrail:]
224
+        if len(ldTmp) != iTrail:
225
+            #raise Exception("truncated list")
226
+            pass
227
+    except Exception, exError:
228
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
+        return False
230
+    
231
+    del ldData[:]
232
+    for item in ldTmp:
233
+        ldData.append(item)
234
+
235
+    if verboseDebug:
236
+        print "parse successful: %d data points" % len(ldData)
237
+    return True
238
+##end def
239
+
240
+def convertData(ldData):
241
+    """Convert individual node signal data items as necessary.
242
+       Parameters:
243
+           dData - a dictionary object containing the node signal data
244
+       Returns: True if successful, False otherwise
245
+    """
246
+    # parse example string
247
+    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
+    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
+    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
+    #
251
+    index = 0
252
+    while index < len(ldData):
253
+        item = ldData[index]
254
+        try:
255
+            item['time'] = int(item.pop('x')) / 1000
256
+            item['signal'] = int(item['y'][0])
257
+            item['noise'] = int(item['y'][1])
258
+            item['snr'] = int(item.pop('m'))
259
+            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
+            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
+            item['rx_rate'] = int(item.pop('rx_rate'))
262
+            item['tx_rate'] = int(item.pop('tx_rate'))
263
+            item.pop('y')
264
+            item.pop('label')
265
+        except Exception, exError:
266
+            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
+            print "discarding %s" % item
268
+            #return False
269
+            ldData.pop(index)
270
+        else:
271
+            index += 1
272
+    ##end for
273
+
274
+    if len(ldData) > 0:
275
+        if verboseDebug:
276
+            print "convert data successful"
277
+        return True
278
+    else:
279
+        print "convert data failed"
280
+        return False
281
+##end def
282
+
283
+def updateDatabase(ldData):
284
+    """
285
+    Update the rrdtool database by executing an rrdtool system command.
286
+    Format the command using the data extracted from the aredn node
287
+    response.   
288
+    Parameters: dData - dictionary object containing data items to be
289
+                        written to the rr database file
290
+    Returns: True if successful, False otherwise
291
+    """
292
+    updateCount = 0
293
+    lastDataPointTime = getLastDataPointTime()
294
+
295
+    if verboseDebug:
296
+         print "updating database..."
297
+
298
+    for item in ldData:
299
+
300
+        if item['time'] <= lastDataPointTime:
301
+            if verboseDebug:
302
+                print "%s invalid timestamp: discarding data" % \
303
+                      (getTimeStamp())
304
+            continue
305
+
306
+        # Format the rrdtool update command.
307
+        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
+        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
+                 item['noise'], item['snr'], item['rx_mcs'], \
310
+                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
+
312
+        if verboseDebug:
313
+            print "%s" % strCmd # DEBUG
314
+
315
+        # Run the command as a subprocess.
316
+        try:
317
+            subprocess.check_output(strCmd, shell=True,  \
318
+                                 stderr=subprocess.STDOUT)
319
+        except subprocess.CalledProcessError, exError:
320
+            print "%s: rrdtool update failed: %s" % \
321
+                        (getTimeStamp(), exError.output)
322
+            return False
323
+        updateCount += 1
324
+    ##end for
325
+
326
+    if debugOption:
327
+        print '%s added %d data points to database' % \
328
+              (getTimeStamp(), updateCount)
329
+    return True
330
+##end def
331
+
332
+def writeOutputDataFile(sData, ldData):
333
+    """Write node data items to the output data file, formatted as 
334
+       a Javascript file.  This file may then be accessed and used by
335
+       by downstream clients, for instance, in HTML documents.
336
+       Parameters:
337
+           sData - a string object containing the data to be written
338
+                   to the output data file
339
+       Returns: True if successful, False otherwise
340
+    """
341
+    # Write file for use by html clients.  The following two
342
+    # data items are sent to the client file.
343
+    #    * The last database update date and time
344
+    #    * The data request interval
345
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
+                                time.localtime(ldData[-1]['time']) )
347
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
+           (lastUpdate, dataRequestInterval)
349
+    try:
350
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
+        fc.write(sDate)
352
+        fc.close()
353
+    except Exception, exError:
354
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
+        return False
356
+
357
+    if _RELAY_SERVER:
358
+        # Write the entire node data response to the output data file.
359
+        try:
360
+            fc = open(_OUTPUT_DATA_FILE, "w")
361
+            fc.write(sData)
362
+            fc.close()
363
+        except Exception, exError:
364
+            print "%s write output file failed: %s" % \
365
+                  (getTimeStamp(), exError)
366
+            return False
367
+        if verboseDebug:
368
+            print "write output data file: %d bytes" % len(sData)
369
+
370
+    return True
371
+## end def
372
+
373
+def setNodeStatus(updateSuccess):
374
+    """Detect if aredn node is offline or not available on
375
+       the network. After a set number of attempts to get data
376
+       from the node set a flag that the node is offline.
377
+       Parameters:
378
+           updateSuccess - a boolean that is True if data request
379
+                           successful, False otherwise
380
+       Returns: nothing
381
+    """
382
+    global failedUpdateCount, nodeOnline
383
+
384
+    if updateSuccess:
385
+        failedUpdateCount = 0
386
+        # Set status and send a message to the log if the node was
387
+        # previously offline and is now online.
388
+        if not nodeOnline:
389
+            print '%s aredn node online' % getTimeStamp()
390
+            nodeOnline = True
391
+    else:
392
+        # The last attempt failed, so update the failed attempts
393
+        # count.
394
+        failedUpdateCount += 1
395
+
396
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
+        # Max number of failed data requests, so set
398
+        # node status to offline.
399
+        setStatusToOffline()
400
+##end def
401
+
402
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
+                lower, upper, addTrend, autoScale):
404
+    """Uses rrdtool to create a graph of specified node data item.
405
+       Parameters:
406
+           fileName - name of file containing the graph
407
+           dataItem - data item to be graphed
408
+           gLabel - string containing a graph label for the data item
409
+           gTitle - string containing a title for the graph
410
+           gStart - beginning time of the graphed data
411
+           lower - lower bound for graph ordinate #NOT USED
412
+           upper - upper bound for graph ordinate #NOT USED
413
+           addTrend - 0, show only graph data
414
+                      1, show only a trend line
415
+                      2, show a trend line and the graph data
416
+           autoScale - if True, then use vertical axis auto scaling
417
+               (lower and upper parameters are ignored), otherwise use
418
+               lower and upper parameters to set vertical axis scale
419
+       Returns: True if successful, False otherwise
420
+    """
421
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
+    trendWindow = { 'end-1day': 7200,
423
+                    'end-4weeks': 172800,
424
+                    'end-12months': 604800 }
425
+ 
426
+    # Format the rrdtool graph command.
427
+
428
+    # Set chart start time, height, and width.
429
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
+   
432
+    # Set the range and scaling of the chart y-axis.
433
+    if lower < upper:
434
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
+    elif autoScale:
436
+        strCmd += "-A "
437
+    strCmd += "-Y "
438
+
439
+    # Set the chart ordinate label and chart title. 
440
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
+ 
442
+    # Show the data, or a moving average trend line over
443
+    # the data, or both.
444
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
+    if addTrend == 0:
446
+        strCmd += "LINE1:dSeries#0400ff "
447
+    elif addTrend == 1:
448
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
+                  % trendWindow[gStart]
450
+    elif addTrend == 2:
451
+        strCmd += "LINE1:dSeries#0400ff "
452
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
+                  % trendWindow[gStart]
454
+     
455
+    if verboseDebug:
456
+        print "%s" % strCmd # DEBUG
457
+    
458
+    # Run the formatted rrdtool command as a subprocess.
459
+    try:
460
+        result = subprocess.check_output(strCmd, \
461
+                     stderr=subprocess.STDOUT,   \
462
+                     shell=True)
463
+    except subprocess.CalledProcessError, exError:
464
+        print "rrdtool graph failed: %s" % (exError.output)
465
+        return False
466
+
467
+    if debugOption:
468
+        print "rrdtool graph: %s\n" % result,
469
+    return True
470
+
471
+##end def
472
+
473
+def generateGraphs():
474
+    """Generate graphs for display in html documents.
475
+       Parameters: none
476
+       Returns: nothing
477
+    """
478
+    autoScale = False
479
+
480
+    # The following will force creation of charts
481
+    # of only signal strength and S/N charts.  Note that the following
482
+    # data items appear constant and do not show variation with time:
483
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
+    # these parameters are demonstrated to vary in time, there is no point
485
+    # in creating the charts for these data items.
486
+    createAllCharts = False
487
+
488
+    # 24 hour stock charts
489
+
490
+    createGraph('24hr_signal', 'S', 'dBm', 
491
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
+    createGraph('24hr_snr', 'SNR', 'dB', 
493
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
+
495
+    if createAllCharts:
496
+        createGraph('24hr_noise', 'N', 'dBm', 
497
+                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
+                    autoScale)
499
+        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
+                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
+                    autoScale)
502
+        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
+                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
+                    autoScale)
505
+        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
+                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
+                     autoScale)
508
+        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
+                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
+                     autoScale)
511
+
512
+    # 4 week stock charts
513
+
514
+    createGraph('4wk_signal', 'S', 'dBm', 
515
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
+    createGraph('4wk_snr', 'SNR', 'dB', 
517
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
+
519
+    if createAllCharts:
520
+        createGraph('4wk_noise', 'N', 'dBm', 
521
+                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
+                    autoScale)
523
+        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
+                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
+                    autoScale)
526
+        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
+                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
+                    autoScale)
529
+        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
+                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
+                    autoScale)
532
+        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
+                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
+                    autoScale)
535
+
536
+    # 12 month stock charts
537
+
538
+    createGraph('12m_signal', 'S', 'dBm', 
539
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
+    createGraph('12m_snr', 'SNR', 'dB', 
541
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
+
543
+    if createAllCharts:
544
+        createGraph('12m_noise', 'N', 'dBm', 
545
+                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
+                    autoScale)
547
+        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
+                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
+                    autoScale)
550
+        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
+                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
+                    autoScale)
553
+        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
+                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
+                    autoScale)
556
+        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
+                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
+                    autoScale)
559
+    if debugOption:
560
+        #print # print a blank line to improve readability when in debug mode
561
+        pass
562
+##end def
563
+
564
+def getCLarguments():
565
+    """Get command line arguments.  There are four possible arguments
566
+          -d turns on debug mode
567
+          -v turns on verbose debug mode
568
+          -t sets the aredn node query interval
569
+          -u sets the url of the aredn nodeing device
570
+       Returns: nothing
571
+    """
572
+    global debugOption, verboseDebug, dataRequestInterval, \
573
+           arednNodeUrl
574
+
575
+    index = 1
576
+    while index < len(sys.argv):
577
+        if sys.argv[index] == '-d':
578
+            debugOption = True
579
+        elif sys.argv[index] == '-v':
580
+            debugOption = True
581
+            verboseDebug = True
582
+        elif sys.argv[index] == '-p':
583
+            try:
584
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
+            except:
586
+                print "invalid polling period"
587
+                exit(-1)
588
+            index += 1
589
+        elif sys.argv[index] == '-u':
590
+            arednNodeUrl = sys.argv[index + 1]
591
+            index += 1
592
+        else:
593
+            cmd_name = sys.argv[0].split('/')
594
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
+            exit(-1)
596
+        index += 1
597
+##end def
598
+
599
+def main():
600
+    """Handles timing of events and acts as executive routine managing
601
+       all other functions.
602
+       Parameters: none
603
+       Returns: nothing
604
+    """
605
+    global dataRequestInterval
606
+
607
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
+
609
+    print '%s starting up arednsig agent process' % \
610
+                  (getTimeStamp())
611
+
612
+    # last time output JSON file updated
613
+    lastDataRequestTime = -1
614
+    # last time charts generated
615
+    lastChartUpdateTime = - 1
616
+    # last time the rrdtool database updated
617
+    lastDatabaseUpdateTime = -1
618
+
619
+    ## Get command line arguments.
620
+    getCLarguments()
621
+
622
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
+    chartUpdateInterval = dataRequestInterval # get charts interval
624
+
625
+    ## Exit with error if rrdtool database does not exist.
626
+    if not os.path.exists(_RRD_FILE):
627
+        print 'rrdtool database does not exist\n' \
628
+              'use createArednsigRrd script to ' \
629
+              'create rrdtool database\n'
630
+        exit(1)
631
+ 
632
+    ## main loop
633
+    while True:
634
+
635
+        currentTime = time.time() # get current time in seconds
636
+
637
+        # Every web update interval request data from the aredn
638
+        # node and process the received data.
639
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
+            lastDataRequestTime = currentTime
641
+            ldData = []
642
+            result = True
643
+
644
+            # Get the data string from the device.
645
+            sData = getArednNodeData()
646
+            # If the first http request fails, try one more time.
647
+            if sData == None:
648
+                time.sleep(5)
649
+                sData = getArednNodeData()
650
+                if sData == None:
651
+                    result = False
652
+
653
+            # If successful parse the data.
654
+            if result:
655
+                result = parseNodeData(sData, ldData)
656
+           
657
+            # If parsing successful, convert the data.
658
+            if result:
659
+                result = convertData(ldData)
660
+
661
+            # If conversion successful, write data to data files.
662
+            if result:
663
+                result = updateDatabase(ldData)
664
+
665
+            if result:
666
+                writeOutputDataFile(sData, ldData)
667
+
668
+            # Set the node status to online or offline depending on the
669
+            # success or failure of the above operations.
670
+            setNodeStatus(result)
671
+
672
+
673
+        # At the chart generation interval, generate charts.
674
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
+            lastChartUpdateTime = currentTime
676
+            p = multiprocessing.Process(target=generateGraphs, args=())
677
+            p.start()
678
+
679
+        # Relinquish processing back to the operating system until
680
+        # the next update interval.
681
+
682
+        elapsedTime = time.time() - currentTime
683
+        if debugOption:
684
+            if result:
685
+                print "%s update successful:" % getTimeStamp(),
686
+            else:
687
+                print "%s update failed:" % getTimeStamp(),
688
+            print "%6f seconds processing time\n" % elapsedTime 
689
+        remainingTime = requestIntervalSeconds - elapsedTime
690
+        if remainingTime > 0.0:
691
+            time.sleep(remainingTime)
692
+    ## end while
693
+    return
694
+## end def
695
+
696
+if __name__ == '__main__':
697
+    try:
698
+        main()
699
+    except KeyboardInterrupt:
700
+        print '\n',
701
+        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

fix version number

gandolf authored on 03/31/2020 17:51:53
Showing 1 changed files
1 1
deleted file mode 100755
... ...
@@ -1,701 +0,0 @@
1
-#!/usr/bin/python -u
2
-# The -u option above turns off block buffering of python output. This 
3
-# assures that each error message gets individually printed to the log file.
4
-#
5
-# Module: arednsigAgent.py
6
-#
7
-# Description: This module acts as an agent between the aredn node
8
-# and aredn mest services.  The agent periodically sends an http
9
-# request to the aredn node, processes the response from
10
-# the node, and performs a number of operations:
11
-#     - conversion of data items
12
-#     - update a round robin (rrdtool) database with the node data
13
-#     - periodically generate graphic charts for display in html documents
14
-#     - write the processed node status to a JSON file for use by html
15
-#       documents
16
-#
17
-# Copyright 2020 Jeff Owrey
18
-#    This program is free software: you can redistribute it and/or modify
19
-#    it under the terms of the GNU General Public License as published by
20
-#    the Free Software Foundation, either version 3 of the License, or
21
-#    (at your option) any later version.
22
-#
23
-#    This program is distributed in the hope that it will be useful,
24
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
-#    GNU General Public License for more details.
27
-#
28
-#    You should have received a copy of the GNU General Public License
29
-#    along with this program.  If not, see http://www.gnu.org/license.
30
-#
31
-# Revision History
32
-#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
-#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
-#     powers on and signal data memory is empty.  Data points with N/A data
35
-#     are discarded.
36
-#
37
-#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
-
39
-import os
40
-import urllib2
41
-import sys
42
-import signal
43
-import subprocess
44
-import multiprocessing
45
-import time
46
-import json
47
-
48
-_USER = os.environ['USER']
49
-_HOSTNAME = os.uname()[1]
50
-
51
-   ### DEFAULT AREDN NODE URL ###
52
-
53
-# set url of the aredn node
54
-
55
-_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
-
57
-    ### FILE AND FOLDER LOCATIONS ###
58
-
59
-# folder for containing dynamic data objects
60
-_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
-# folder for charts and output data file
62
-_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
-# location of data output file
64
-_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
-# dummy output data file
66
-_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
-# database that stores node data
68
-_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
-
70
-    ### GLOBAL CONSTANTS ###
71
-
72
-# max number of failed data requests allowed
73
-_MAX_FAILED_DATA_REQUESTS = 0
74
-# interval in minutes between data requests to the aredn node
75
-_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
-# number seconds to wait for a response to HTTP request
77
-_HTTP_REQUEST_TIMEOUT = 10
78
-# standard chart width in pixels
79
-_CHART_WIDTH = 600
80
-# standard chart height in pixels
81
-_CHART_HEIGHT = 150
82
-# Set this to True only if this server is intended to relay raw
83
-# node data to a mirror server.
84
-_RELAY_SERVER = False
85
-
86
-   ### GLOBAL VARIABLES ###
87
-
88
-# turn on or off of verbose debugging information
89
-debugOption = False
90
-verboseDebug = False
91
-
92
-# The following two items are used for detecting system faults
93
-# and aredn node online or offline status.
94
-
95
-# count of failed attempts to get data from aredn node
96
-failedUpdateCount = 0
97
-# detected status of aredn node device
98
-nodeOnline = True
99
-
100
-# ip address of aredn node
101
-arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
-# frequency of data requests to aredn node
103
-dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
-# last node request time
105
-lastDataPointTime = -1
106
-
107
-  ###  PRIVATE METHODS  ###
108
-
109
-def getTimeStamp():
110
-    """
111
-    Set the error message time stamp to the local system time.
112
-    Parameters: none
113
-    Returns: string containing the time stamp
114
-    """
115
-    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
-##end def
117
-
118
-def getLastDataPointTime():
119
-    """
120
-    Get the timestamp of the most recent update to the round robin
121
-    database.
122
-    Parameters: none
123
-    Returns: string epoch time stamp of the last rrd update
124
-    """
125
-    strCmd = "rrdtool lastupdate %s" % \
126
-             (_RRD_FILE)
127
-
128
-    # Run the command as a subprocess.
129
-    try:
130
-        result = subprocess.check_output(strCmd, shell=True,  \
131
-                             stderr=subprocess.STDOUT)
132
-    except subprocess.CalledProcessError, exError:
133
-        print "%s: rrdtool update failed: %s" % \
134
-                    (getTimeStamp(), exError.output)
135
-        return None
136
-
137
-    # Return just the epoch time stamp of the last rrd update
138
-    return int((result.split('\n')[-2]).split(':')[0])
139
-##end def
140
-
141
-def setStatusToOffline():
142
-    """Set the detected status of the aredn node to
143
-       "offline" and inform downstream clients by removing input
144
-       and output data files.
145
-       Parameters: none
146
-       Returns: nothing
147
-    """
148
-    global nodeOnline
149
-
150
-    # Inform downstream clients by removing output data file.
151
-    if os.path.exists(_OUTPUT_DATA_FILE):
152
-       os.remove(_OUTPUT_DATA_FILE)
153
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
-       os.remove(_DUMMY_OUTPUT_FILE)
155
-    # If the aredn node was previously online, then send
156
-    # a message that we are now offline.
157
-    if nodeOnline:
158
-        print '%s aredn node offline' % getTimeStamp()
159
-    nodeOnline = False
160
-##end def
161
-
162
-def terminateAgentProcess(signal, frame):
163
-    """Send a message to log when the agent process gets killed
164
-       by the operating system.  Inform downstream clients
165
-       by removing input and output data files.
166
-       Parameters:
167
-           signal, frame - dummy parameters
168
-       Returns: nothing
169
-    """
170
-    # Inform downstream clients by removing output data file.
171
-    if os.path.exists(_OUTPUT_DATA_FILE):
172
-       os.remove(_OUTPUT_DATA_FILE)
173
-    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
-       os.remove(_DUMMY_OUTPUT_FILE)
175
-    print '%s terminating arednsig agent process' % \
176
-              (getTimeStamp())
177
-    sys.exit(0)
178
-##end def
179
-
180
-  ###  PUBLIC METHODS  ###
181
-
182
-def getArednNodeData():
183
-    """Send http request to aredn node.  The response from the
184
-       node contains the node signal data as unformatted ascii text.
185
-       Parameters: none
186
-       Returns: a string containing the node signal data if successful,
187
-                or None if not successful
188
-    """
189
-    try:
190
-        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
-
192
-        # Format received data into a single string.
193
-        content = ""
194
-        for line in conn:
195
-            content += line.strip()
196
-        del conn
197
-
198
-    except Exception, exError:
199
-        # If no response is received from the device, then assume that
200
-        # the device is down or unavailable over the network.  In
201
-        # that case return None to the calling function.
202
-        print "%s http error: %s" % (getTimeStamp(), exError)
203
-        return None
204
-
205
-    if verboseDebug:
206
-        print "http request successful: %d bytes" % len(content)
207
-
208
-    return content
209
-##end def
210
-
211
-def parseNodeData(sData, ldData):
212
-    """Parse the node  signal data JSON string from the aredn node
213
-       into its component parts.  
214
-       Parameters:
215
-           sData - the string containing the data to be parsed
216
-           dData - a dictionary object to contain the parsed data items
217
-       Returns: True if successful, False otherwise
218
-    """
219
-    iTrail = int(dataRequestInterval)
220
-
221
-    try:
222
-        ldTmp = json.loads(sData[1:-1])
223
-        ldTmp = ldTmp[-iTrail:]
224
-        if len(ldTmp) != iTrail:
225
-            #raise Exception("truncated list")
226
-            pass
227
-    except Exception, exError:
228
-        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
-        return False
230
-    
231
-    del ldData[:]
232
-    for item in ldTmp:
233
-        ldData.append(item)
234
-
235
-    if verboseDebug:
236
-        print "parse successful: %d data points" % len(ldData)
237
-    return True
238
-##end def
239
-
240
-def convertData(ldData):
241
-    """Convert individual node signal data items as necessary.
242
-       Parameters:
243
-           dData - a dictionary object containing the node signal data
244
-       Returns: True if successful, False otherwise
245
-    """
246
-    # parse example string
247
-    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
-    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
-    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
-    #
251
-    index = 0
252
-    while index < len(ldData):
253
-        item = ldData[index]
254
-        try:
255
-            item['time'] = int(item.pop('x')) / 1000
256
-            item['signal'] = int(item['y'][0])
257
-            item['noise'] = int(item['y'][1])
258
-            item['snr'] = int(item.pop('m'))
259
-            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
-            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
-            item['rx_rate'] = int(item.pop('rx_rate'))
262
-            item['tx_rate'] = int(item.pop('tx_rate'))
263
-            item.pop('y')
264
-            item.pop('label')
265
-        except Exception, exError:
266
-            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
-            print "discarding %s" % item
268
-            #return False
269
-            ldData.pop(index)
270
-        else:
271
-            index += 1
272
-    ##end for
273
-
274
-    if len(ldData) > 0:
275
-        if verboseDebug:
276
-            print "convert data successful"
277
-        return True
278
-    else:
279
-        print "convert data failed"
280
-        return False
281
-##end def
282
-
283
-def updateDatabase(ldData):
284
-    """
285
-    Update the rrdtool database by executing an rrdtool system command.
286
-    Format the command using the data extracted from the aredn node
287
-    response.   
288
-    Parameters: dData - dictionary object containing data items to be
289
-                        written to the rr database file
290
-    Returns: True if successful, False otherwise
291
-    """
292
-    updateCount = 0
293
-    lastDataPointTime = getLastDataPointTime()
294
-
295
-    if verboseDebug:
296
-         print "updating database..."
297
-
298
-    for item in ldData:
299
-
300
-        if item['time'] <= lastDataPointTime:
301
-            if verboseDebug:
302
-                print "%s invalid timestamp: discarding data" % \
303
-                      (getTimeStamp())
304
-            continue
305
-
306
-        # Format the rrdtool update command.
307
-        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
-        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
-                 item['noise'], item['snr'], item['rx_mcs'], \
310
-                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
-
312
-        if verboseDebug:
313
-            print "%s" % strCmd # DEBUG
314
-
315
-        # Run the command as a subprocess.
316
-        try:
317
-            subprocess.check_output(strCmd, shell=True,  \
318
-                                 stderr=subprocess.STDOUT)
319
-        except subprocess.CalledProcessError, exError:
320
-            print "%s: rrdtool update failed: %s" % \
321
-                        (getTimeStamp(), exError.output)
322
-            return False
323
-        updateCount += 1
324
-    ##end for
325
-
326
-    if debugOption:
327
-        print '%s added %d data points to database' % \
328
-              (getTimeStamp(), updateCount)
329
-    return True
330
-##end def
331
-
332
-def writeOutputDataFile(sData, ldData):
333
-    """Write node data items to the output data file, formatted as 
334
-       a Javascript file.  This file may then be accessed and used by
335
-       by downstream clients, for instance, in HTML documents.
336
-       Parameters:
337
-           sData - a string object containing the data to be written
338
-                   to the output data file
339
-       Returns: True if successful, False otherwise
340
-    """
341
-    # Write file for use by html clients.  The following two
342
-    # data items are sent to the client file.
343
-    #    * The last database update date and time
344
-    #    * The data request interval
345
-    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
-                                time.localtime(ldData[-1]['time']) )
347
-    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
-           (lastUpdate, dataRequestInterval)
349
-    try:
350
-        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
-        fc.write(sDate)
352
-        fc.close()
353
-    except Exception, exError:
354
-        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
-        return False
356
-
357
-    if _RELAY_SERVER:
358
-        # Write the entire node data response to the output data file.
359
-        try:
360
-            fc = open(_OUTPUT_DATA_FILE, "w")
361
-            fc.write(sData)
362
-            fc.close()
363
-        except Exception, exError:
364
-            print "%s write output file failed: %s" % \
365
-                  (getTimeStamp(), exError)
366
-            return False
367
-        if verboseDebug:
368
-            print "write output data file: %d bytes" % len(sData)
369
-
370
-    return True
371
-## end def
372
-
373
-def setNodeStatus(updateSuccess):
374
-    """Detect if aredn node is offline or not available on
375
-       the network. After a set number of attempts to get data
376
-       from the node set a flag that the node is offline.
377
-       Parameters:
378
-           updateSuccess - a boolean that is True if data request
379
-                           successful, False otherwise
380
-       Returns: nothing
381
-    """
382
-    global failedUpdateCount, nodeOnline
383
-
384
-    if updateSuccess:
385
-        failedUpdateCount = 0
386
-        # Set status and send a message to the log if the node was
387
-        # previously offline and is now online.
388
-        if not nodeOnline:
389
-            print '%s aredn node online' % getTimeStamp()
390
-            nodeOnline = True
391
-    else:
392
-        # The last attempt failed, so update the failed attempts
393
-        # count.
394
-        failedUpdateCount += 1
395
-
396
-    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
-        # Max number of failed data requests, so set
398
-        # node status to offline.
399
-        setStatusToOffline()
400
-##end def
401
-
402
-def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
-                lower, upper, addTrend, autoScale):
404
-    """Uses rrdtool to create a graph of specified node data item.
405
-       Parameters:
406
-           fileName - name of file containing the graph
407
-           dataItem - data item to be graphed
408
-           gLabel - string containing a graph label for the data item
409
-           gTitle - string containing a title for the graph
410
-           gStart - beginning time of the graphed data
411
-           lower - lower bound for graph ordinate #NOT USED
412
-           upper - upper bound for graph ordinate #NOT USED
413
-           addTrend - 0, show only graph data
414
-                      1, show only a trend line
415
-                      2, show a trend line and the graph data
416
-           autoScale - if True, then use vertical axis auto scaling
417
-               (lower and upper parameters are ignored), otherwise use
418
-               lower and upper parameters to set vertical axis scale
419
-       Returns: True if successful, False otherwise
420
-    """
421
-    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
-    trendWindow = { 'end-1day': 7200,
423
-                    'end-4weeks': 172800,
424
-                    'end-12months': 604800 }
425
- 
426
-    # Format the rrdtool graph command.
427
-
428
-    # Set chart start time, height, and width.
429
-    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
-             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
-   
432
-    # Set the range and scaling of the chart y-axis.
433
-    if lower < upper:
434
-        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
-    elif autoScale:
436
-        strCmd += "-A "
437
-    strCmd += "-Y "
438
-
439
-    # Set the chart ordinate label and chart title. 
440
-    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
- 
442
-    # Show the data, or a moving average trend line over
443
-    # the data, or both.
444
-    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
-    if addTrend == 0:
446
-        strCmd += "LINE1:dSeries#0400ff "
447
-    elif addTrend == 1:
448
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
-                  % trendWindow[gStart]
450
-    elif addTrend == 2:
451
-        strCmd += "LINE1:dSeries#0400ff "
452
-        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
-                  % trendWindow[gStart]
454
-     
455
-    if verboseDebug:
456
-        print "%s" % strCmd # DEBUG
457
-    
458
-    # Run the formatted rrdtool command as a subprocess.
459
-    try:
460
-        result = subprocess.check_output(strCmd, \
461
-                     stderr=subprocess.STDOUT,   \
462
-                     shell=True)
463
-    except subprocess.CalledProcessError, exError:
464
-        print "rrdtool graph failed: %s" % (exError.output)
465
-        return False
466
-
467
-    if debugOption:
468
-        print "rrdtool graph: %s\n" % result,
469
-    return True
470
-
471
-##end def
472
-
473
-def generateGraphs():
474
-    """Generate graphs for display in html documents.
475
-       Parameters: none
476
-       Returns: nothing
477
-    """
478
-    autoScale = False
479
-
480
-    # The following will force creation of charts
481
-    # of only signal strength and S/N charts.  Note that the following
482
-    # data items appear constant and do not show variation with time:
483
-    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
-    # these parameters are demonstrated to vary in time, there is no point
485
-    # in creating the charts for these data items.
486
-    createAllCharts = False
487
-
488
-    # 24 hour stock charts
489
-
490
-    createGraph('24hr_signal', 'S', 'dBm', 
491
-                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
-    createGraph('24hr_snr', 'SNR', 'dB', 
493
-                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
-
495
-    if createAllCharts:
496
-        createGraph('24hr_noise', 'N', 'dBm', 
497
-                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
-                    autoScale)
499
-        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
-                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
-                    autoScale)
502
-        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
-                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
-                    autoScale)
505
-        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
-                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
-                     autoScale)
508
-        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
-                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
-                     autoScale)
511
-
512
-    # 4 week stock charts
513
-
514
-    createGraph('4wk_signal', 'S', 'dBm', 
515
-                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
-    createGraph('4wk_snr', 'SNR', 'dB', 
517
-                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
-
519
-    if createAllCharts:
520
-        createGraph('4wk_noise', 'N', 'dBm', 
521
-                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
-                    autoScale)
523
-        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
-                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
-                    autoScale)
526
-        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
-                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
-                    autoScale)
529
-        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
-                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
-                    autoScale)
532
-        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
-                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
-                    autoScale)
535
-
536
-    # 12 month stock charts
537
-
538
-    createGraph('12m_signal', 'S', 'dBm', 
539
-                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
-    createGraph('12m_snr', 'SNR', 'dB', 
541
-                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
-
543
-    if createAllCharts:
544
-        createGraph('12m_noise', 'N', 'dBm', 
545
-                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
-                    autoScale)
547
-        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
-                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
-                    autoScale)
550
-        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
-                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
-                    autoScale)
553
-        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
-                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
-                    autoScale)
556
-        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
-                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
-                    autoScale)
559
-    if debugOption:
560
-        #print # print a blank line to improve readability when in debug mode
561
-        pass
562
-##end def
563
-
564
-def getCLarguments():
565
-    """Get command line arguments.  There are four possible arguments
566
-          -d turns on debug mode
567
-          -v turns on verbose debug mode
568
-          -t sets the aredn node query interval
569
-          -u sets the url of the aredn nodeing device
570
-       Returns: nothing
571
-    """
572
-    global debugOption, verboseDebug, dataRequestInterval, \
573
-           arednNodeUrl
574
-
575
-    index = 1
576
-    while index < len(sys.argv):
577
-        if sys.argv[index] == '-d':
578
-            debugOption = True
579
-        elif sys.argv[index] == '-v':
580
-            debugOption = True
581
-            verboseDebug = True
582
-        elif sys.argv[index] == '-p':
583
-            try:
584
-                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
-            except:
586
-                print "invalid polling period"
587
-                exit(-1)
588
-            index += 1
589
-        elif sys.argv[index] == '-u':
590
-            arednNodeUrl = sys.argv[index + 1]
591
-            index += 1
592
-        else:
593
-            cmd_name = sys.argv[0].split('/')
594
-            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
-            exit(-1)
596
-        index += 1
597
-##end def
598
-
599
-def main():
600
-    """Handles timing of events and acts as executive routine managing
601
-       all other functions.
602
-       Parameters: none
603
-       Returns: nothing
604
-    """
605
-    global dataRequestInterval
606
-
607
-    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
-
609
-    print '%s starting up arednsig agent process' % \
610
-                  (getTimeStamp())
611
-
612
-    # last time output JSON file updated
613
-    lastDataRequestTime = -1
614
-    # last time charts generated
615
-    lastChartUpdateTime = - 1
616
-    # last time the rrdtool database updated
617
-    lastDatabaseUpdateTime = -1
618
-
619
-    ## Get command line arguments.
620
-    getCLarguments()
621
-
622
-    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
-    chartUpdateInterval = dataRequestInterval # get charts interval
624
-
625
-    ## Exit with error if rrdtool database does not exist.
626
-    if not os.path.exists(_RRD_FILE):
627
-        print 'rrdtool database does not exist\n' \
628
-              'use createArednsigRrd script to ' \
629
-              'create rrdtool database\n'
630
-        exit(1)
631
- 
632
-    ## main loop
633
-    while True:
634
-
635
-        currentTime = time.time() # get current time in seconds
636
-
637
-        # Every web update interval request data from the aredn
638
-        # node and process the received data.
639
-        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
-            lastDataRequestTime = currentTime
641
-            ldData = []
642
-            result = True
643
-
644
-            # Get the data string from the device.
645
-            sData = getArednNodeData()
646
-            # If the first http request fails, try one more time.
647
-            if sData == None:
648
-                time.sleep(5)
649
-                sData = getArednNodeData()
650
-                if sData == None:
651
-                    result = False
652
-
653
-            # If successful parse the data.
654
-            if result:
655
-                result = parseNodeData(sData, ldData)
656
-           
657
-            # If parsing successful, convert the data.
658
-            if result:
659
-                result = convertData(ldData)
660
-
661
-            # If conversion successful, write data to data files.
662
-            if result:
663
-                result = updateDatabase(ldData)
664
-
665
-            if result:
666
-                writeOutputDataFile(sData, ldData)
667
-
668
-            # Set the node status to online or offline depending on the
669
-            # success or failure of the above operations.
670
-            setNodeStatus(result)
671
-
672
-
673
-        # At the chart generation interval, generate charts.
674
-        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
-            lastChartUpdateTime = currentTime
676
-            p = multiprocessing.Process(target=generateGraphs, args=())
677
-            p.start()
678
-
679
-        # Relinquish processing back to the operating system until
680
-        # the next update interval.
681
-
682
-        elapsedTime = time.time() - currentTime
683
-        if debugOption:
684
-            if result:
685
-                print "%s update successful:" % getTimeStamp(),
686
-            else:
687
-                print "%s update failed:" % getTimeStamp(),
688
-            print "%6f seconds processing time\n" % elapsedTime 
689
-        remainingTime = requestIntervalSeconds - elapsedTime
690
-        if remainingTime > 0.0:
691
-            time.sleep(remainingTime)
692
-    ## end while
693
-    return
694
-## end def
695
-
696
-if __name__ == '__main__':
697
-    try:
698
-        main()
699
-    except KeyboardInterrupt:
700
-        print '\n',
701
-        terminateAgentProcess('KeyboardInterrupt','Module')
Browse code

support for Aredn FW v3.20.3.0

gandolf authored on 03/31/2020 17:38:41
Showing 1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,701 @@
1
+#!/usr/bin/python -u
2
+# The -u option above turns off block buffering of python output. This 
3
+# assures that each error message gets individually printed to the log file.
4
+#
5
+# Module: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mest services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed node status to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+import json
47
+
48
+_USER = os.environ['USER']
49
+_HOSTNAME = os.uname()[1]
50
+
51
+   ### DEFAULT AREDN NODE URL ###
52
+
53
+# set url of the aredn node
54
+
55
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
+
57
+    ### FILE AND FOLDER LOCATIONS ###
58
+
59
+# folder for containing dynamic data objects
60
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
+# folder for charts and output data file
62
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
+# location of data output file
64
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
+# dummy output data file
66
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
+# database that stores node data
68
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
+
70
+    ### GLOBAL CONSTANTS ###
71
+
72
+# max number of failed data requests allowed
73
+_MAX_FAILED_DATA_REQUESTS = 0
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# standard chart width in pixels
79
+_CHART_WIDTH = 600
80
+# standard chart height in pixels
81
+_CHART_HEIGHT = 150
82
+# Set this to True only if this server is intended to relay raw
83
+# node data to a mirror server.
84
+_RELAY_SERVER = False
85
+
86
+   ### GLOBAL VARIABLES ###
87
+
88
+# turn on or off of verbose debugging information
89
+debugOption = False
90
+verboseDebug = False
91
+
92
+# The following two items are used for detecting system faults
93
+# and aredn node online or offline status.
94
+
95
+# count of failed attempts to get data from aredn node
96
+failedUpdateCount = 0
97
+# detected status of aredn node device
98
+nodeOnline = True
99
+
100
+# ip address of aredn node
101
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
+# frequency of data requests to aredn node
103
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
+# last node request time
105
+lastDataPointTime = -1
106
+
107
+  ###  PRIVATE METHODS  ###
108
+
109
+def getTimeStamp():
110
+    """
111
+    Set the error message time stamp to the local system time.
112
+    Parameters: none
113
+    Returns: string containing the time stamp
114
+    """
115
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
+##end def
117
+
118
+def getLastDataPointTime():
119
+    """
120
+    Get the timestamp of the most recent update to the round robin
121
+    database.
122
+    Parameters: none
123
+    Returns: string epoch time stamp of the last rrd update
124
+    """
125
+    strCmd = "rrdtool lastupdate %s" % \
126
+             (_RRD_FILE)
127
+
128
+    # Run the command as a subprocess.
129
+    try:
130
+        result = subprocess.check_output(strCmd, shell=True,  \
131
+                             stderr=subprocess.STDOUT)
132
+    except subprocess.CalledProcessError, exError:
133
+        print "%s: rrdtool update failed: %s" % \
134
+                    (getTimeStamp(), exError.output)
135
+        return None
136
+
137
+    # Return just the epoch time stamp of the last rrd update
138
+    return int((result.split('\n')[-2]).split(':')[0])
139
+##end def
140
+
141
+def setStatusToOffline():
142
+    """Set the detected status of the aredn node to
143
+       "offline" and inform downstream clients by removing input
144
+       and output data files.
145
+       Parameters: none
146
+       Returns: nothing
147
+    """
148
+    global nodeOnline
149
+
150
+    # Inform downstream clients by removing output data file.
151
+    if os.path.exists(_OUTPUT_DATA_FILE):
152
+       os.remove(_OUTPUT_DATA_FILE)
153
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
+       os.remove(_DUMMY_OUTPUT_FILE)
155
+    # If the aredn node was previously online, then send
156
+    # a message that we are now offline.
157
+    if nodeOnline:
158
+        print '%s aredn node offline' % getTimeStamp()
159
+    nodeOnline = False
160
+##end def
161
+
162
+def terminateAgentProcess(signal, frame):
163
+    """Send a message to log when the agent process gets killed
164
+       by the operating system.  Inform downstream clients
165
+       by removing input and output data files.
166
+       Parameters:
167
+           signal, frame - dummy parameters
168
+       Returns: nothing
169
+    """
170
+    # Inform downstream clients by removing output data file.
171
+    if os.path.exists(_OUTPUT_DATA_FILE):
172
+       os.remove(_OUTPUT_DATA_FILE)
173
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
+       os.remove(_DUMMY_OUTPUT_FILE)
175
+    print '%s terminating arednsig agent process' % \
176
+              (getTimeStamp())
177
+    sys.exit(0)
178
+##end def
179
+
180
+  ###  PUBLIC METHODS  ###
181
+
182
+def getArednNodeData():
183
+    """Send http request to aredn node.  The response from the
184
+       node contains the node signal data as unformatted ascii text.
185
+       Parameters: none
186
+       Returns: a string containing the node signal data if successful,
187
+                or None if not successful
188
+    """
189
+    try:
190
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
+
192
+        # Format received data into a single string.
193
+        content = ""
194
+        for line in conn:
195
+            content += line.strip()
196
+        del conn
197
+
198
+    except Exception, exError:
199
+        # If no response is received from the device, then assume that
200
+        # the device is down or unavailable over the network.  In
201
+        # that case return None to the calling function.
202
+        print "%s http error: %s" % (getTimeStamp(), exError)
203
+        return None
204
+
205
+    if verboseDebug:
206
+        print "http request successful: %d bytes" % len(content)
207
+
208
+    return content
209
+##end def
210
+
211
+def parseNodeData(sData, ldData):
212
+    """Parse the node  signal data JSON string from the aredn node
213
+       into its component parts.  
214
+       Parameters:
215
+           sData - the string containing the data to be parsed
216
+           dData - a dictionary object to contain the parsed data items
217
+       Returns: True if successful, False otherwise
218
+    """
219
+    iTrail = int(dataRequestInterval)
220
+
221
+    try:
222
+        ldTmp = json.loads(sData[1:-1])
223
+        ldTmp = ldTmp[-iTrail:]
224
+        if len(ldTmp) != iTrail:
225
+            #raise Exception("truncated list")
226
+            pass
227
+    except Exception, exError:
228
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
+        return False
230
+    
231
+    del ldData[:]
232
+    for item in ldTmp:
233
+        ldData.append(item)
234
+
235
+    if verboseDebug:
236
+        print "parse successful: %d data points" % len(ldData)
237
+    return True
238
+##end def
239
+
240
+def convertData(ldData):
241
+    """Convert individual node signal data items as necessary.
242
+       Parameters:
243
+           dData - a dictionary object containing the node signal data
244
+       Returns: True if successful, False otherwise
245
+    """
246
+    # parse example string
247
+    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
+    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
+    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
+    #
251
+    index = 0
252
+    while index < len(ldData):
253
+        item = ldData[index]
254
+        try:
255
+            item['time'] = int(item.pop('x')) / 1000
256
+            item['signal'] = int(item['y'][0])
257
+            item['noise'] = int(item['y'][1])
258
+            item['snr'] = int(item.pop('m'))
259
+            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
+            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
+            item['rx_rate'] = int(item.pop('rx_rate'))
262
+            item['tx_rate'] = int(item.pop('tx_rate'))
263
+            item.pop('y')
264
+            item.pop('label')
265
+        except Exception, exError:
266
+            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
+            print "discarding %s" % item
268
+            #return False
269
+            ldData.pop(index)
270
+        else:
271
+            index += 1
272
+    ##end for
273
+
274
+    if len(ldData) > 0:
275
+        if verboseDebug:
276
+            print "convert data successful"
277
+        return True
278
+    else:
279
+        print "convert data failed"
280
+        return False
281
+##end def
282
+
283
+def updateDatabase(ldData):
284
+    """
285
+    Update the rrdtool database by executing an rrdtool system command.
286
+    Format the command using the data extracted from the aredn node
287
+    response.   
288
+    Parameters: dData - dictionary object containing data items to be
289
+                        written to the rr database file
290
+    Returns: True if successful, False otherwise
291
+    """
292
+    updateCount = 0
293
+    lastDataPointTime = getLastDataPointTime()
294
+
295
+    if verboseDebug:
296
+         print "updating database..."
297
+
298
+    for item in ldData:
299
+
300
+        if item['time'] <= lastDataPointTime:
301
+            if verboseDebug:
302
+                print "%s invalid timestamp: discarding data" % \
303
+                      (getTimeStamp())
304
+            continue
305
+
306
+        # Format the rrdtool update command.
307
+        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
+        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
+                 item['noise'], item['snr'], item['rx_mcs'], \
310
+                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
+
312
+        if verboseDebug:
313
+            print "%s" % strCmd # DEBUG
314
+
315
+        # Run the command as a subprocess.
316
+        try:
317
+            subprocess.check_output(strCmd, shell=True,  \
318
+                                 stderr=subprocess.STDOUT)
319
+        except subprocess.CalledProcessError, exError:
320
+            print "%s: rrdtool update failed: %s" % \
321
+                        (getTimeStamp(), exError.output)
322
+            return False
323
+        updateCount += 1
324
+    ##end for
325
+
326
+    if debugOption:
327
+        print '%s added %d data points to database' % \
328
+              (getTimeStamp(), updateCount)
329
+    return True
330
+##end def
331
+
332
+def writeOutputDataFile(sData, ldData):
333
+    """Write node data items to the output data file, formatted as 
334
+       a Javascript file.  This file may then be accessed and used by
335
+       by downstream clients, for instance, in HTML documents.
336
+       Parameters:
337
+           sData - a string object containing the data to be written
338
+                   to the output data file
339
+       Returns: True if successful, False otherwise
340
+    """
341
+    # Write file for use by html clients.  The following two
342
+    # data items are sent to the client file.
343
+    #    * The last database update date and time
344
+    #    * The data request interval
345
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
+                                time.localtime(ldData[-1]['time']) )
347
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
+           (lastUpdate, dataRequestInterval)
349
+    try:
350
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
+        fc.write(sDate)
352
+        fc.close()
353
+    except Exception, exError:
354
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
+        return False
356
+
357
+    if _RELAY_SERVER:
358
+        # Write the entire node data response to the output data file.
359
+        try:
360
+            fc = open(_OUTPUT_DATA_FILE, "w")
361
+            fc.write(sData)
362
+            fc.close()
363
+        except Exception, exError:
364
+            print "%s write output file failed: %s" % \
365
+                  (getTimeStamp(), exError)
366
+            return False
367
+        if verboseDebug:
368
+            print "write output data file: %d bytes" % len(sData)
369
+
370
+    return True
371
+## end def
372
+
373
+def setNodeStatus(updateSuccess):
374
+    """Detect if aredn node is offline or not available on
375
+       the network. After a set number of attempts to get data
376
+       from the node set a flag that the node is offline.
377
+       Parameters:
378
+           updateSuccess - a boolean that is True if data request
379
+                           successful, False otherwise
380
+       Returns: nothing
381
+    """
382
+    global failedUpdateCount, nodeOnline
383
+
384
+    if updateSuccess:
385
+        failedUpdateCount = 0
386
+        # Set status and send a message to the log if the node was
387
+        # previously offline and is now online.
388
+        if not nodeOnline:
389
+            print '%s aredn node online' % getTimeStamp()
390
+            nodeOnline = True
391
+    else:
392
+        # The last attempt failed, so update the failed attempts
393
+        # count.
394
+        failedUpdateCount += 1
395
+
396
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
+        # Max number of failed data requests, so set
398
+        # node status to offline.
399
+        setStatusToOffline()
400
+##end def
401
+
402
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
+                lower, upper, addTrend, autoScale):
404
+    """Uses rrdtool to create a graph of specified node data item.
405
+       Parameters:
406
+           fileName - name of file containing the graph
407
+           dataItem - data item to be graphed
408
+           gLabel - string containing a graph label for the data item
409
+           gTitle - string containing a title for the graph
410
+           gStart - beginning time of the graphed data
411
+           lower - lower bound for graph ordinate #NOT USED
412
+           upper - upper bound for graph ordinate #NOT USED
413
+           addTrend - 0, show only graph data
414
+                      1, show only a trend line
415
+                      2, show a trend line and the graph data
416
+           autoScale - if True, then use vertical axis auto scaling
417
+               (lower and upper parameters are ignored), otherwise use
418
+               lower and upper parameters to set vertical axis scale
419
+       Returns: True if successful, False otherwise
420
+    """
421
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
+    trendWindow = { 'end-1day': 7200,
423
+                    'end-4weeks': 172800,
424
+                    'end-12months': 604800 }
425
+ 
426
+    # Format the rrdtool graph command.
427
+
428
+    # Set chart start time, height, and width.
429
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
+   
432
+    # Set the range and scaling of the chart y-axis.
433
+    if lower < upper:
434
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
+    elif autoScale:
436
+        strCmd += "-A "
437
+    strCmd += "-Y "
438
+
439
+    # Set the chart ordinate label and chart title. 
440
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
+ 
442
+    # Show the data, or a moving average trend line over
443
+    # the data, or both.
444
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
+    if addTrend == 0:
446
+        strCmd += "LINE1:dSeries#0400ff "
447
+    elif addTrend == 1:
448
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
+                  % trendWindow[gStart]
450
+    elif addTrend == 2:
451
+        strCmd += "LINE1:dSeries#0400ff "
452
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
+                  % trendWindow[gStart]
454
+     
455
+    if verboseDebug:
456
+        print "%s" % strCmd # DEBUG
457
+    
458
+    # Run the formatted rrdtool command as a subprocess.
459
+    try:
460
+        result = subprocess.check_output(strCmd, \
461
+                     stderr=subprocess.STDOUT,   \
462
+                     shell=True)
463
+    except subprocess.CalledProcessError, exError:
464
+        print "rrdtool graph failed: %s" % (exError.output)
465
+        return False
466
+
467
+    if debugOption:
468
+        print "rrdtool graph: %s\n" % result,
469
+    return True
470
+
471
+##end def
472
+
473
+def generateGraphs():
474
+    """Generate graphs for display in html documents.
475
+       Parameters: none
476
+       Returns: nothing
477
+    """
478
+    autoScale = False
479
+
480
+    # The following will force creation of charts
481
+    # of only signal strength and S/N charts.  Note that the following
482
+    # data items appear constant and do not show variation with time:
483
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
+    # these parameters are demonstrated to vary in time, there is no point
485
+    # in creating the charts for these data items.
486
+    createAllCharts = False
487
+
488
+    # 24 hour stock charts
489
+
490
+    createGraph('24hr_signal', 'S', 'dBm', 
491
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
+    createGraph('24hr_snr', 'SNR', 'dB', 
493
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
+
495
+    if createAllCharts:
496
+        createGraph('24hr_noise', 'N', 'dBm', 
497
+                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
+                    autoScale)
499
+        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
+                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
+                    autoScale)
502
+        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
+                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
+                    autoScale)
505
+        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
+                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
+                     autoScale)
508
+        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
+                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
+                     autoScale)
511
+
512
+    # 4 week stock charts
513
+
514
+    createGraph('4wk_signal', 'S', 'dBm', 
515
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
+    createGraph('4wk_snr', 'SNR', 'dB', 
517
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
+
519
+    if createAllCharts:
520
+        createGraph('4wk_noise', 'N', 'dBm', 
521
+                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
+                    autoScale)
523
+        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
+                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
+                    autoScale)
526
+        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
+                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
+                    autoScale)
529
+        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
+                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
+                    autoScale)
532
+        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
+                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
+                    autoScale)
535
+
536
+    # 12 month stock charts
537
+
538
+    createGraph('12m_signal', 'S', 'dBm', 
539
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
+    createGraph('12m_snr', 'SNR', 'dB', 
541
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
+
543
+    if createAllCharts:
544
+        createGraph('12m_noise', 'N', 'dBm', 
545
+                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
+                    autoScale)
547
+        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
+                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
+                    autoScale)
550
+        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
+                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
+                    autoScale)
553
+        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
+                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
+                    autoScale)
556
+        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
+                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
+                    autoScale)
559
+    if debugOption:
560
+        #print # print a blank line to improve readability when in debug mode
561
+        pass
562
+##end def
563
+
564
+def getCLarguments():
565
+    """Get command line arguments.  There are four possible arguments
566
+          -d turns on debug mode
567
+          -v turns on verbose debug mode
568
+          -t sets the aredn node query interval
569
+          -u sets the url of the aredn nodeing device
570
+       Returns: nothing
571
+    """
572
+    global debugOption, verboseDebug, dataRequestInterval, \
573
+           arednNodeUrl
574
+
575
+    index = 1
576
+    while index < len(sys.argv):
577
+        if sys.argv[index] == '-d':
578
+            debugOption = True
579
+        elif sys.argv[index] == '-v':
580
+            debugOption = True
581
+            verboseDebug = True
582
+        elif sys.argv[index] == '-p':
583
+            try:
584
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
+            except:
586
+                print "invalid polling period"
587
+                exit(-1)
588
+            index += 1
589
+        elif sys.argv[index] == '-u':
590
+            arednNodeUrl = sys.argv[index + 1]
591
+            index += 1
592
+        else:
593
+            cmd_name = sys.argv[0].split('/')
594
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
+            exit(-1)
596
+        index += 1
597
+##end def
598
+
599
+def main():
600
+    """Handles timing of events and acts as executive routine managing
601
+       all other functions.
602
+       Parameters: none
603
+       Returns: nothing
604
+    """
605
+    global dataRequestInterval
606
+
607
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
+
609
+    print '%s starting up arednsig agent process' % \
610
+                  (getTimeStamp())
611
+
612
+    # last time output JSON file updated
613
+    lastDataRequestTime = -1
614
+    # last time charts generated
615
+    lastChartUpdateTime = - 1
616
+    # last time the rrdtool database updated
617
+    lastDatabaseUpdateTime = -1
618
+
619
+    ## Get command line arguments.
620
+    getCLarguments()
621
+
622
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
+    chartUpdateInterval = dataRequestInterval # get charts interval
624
+
625
+    ## Exit with error if rrdtool database does not exist.
626
+    if not os.path.exists(_RRD_FILE):
627
+        print 'rrdtool database does not exist\n' \
628
+              'use createArednsigRrd script to ' \
629
+              'create rrdtool database\n'
630
+        exit(1)
631
+ 
632
+    ## main loop
633
+    while True:
634
+
635
+        currentTime = time.time() # get current time in seconds
636
+
637
+        # Every web update interval request data from the aredn
638
+        # node and process the received data.
639
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
+            lastDataRequestTime = currentTime
641
+            ldData = []
642
+            result = True
643
+
644
+            # Get the data string from the device.
645
+            sData = getArednNodeData()
646
+            # If the first http request fails, try one more time.
647
+            if sData == None:
648
+                time.sleep(5)
649
+                sData = getArednNodeData()
650
+                if sData == None:
651
+                    result = False
652
+
653
+            # If successful parse the data.
654
+            if result:
655
+                result = parseNodeData(sData, ldData)
656
+           
657
+            # If parsing successful, convert the data.
658
+            if result:
659
+                result = convertData(ldData)
660
+
661
+            # If conversion successful, write data to data files.
662
+            if result:
663
+                result = updateDatabase(ldData)
664
+
665
+            if result:
666
+                writeOutputDataFile(sData, ldData)
667
+
668
+            # Set the node status to online or offline depending on the
669
+            # success or failure of the above operations.
670
+            setNodeStatus(result)
671
+
672
+
673
+        # At the chart generation interval, generate charts.
674
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
+            lastChartUpdateTime = currentTime
676
+            p = multiprocessing.Process(target=generateGraphs, args=())
677
+            p.start()
678
+
679
+        # Relinquish processing back to the operating system until
680
+        # the next update interval.
681
+
682
+        elapsedTime = time.time() - currentTime
683
+        if debugOption:
684
+            if result:
685
+                print "%s update successful:" % getTimeStamp(),
686
+            else:
687
+                print "%s update failed:" % getTimeStamp(),
688
+            print "%6f seconds processing time\n" % elapsedTime 
689
+        remainingTime = requestIntervalSeconds - elapsedTime
690
+        if remainingTime > 0.0:
691
+            time.sleep(remainingTime)
692
+    ## end while
693
+    return
694
+## end def
695
+
696
+if __name__ == '__main__':
697
+    try:
698
+        main()
699
+    except KeyboardInterrupt:
700
+        print '\n',
701
+        terminateAgentProcess('KeyboardInterrupt','Module')