6
6
import json
7
7
import os
8
8
import threading
9
+ from datetime import datetime , timedelta
9
10
import logging
10
11
import logging .config
11
12
from multiprocessing import Lock
18
19
19
20
from ._version import __version__
20
21
22
+ LOG_LEVELS = ["DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ]
23
+ DEFAULT_LOG_LEVEL = "ERROR"
24
+
25
+ LOG = logging .getLogger ("ProSafeExporter" )
26
+
21
27
mutex = Lock ()
22
28
speedmap = {'Nicht verbunden' : '0' , 'No Speed' : '0' , '10M' : '10' , '100M' : '100' , '1000M' : '1000' }
23
29
24
30
25
31
class ProSafeExporter :
26
- def __init__ (self , retrievers = None , logger = logging .getLogger ()):
27
- self .logger = logger
32
+ def __init__ (self , retrievers = None , retrieveInterval = 20.0 ):
28
33
self .retrievers = retrievers
34
+ self .retrieveInterval = retrieveInterval
35
+ self .lastRetrieve = None
29
36
30
37
self .app = flask .Flask ('ProSafeExporter' )
31
38
self .app .add_url_rule ('/<path>' , '/<path:path>' ,
32
39
self .__probe , methods = ['POST' , 'GET' ])
33
40
self .app .add_url_rule ('/' , '/' , self .__probe , methods = ['POST' , 'GET' ])
34
41
35
- def run (self , host = "0.0.0.0" , port = 9493 , retrieveInterval = 20.0 , debug = False , endless = True ): # nosec
42
+ def run (self , host = "0.0.0.0" , port = 9493 , debug = False , endless = True ): # nosec
36
43
if not debug : # pragma: no cover
37
44
os .environ ['WERKZEUG_RUN_MAIN' ] = 'true'
38
45
log = logging .getLogger ('werkzeug' )
@@ -42,37 +49,41 @@ def run(self, host="0.0.0.0", port=9493, retrieveInterval=20.0, debug=False, end
42
49
43
50
webthread = threading .Thread (target = server .serve_forever )
44
51
webthread .start ()
45
- self . logger .info ('ProSafeExporter is listening on %s:%s for request on /metrics endpoint'
46
- ' (but you can also use any other path)' , host , port )
52
+ LOG .info ('ProSafeExporter is listening on %s:%s for request on /metrics endpoint'
53
+ ' (but you can also use any other path)' , host , port )
47
54
48
55
try :
49
56
self .__retrieve ()
50
57
while endless : # pragma: no cover
51
- time .sleep (retrieveInterval )
58
+ time .sleep (self . retrieveInterval )
52
59
self .__retrieve ()
53
60
except KeyboardInterrupt : # pragma: no cover
54
61
pass
55
62
server .shutdown ()
56
63
webthread .join ()
57
- self . logger .info ('ProSafeExporter was stopped' )
64
+ LOG .info ('ProSafeExporter was stopped' )
58
65
59
66
def __probe (self , path = None ):
60
- result = "# Exporter output\n \n "
61
- for retriever in self .retrievers :
62
- result += retriever .result + '\n \n '
63
- self .logger .info ('Request on endpoint /%s \n %s' , path , result )
64
- return flask .Response (result , status = 200 , headers = {})
67
+ if self .lastRetrieve is not None \
68
+ and self .lastRetrieve > datetime .now () - timedelta (seconds = (self .retrieveInterval * 5 )):
69
+ result = "# Exporter output\n \n "
70
+ for retriever in self .retrievers :
71
+ result += retriever .result + '\n \n '
72
+ LOG .debug ('Request on endpoint /%s \n %s' , path , result )
73
+ return flask .Response (result , status = 200 , headers = {})
74
+ return flask .Response ('' , status = 503 , headers = {'Retry-After' : self .retrieveInterval })
65
75
66
76
def __retrieve (self ):
67
- self . logger .info ('Retrieving data from all devies' )
77
+ LOG .info ('Retrieving data from all devies' )
68
78
for retriever in self .retrievers :
69
79
try :
70
80
retriever .retrieve ()
71
81
except (ConnectionRefusedError , requests .exceptions .ConnectionError ):
72
- self . logger .error (
82
+ LOG .error (
73
83
'Failed to refrieve for host %s' , retriever .hostname )
84
+ self .lastRetrieve = datetime .now ()
74
85
retriever .writeResult ()
75
- self . logger .info ('Retrieving done' )
86
+ LOG .info ('Retrieving done' )
76
87
77
88
78
89
class ProSafeRetrieve :
@@ -83,10 +94,8 @@ def __init__(self,
83
94
hostname ,
84
95
password ,
85
96
cookiefile = None ,
86
- logger = logging .getLogger (),
87
97
retries = 10 ,
88
98
requestTimeout = 10.0 ):
89
- self .logger = logger
90
99
self .retries = retries
91
100
self .requestTimeout = requestTimeout
92
101
self .hostname = hostname
@@ -108,41 +117,41 @@ def __init__(self,
108
117
self .__session .cookies .update (cookies )
109
118
self .loggedIn = True
110
119
except json .JSONDecodeError as err :
111
- self . logger .info ('Created retriever for host %s'
112
- ' but could not use cookiefile %s (%s)' , self .hostname , cookiefile , err .msg )
120
+ LOG .info ('Created retriever for host %s'
121
+ ' but could not use cookiefile %s (%s)' , self .hostname , cookiefile , err .msg )
113
122
except FileNotFoundError as err :
114
- self . logger .info ('Created retriever for host %s'
115
- ' but could not use cookiefile %s (%s)' , self .hostname , cookiefile , err )
123
+ LOG .info ('Created retriever for host %s'
124
+ ' but could not use cookiefile %s (%s)' , self .hostname , cookiefile , err )
116
125
self .cookieFile = cookiefile
117
- self . logger .info ('Created retriever for host %s using cookiefile %s' , self .hostname , cookiefile )
126
+ LOG .info ('Created retriever for host %s using cookiefile %s' , self .hostname , cookiefile )
118
127
except OSError : # pragma: no cover
119
- self . logger .info ('Created retriever for host %s'
120
- ' but could not use cookiefile %s' , self .hostname , cookiefile )
128
+ LOG .info ('Created retriever for host %s'
129
+ ' but could not use cookiefile %s' , self .hostname , cookiefile )
121
130
else :
122
- self . logger .info ('Created retriever for host %s' , self .hostname )
131
+ LOG .info ('Created retriever for host %s' , self .hostname )
123
132
124
133
def __del__ (self ):
125
134
if self .cookieFile :
126
135
try :
127
136
with open (self .cookieFile , 'w' ) as file :
128
137
json .dump (requests .utils .dict_from_cookiejar (self .__session .cookies ), file )
129
- self . logger .info ('Writing cookiefile %s' , self .cookieFile )
138
+ LOG .info ('Writing cookiefile %s' , self .cookieFile )
130
139
self .__cookiefd = None
131
140
except ValueError as err : # pragma: no cover
132
- self . logger .info ('Could not write cookiefile %s for host %s (%s)' ,
133
- self .__cookiefd .name , self .hostname , err )
141
+ LOG .info ('Could not write cookiefile %s for host %s (%s)' ,
142
+ self .__cookiefd .name , self .hostname , err )
134
143
135
144
def __login (self ):
136
145
if self .loggedIn :
137
146
indexPageRequest = self .__session .get (
138
147
f'http://{ self .hostname } /index.htm' , timeout = self .requestTimeout )
139
148
if 'RedirectToLoginPage' not in indexPageRequest .text :
140
- self . logger .info ('Already logged in for %s' , self .hostname )
149
+ LOG .info ('Already logged in for %s' , self .hostname )
141
150
return
142
151
# lets start with a new session
143
152
self .__session = requests .Session ()
144
153
self .loggedIn = False
145
- self . logger .info ('Have to login again for %s due to inactive session' , self .hostname )
154
+ LOG .info ('Have to login again for %s due to inactive session' , self .hostname )
146
155
loginPageRequest = self .__session .get (
147
156
f'http://{ self .hostname } /login.htm' , timeout = self .requestTimeout )
148
157
loginPageRequest .raise_for_status ()
@@ -152,8 +161,8 @@ def __login(self):
152
161
payload = None
153
162
if len (rand ) != 1 :
154
163
# looks like an old firmware without seed
155
- self . logger .warning ('Your switch %s uses an old firmware which sends your password'
156
- ' unencrypted while retrieving data. Please conscider updating' , self .hostname )
164
+ LOG .warning ('Your switch %s uses an old firmware which sends your password'
165
+ ' unencrypted while retrieving data. Please conscider updating' , self .hostname )
157
166
158
167
payload = {
159
168
'password' : self .password ,
@@ -176,7 +185,7 @@ def __login(self):
176
185
errorMsg = tree .xpath ('//input[@id="err_msg"]/@value[1]' )
177
186
if errorMsg and errorMsg [0 ]:
178
187
self .error = f'I could not login at the switch { self .hostname } due to: { errorMsg [0 ]} '
179
- self . logger .error (self .error )
188
+ LOG .error (self .error )
180
189
raise ConnectionRefusedError (self .error )
181
190
self .loggedIn = True
182
191
@@ -186,7 +195,7 @@ def __retrieveInfos(self): # noqa: C901
186
195
187
196
if 'RedirectToLoginPage' in infoRequest .text :
188
197
self .error = 'Login failed for ' + self .hostname
189
- self . logger .error (self .error )
198
+ LOG .error (self .error )
190
199
raise ConnectionRefusedError (self .error )
191
200
tree = html .fromstring (infoRequest .content )
192
201
allinfos = tree .xpath ('//table[@class="tableStyle"]//td[@nowrap=""]' )
@@ -230,7 +239,7 @@ def __retrieveStatus(self):
230
239
231
240
if 'RedirectToLoginPage' in statusRequest .text :
232
241
self .error = 'Login failed for ' + self .hostname
233
- self . logger .error (self .error )
242
+ LOG .error (self .error )
234
243
self .__infos = None
235
244
raise ConnectionRefusedError (self .error )
236
245
@@ -275,15 +284,15 @@ def __retrieveStatus(self):
275
284
self .__status = [[speedmap [n ] if i == 2 else n for i ,
276
285
n in enumerate (portStatus )] for portStatus in self .__status ]
277
286
break
278
- self . logger .info ('Problem while retrieving status for %s'
279
- ' this can happen when there is much traffic on the device' , self .hostname )
287
+ LOG .info ('Problem while retrieving status for %s'
288
+ ' this can happen when there is much traffic on the device' , self .hostname )
280
289
retries -= 1
281
290
if retries == 0 :
282
291
self .__status = None
283
292
self .error = f'Could not retrieve correct status for { self .hostname } after { self .retries } ' \
284
293
' retries. This can happen when there is much traffic on the device, but it is more likely' \
285
294
' that the firmware is not understood'
286
- self . logger .error (self .error )
295
+ LOG .error (self .error )
287
296
return False
288
297
return True
289
298
@@ -302,7 +311,7 @@ def __retrieveStatistics(self):
302
311
303
312
if 'RedirectToLoginPage' in statisticsRequest .text :
304
313
self .error = f'Login failed for { self .hostname } '
305
- self . logger .error (self .error )
314
+ LOG .error (self .error )
306
315
self .__infos = None
307
316
self .__status = None
308
317
raise ConnectionRefusedError (self .error )
@@ -325,19 +334,19 @@ def __retrieveStatistics(self):
325
334
noProblem = False
326
335
if noProblem :
327
336
break
328
- self . logger .info ('Problem while retrieving statistics for %s'
329
- ' this can happen when there is much traffic on the device' , self .hostname )
337
+ LOG .info ('Problem while retrieving statistics for %s'
338
+ ' this can happen when there is much traffic on the device' , self .hostname )
330
339
retries -= 1
331
340
if retries == 0 :
332
341
self .__statistics = None
333
342
self .error = f'Could not retrieve correct statistics for { self .hostname } after { self .retries } retries.' \
334
343
' This can happen when there is much traffic on the device'
335
- self . logger .error (self .error )
344
+ LOG .error (self .error )
336
345
return False
337
346
return True
338
347
339
348
def retrieve (self ):
340
- self . logger .info ('Start retrieval for %s' , self .hostname )
349
+ LOG .info ('Start retrieval for %s' , self .hostname )
341
350
342
351
with mutex :
343
352
self .error = ""
@@ -362,17 +371,17 @@ def retrieve(self):
362
371
self .error = f'Result is not plausible for { self .hostname } ' \
363
372
' Different number of ports for statistics and status. This can happen when there is much' \
364
373
' traffic on the device'
365
- self . logger .error (self .error )
374
+ LOG .error (self .error )
366
375
return
367
376
368
- self . logger .info ('Retrieval for %s done' , self .hostname )
377
+ LOG .info ('Retrieval for %s done' , self .hostname )
369
378
370
379
except (requests .exceptions .ConnectionError , requests .exceptions .HTTPError ):
371
380
self .__infos = None
372
381
self .__status = None
373
382
self .__statistics = None
374
383
self .error = f'Connection Error with host { self .hostname } '
375
- self . logger .error (self .error )
384
+ LOG .error (self .error )
376
385
377
386
def writeResult (self ): # noqa: C901
378
387
result = ""
@@ -444,35 +453,20 @@ def main(endless=True, always_early_timeout=False): # noqa: C901
444
453
description = 'Query Netgear ProSafe Switches using the web interface to provide statistics for Prometheus' )
445
454
parser .add_argument ('config' , type = argparse .FileType (
446
455
'r' ), help = 'configuration' )
447
- parser .add_argument ('-v' , '--verbose' ,
448
- help = 'increase output verbosity' , action = 'store_true' )
456
+ parser .add_argument ('-v' , '--verbose' , action = "append_const" , const = - 1 ,)
449
457
parser .add_argument ('--version' , action = 'version' ,
450
458
version = '%(prog)s {version}' .format (version = __version__ ))
451
459
args = parser .parse_args ()
452
460
453
- logger = logging .getLogger ('ProSafe_Exporter' )
454
- logger .setLevel (logging .INFO )
455
-
456
- ch = logging .StreamHandler ()
457
-
458
- if args .verbose :
459
- ch .setLevel (logging .INFO )
460
- else :
461
- ch .setLevel (logging .WARNING )
462
-
463
- # create formatter
464
- formatter = logging .Formatter (
465
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )
466
-
467
- # add formatter to ch
468
- ch .setFormatter (formatter )
461
+ logLevel = LOG_LEVELS .index (DEFAULT_LOG_LEVEL )
462
+ for adjustment in args .verbose or ():
463
+ logLevel = min (len (LOG_LEVELS ) - 1 , max (logLevel + adjustment , 0 ))
469
464
470
- # add ch to logger
471
- logger .addHandler (ch )
465
+ logging .basicConfig (level = LOG_LEVELS [logLevel ])
472
466
473
467
config = yaml .load (args .config , Loader = yaml .SafeLoader )
474
468
if not config :
475
- logger .error ('Config empty or cannot be parsed' )
469
+ LOG .error ('Config empty or cannot be parsed' )
476
470
sys .exit (3 )
477
471
478
472
if 'global' not in config :
@@ -489,7 +483,7 @@ def main(endless=True, always_early_timeout=False): # noqa: C901
489
483
config ['global' ]['retries' ] = 10
490
484
491
485
if 'switches' not in config or not config ['switches' ]:
492
- logger .error (
486
+ LOG .error (
493
487
'You have to define switches in the switches: section of your configuration' )
494
488
sys .exit (4 )
495
489
@@ -499,11 +493,11 @@ def main(endless=True, always_early_timeout=False): # noqa: C901
499
493
retrievers = list ()
500
494
for switch in config ['switches' ]:
501
495
if 'hostname' not in switch :
502
- logger .error (
496
+ LOG .error (
503
497
'You have to define the hostname for the switch, ignoring this switch entry' )
504
498
continue
505
499
if 'password' not in switch :
506
- logger .error (
500
+ LOG .error (
507
501
'You have to define the password for the switch, ignoring this switch entry' )
508
502
continue
509
503
if 'cookiefile' not in switch :
@@ -512,13 +506,11 @@ def main(endless=True, always_early_timeout=False): # noqa: C901
512
506
ProSafeRetrieve (
513
507
hostname = switch ['hostname' ],
514
508
password = switch ['password' ],
515
- logger = logger ,
516
509
retries = config ['global' ]['retries' ],
517
510
requestTimeout = config ['global' ]['retrieve_timeout' ],
518
511
cookiefile = switch ['cookiefile' ]))
519
- exporter = ProSafeExporter (retrievers = retrievers , logger = logger )
520
- exporter .run (host = config ['global' ]['host' ], port = config ['global' ]['port' ],
521
- retrieveInterval = config ['global' ]['retrieve_interval' ], debug = args .verbose , endless = endless )
512
+ exporter = ProSafeExporter (retrievers = retrievers , retrieveInterval = config ['global' ]['retrieve_interval' ])
513
+ exporter .run (host = config ['global' ]['host' ], port = config ['global' ]['port' ], debug = args .verbose , endless = endless )
522
514
# Cleanup
523
515
del exporter
524
516
retrievers .clear ()
0 commit comments