1 | #!/usr/bin/env python3 |
---|
2 | import sys |
---|
3 | import os |
---|
4 | import re |
---|
5 | import signal |
---|
6 | import time |
---|
7 | import subprocess |
---|
8 | import requests |
---|
9 | import json |
---|
10 | import configparser |
---|
11 | import daemon |
---|
12 | from xmlrpc import client |
---|
13 | import lockfile |
---|
14 | import logging.handlers |
---|
15 | from logging import config as cfg |
---|
16 | import ssl |
---|
17 | import psutil |
---|
18 | |
---|
19 | |
---|
20 | oldsignals = {} |
---|
21 | for sig in signal.Signals: |
---|
22 | try: |
---|
23 | oldsignals.setdefault(sig.name, signal.getsignal(sig)) |
---|
24 | signal.signal(sig.value, signal.SIG_IGN) |
---|
25 | except: |
---|
26 | continue |
---|
27 | |
---|
28 | # |
---|
29 | # START EDITABLE VARS (OVERRIDES VALUES IN CONFIGFILE) |
---|
30 | # |
---|
31 | |
---|
32 | # DEBUG = 1 |
---|
33 | |
---|
34 | MODE = 'PROCEDURAL' |
---|
35 | # MODE = 'THREADED' |
---|
36 | |
---|
37 | CONFIGFILE = '/etc/lliurex-analytics/agent.cfg' |
---|
38 | # CONFIGFILE = 'config.txt' |
---|
39 | |
---|
40 | # OVERRIDE_SEND_PERMISSION = 1 |
---|
41 | |
---|
42 | # MIN_LOG_LEVEL = 'info' |
---|
43 | |
---|
44 | # DAEMON_MODE = 1 |
---|
45 | |
---|
46 | # FILELOCK = '/tmp/analytics' |
---|
47 | |
---|
48 | # PIDFILE = '/var/run/analitics.pid' |
---|
49 | # PIDFILE = '/tmp/analitics.pid' |
---|
50 | |
---|
51 | # STATUSFILE = '/etc/lliurex-analytics/status' |
---|
52 | |
---|
53 | # TIMEOUT = 1 |
---|
54 | |
---|
55 | # |
---|
56 | # END EDITABLE VARS # |
---|
57 | # |
---|
58 | |
---|
59 | |
---|
60 | if MODE == 'PROCEDURAL': |
---|
61 | from multiprocessing import Process, Manager |
---|
62 | str_formatter = '(%(processName)s)' |
---|
63 | if MODE == 'THREADED': |
---|
64 | from multiprocessing.dummy import Process, Manager |
---|
65 | str_formatter = '(%(threadName)s)' |
---|
66 | |
---|
67 | def get_var_value(varname, config=None, mode='string', section='Agent'): |
---|
68 | value = None |
---|
69 | |
---|
70 | if config: |
---|
71 | varname = varname.lower() |
---|
72 | try: |
---|
73 | if mode == 'string': |
---|
74 | value = config.get(section, varname) |
---|
75 | elif mode == 'bool': |
---|
76 | value = config.getboolean(section, varname) |
---|
77 | elif mode == 'int': |
---|
78 | value = config.getint(section, varname) |
---|
79 | elif mode == 'float': |
---|
80 | value = config.getfloat(section, varname) |
---|
81 | except: |
---|
82 | pass |
---|
83 | |
---|
84 | def f(item): |
---|
85 | if isinstance(item, str) or isinstance(item, bool) or isinstance(item, int) or isinstance(item, float): |
---|
86 | return True |
---|
87 | else: |
---|
88 | return False |
---|
89 | |
---|
90 | for x in (v for v in globals() if varname.lower() == v.lower() and f(globals()[v])): |
---|
91 | value = globals()[x] |
---|
92 | |
---|
93 | if mode == 'string': |
---|
94 | return str(value) |
---|
95 | elif mode == 'bool': |
---|
96 | return bool(value) |
---|
97 | elif mode == 'int': |
---|
98 | return int(value) |
---|
99 | elif mode == 'float': |
---|
100 | return float(value) |
---|
101 | else: |
---|
102 | return value |
---|
103 | |
---|
104 | |
---|
105 | if get_var_value('DEBUG', mode='bool'): |
---|
106 | loglevel = get_var_value('MIN_LOG_LEVEL') |
---|
107 | if loglevel: |
---|
108 | if loglevel == 'debug' or loglevel == logging.DEBUG: |
---|
109 | loglevel = logging.DEBUG |
---|
110 | elif loglevel == 'critical' or loglevel == logging.CRITICAL: |
---|
111 | loglevel = logging.CRITICAL |
---|
112 | elif loglevel == 'error' or loglevel == logging.ERROR: |
---|
113 | loglevel = logging.ERROR |
---|
114 | elif loglevel == 'warning' or loglevel == logging.WARNING: |
---|
115 | loglevel = logging.WARNING |
---|
116 | elif loglevel == 'info' or loglevel == logging.INFO: |
---|
117 | loglevel = logging.INFO |
---|
118 | else: |
---|
119 | loglevel = logging.DEBUG |
---|
120 | else: |
---|
121 | loglevel = logging.DEBUG |
---|
122 | else: |
---|
123 | loglevel = logging.INFO |
---|
124 | |
---|
125 | |
---|
126 | LOGGING = { |
---|
127 | 'version': 1, |
---|
128 | 'disable_existing_loggers': False, |
---|
129 | 'formatters': { |
---|
130 | 'verbose': { |
---|
131 | # 'format': '%(levelname)s %(module)s (%(pathname)s:%(lineno)d) ' + str_formatter + ' %(message)s' |
---|
132 | 'format': '%(levelname)s %(module)s ' + str_formatter + ' %(message)s' |
---|
133 | }, |
---|
134 | }, |
---|
135 | 'handlers': { |
---|
136 | 'stdout': { |
---|
137 | 'class': 'logging.StreamHandler', |
---|
138 | 'stream': sys.stdout, |
---|
139 | 'formatter': 'verbose', |
---|
140 | }, |
---|
141 | 'sys-logger6': { |
---|
142 | 'class': 'logging.handlers.SysLogHandler', |
---|
143 | 'address': '/dev/log', |
---|
144 | 'facility': "local6", |
---|
145 | 'formatter': 'verbose', |
---|
146 | }, |
---|
147 | }, |
---|
148 | 'loggers': { |
---|
149 | 'analytics-logger': { |
---|
150 | 'handlers': ['sys-logger6', 'stdout'], |
---|
151 | 'level': loglevel, |
---|
152 | 'propagate': True, |
---|
153 | }, |
---|
154 | } |
---|
155 | } |
---|
156 | |
---|
157 | |
---|
158 | cfg.dictConfig(LOGGING) |
---|
159 | log = logging.getLogger('analytics-logger') |
---|
160 | |
---|
161 | |
---|
162 | def print_config(config): |
---|
163 | global log |
---|
164 | |
---|
165 | for sect in config: |
---|
166 | for key in config[sect]: |
---|
167 | log.debug("Config Loaded: '{}' '{}' '{}'".format(sect, key, config[sect][key])) |
---|
168 | |
---|
169 | |
---|
170 | def init_config(): |
---|
171 | |
---|
172 | config = configparser.ConfigParser() |
---|
173 | config.read(CONFIGFILE) |
---|
174 | |
---|
175 | return config |
---|
176 | |
---|
177 | |
---|
178 | def init_logging(config): |
---|
179 | global DEBUG, log |
---|
180 | |
---|
181 | if get_var_value('DEBUG', config): |
---|
182 | DEBUG = True |
---|
183 | log.setLevel(loglevel) |
---|
184 | print_config(config) |
---|
185 | |
---|
186 | |
---|
187 | def bin_to_ascii(value): |
---|
188 | global log |
---|
189 | |
---|
190 | try: |
---|
191 | if isinstance(value, bytes): |
---|
192 | return value.decode('utf-8') |
---|
193 | else: |
---|
194 | return value |
---|
195 | except Exception as e: |
---|
196 | log.error('Error bin_to_ascii {}'.format(e)) |
---|
197 | return value |
---|
198 | |
---|
199 | |
---|
200 | def get_llx_version(): |
---|
201 | global log |
---|
202 | |
---|
203 | output = bin_to_ascii(subprocess.check_output(['lliurex-version', '-n'])) |
---|
204 | release = output[0:2].strip() |
---|
205 | if release == '15': |
---|
206 | use = 'lliurex-detect' |
---|
207 | else: |
---|
208 | use = 'lliurex-version' |
---|
209 | output = bin_to_ascii(subprocess.check_output([use, '-f'])) |
---|
210 | flavour = output.strip() |
---|
211 | log.info("Detected release:'{}' flavour:'{}'".format(release, flavour)) |
---|
212 | return release, flavour |
---|
213 | |
---|
214 | |
---|
215 | def detect_proxy(): |
---|
216 | global log |
---|
217 | |
---|
218 | px = subprocess.Popen(["bash", "-c", "source /etc/profile && echo $http_proxy"], stdout=subprocess.PIPE) |
---|
219 | proxy = bin_to_ascii(px.stdout.readline()).strip() |
---|
220 | log.info("Detected proxy: '{}'".format(proxy)) |
---|
221 | return proxy |
---|
222 | |
---|
223 | |
---|
224 | def daemonize(*args, **kwargs): |
---|
225 | global glob, log, CONFIG |
---|
226 | |
---|
227 | log.info('Running daemon mode...') |
---|
228 | filelock = get_var_value('filelock', CONFIG) |
---|
229 | if not filelock: |
---|
230 | filelock = '/var/run/analytics' |
---|
231 | |
---|
232 | try: |
---|
233 | with daemon.DaemonContext(detach_process=True, |
---|
234 | working_directory='/tmp', |
---|
235 | umask=0o002, |
---|
236 | pidfile=lockfile.FileLock(filelock), |
---|
237 | files_preserve=[log.handlers[0].socket.fileno()]): |
---|
238 | start(daemon_mode=True) |
---|
239 | except Exception as e: |
---|
240 | log.critical("Error daemonizing {}".format(e)) |
---|
241 | sys.exit(1) |
---|
242 | |
---|
243 | |
---|
244 | def add_item(item,regexp): |
---|
245 | global glob, log |
---|
246 | |
---|
247 | log.debug('Request to add {}'.format(item)) |
---|
248 | |
---|
249 | parts_item = item.split(' ') |
---|
250 | |
---|
251 | executable = None |
---|
252 | for i in range(len(parts_item)): |
---|
253 | executable = parts_item[i].strip() |
---|
254 | if not re.match(executable[0]): |
---|
255 | log.debug('Skipping malformed executable {}'.format(executable)) |
---|
256 | executable = None |
---|
257 | continue |
---|
258 | if '/' in executable: |
---|
259 | executable = executable.split('/')[-1] |
---|
260 | log.debug('Trimming executable to {}'.format(executable)) |
---|
261 | if executable in glob['INTERPRETERS']: |
---|
262 | log.debug('Trimming interpreter part {}'.format(executable)) |
---|
263 | executable = None |
---|
264 | continue |
---|
265 | else: |
---|
266 | if executable in glob['BLACKLIST']: |
---|
267 | log.debug('Skipping add due to blacklisted command {}'.format(executable)) |
---|
268 | return None |
---|
269 | log.debug('Valid executable {}'.format(executable)) |
---|
270 | break |
---|
271 | |
---|
272 | the_list = glob['LIST'] |
---|
273 | if executable: |
---|
274 | if executable in the_list: |
---|
275 | the_list[executable] = the_list[executable] + 1 |
---|
276 | log.info('+++ Incrementing {} = {}'.format(executable, the_list[executable])) |
---|
277 | else: |
---|
278 | log.info('*** Adding {} = 1'.format(executable, 1)) |
---|
279 | the_list[executable] = 1 |
---|
280 | glob['LIST'] = the_list |
---|
281 | |
---|
282 | |
---|
283 | def monitor(): |
---|
284 | global glob, log |
---|
285 | |
---|
286 | log.info('Start monitor') |
---|
287 | logfilename = get_var_value('file', glob['config'], section='Audit') |
---|
288 | |
---|
289 | glob['BLACKLIST'] = get_var_value('blacklist', glob['config'], section='Audit') |
---|
290 | glob['INTERPRETERS'] = get_var_value('interpreters', glob['config'], section='Audit') |
---|
291 | |
---|
292 | try: |
---|
293 | glob['INTERPRETERS'] = [x.strip() for x in glob['INTERPRETERS'].split(',')] |
---|
294 | except Exception as e: |
---|
295 | log.error('Malformed interpreters list ,{}'.format(e)) |
---|
296 | glob['INTERPRETERS'] = [] |
---|
297 | return None |
---|
298 | |
---|
299 | try: |
---|
300 | with open(glob['BLACKLIST'], 'r') as fp: |
---|
301 | glob['BLACKLIST'] = [line.strip() for line in fp] |
---|
302 | except Exception as e: |
---|
303 | log.error('Unable to read blacklist from {} , {}'.format(glob['BLACKLIST'], e)) |
---|
304 | glob['BLACKLIST'] = [] |
---|
305 | return None |
---|
306 | |
---|
307 | try: |
---|
308 | if not (os.path.isfile(logfilename) and os.access(logfilename, os.R_OK)): |
---|
309 | log.critical('File {} not readable'.format(logfilename)) |
---|
310 | glob['TERMINATE'] = True |
---|
311 | |
---|
312 | fp = subprocess.Popen(['tail', '-F', logfilename], stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')) |
---|
313 | except Exception as e: |
---|
314 | log.critical('Error initializing {} read, {}'.format(logfilename, e)) |
---|
315 | glob['TERMINATE'] = True |
---|
316 | return None |
---|
317 | |
---|
318 | try: |
---|
319 | log.info('Starting monitoring {}'.format(logfilename)) |
---|
320 | regexp = re.compile('^[a-zA-Z][a-zA-Z0-9_.+\-]+$') |
---|
321 | while not glob['TERMINATE']: |
---|
322 | if fp.poll() is not None: |
---|
323 | log.error('Dead subprocess monitoring {}'.format(logfilename)) |
---|
324 | fp = subprocess.Popen(['tail', '-F', logfilename], stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')) |
---|
325 | else: |
---|
326 | line = bin_to_ascii(fp.stdout.readline()).strip() |
---|
327 | if re.search('type=EXECVE', line): |
---|
328 | m = re.findall('a[0-9]+="([^"]+)"', line) |
---|
329 | if m: |
---|
330 | captured = ' '.join(m) |
---|
331 | add_item(captured,regexp) |
---|
332 | |
---|
333 | except Exception as e: |
---|
334 | if isinstance(e, ConnectionResetError): |
---|
335 | log.info('Connection reset exitting monitor thread') |
---|
336 | glob['TERMINATE'] = True |
---|
337 | return |
---|
338 | else: |
---|
339 | log.error('Error reading file {}, {}'.format(logfilename, e)) |
---|
340 | glob['TERMINATE'] = True |
---|
341 | return |
---|
342 | |
---|
343 | log.info('Exitting monitor thread') |
---|
344 | return |
---|
345 | |
---|
346 | def update_list(): |
---|
347 | global glob, log |
---|
348 | |
---|
349 | log.info('Start update list') |
---|
350 | try: |
---|
351 | list_path = get_var_value('list_path', glob['config'], mode='string', section='Server') |
---|
352 | server = get_var_value('server', glob['config'], mode='string', section='Server') |
---|
353 | url = 'http://' + server + '/' + list_path |
---|
354 | agent = glob['user_agent'] |
---|
355 | headers = {'user-agent': agent} |
---|
356 | except Exception as e: |
---|
357 | log.warning('Error gettting update list settings {}'.format(e)) |
---|
358 | |
---|
359 | log.debug('List path {}'.format(url)) |
---|
360 | |
---|
361 | tick = 1 |
---|
362 | timeout = 60 * 60 * 12 |
---|
363 | c = 10 |
---|
364 | |
---|
365 | while not glob['TERMINATE']: |
---|
366 | time.sleep(tick) |
---|
367 | if c > 0: |
---|
368 | c = c - tick |
---|
369 | else: |
---|
370 | c = timeout |
---|
371 | |
---|
372 | sent = False |
---|
373 | rq = None |
---|
374 | |
---|
375 | try: |
---|
376 | if glob['use_proxy']: |
---|
377 | proxy_obj = dict() |
---|
378 | proxy_obj.setdefault('http', glob['proxy']) |
---|
379 | |
---|
380 | rq = requests.get(url, headers=headers, proxies=proxy_obj, timeout=5) |
---|
381 | sent = True |
---|
382 | else: |
---|
383 | rq = requests.get(url, headers=headers, timeout=5) |
---|
384 | sent = True |
---|
385 | except Exception as e: |
---|
386 | log.warning('Error getting list from {}, {}'.format(url,e)) |
---|
387 | |
---|
388 | try: |
---|
389 | blist = glob['BLACKLIST'] |
---|
390 | except Exception as e: |
---|
391 | log.error('Error loading current blacklist on update_list, {}'.format(e)) |
---|
392 | |
---|
393 | try: |
---|
394 | the_list = glob['LIST'] |
---|
395 | except Exception as e: |
---|
396 | log.error('Error loading current applist on update_list, {}'.format(e)) |
---|
397 | |
---|
398 | if sent and rq: |
---|
399 | result = rq.text |
---|
400 | try: |
---|
401 | json_list = json.loads(result) |
---|
402 | except Exception as e: |
---|
403 | log.warning('Wrong list received {}, {}'.format(result,e)) |
---|
404 | continue |
---|
405 | |
---|
406 | try: |
---|
407 | for item in json_list: |
---|
408 | if item not in blist: |
---|
409 | blist.append(item) |
---|
410 | log.info("Received item list '{}'".format(item)) |
---|
411 | if item in the_list: |
---|
412 | del the_list[item] |
---|
413 | log.info("Removed item from list '{}'".format(item)) |
---|
414 | glob['BLACKLIST'] = blist |
---|
415 | glob['LIST'] = the_list |
---|
416 | except Exception as e: |
---|
417 | log.error('Error updating blacklist, {}'.format(e)) |
---|
418 | else: |
---|
419 | log.warning('Unable to get list data') |
---|
420 | |
---|
421 | log.info('Exitting update list thread') |
---|
422 | |
---|
423 | |
---|
424 | def timed_send(): |
---|
425 | global glob, log |
---|
426 | |
---|
427 | log.debug('Start timed_send ') |
---|
428 | try: |
---|
429 | count = get_var_value('timeout', glob['config'], mode='int') |
---|
430 | if count < 0: |
---|
431 | log.warning('Not valid timeout value setting default 300') |
---|
432 | except Exception as e: |
---|
433 | log.warning('Unable to read timeout value defaulting to 300, {}'.format(e)) |
---|
434 | count = 300 |
---|
435 | |
---|
436 | log.info('Initialized timed send with value {} seconds'.format(count)) |
---|
437 | c = count |
---|
438 | tick = 0.2 |
---|
439 | try: |
---|
440 | while not glob['TERMINATE']: |
---|
441 | while glob['PRINTING'] == True: |
---|
442 | time.sleep(1) |
---|
443 | time.sleep(tick) |
---|
444 | if c > 0: |
---|
445 | c = c - tick |
---|
446 | else: |
---|
447 | c = count |
---|
448 | log.debug('Triggering timed send') |
---|
449 | clean_and_send() |
---|
450 | |
---|
451 | except Exception as e: |
---|
452 | if isinstance(e, ConnectionResetError): |
---|
453 | log.info('Connection reset exitting timer thread') |
---|
454 | else: |
---|
455 | log.error('Error with timed send, {}'.format(e)) |
---|
456 | |
---|
457 | log.info('Exitting timer thread') |
---|
458 | return |
---|
459 | |
---|
460 | |
---|
461 | def start(*args, daemon_mode=False, **kwargs): |
---|
462 | global THREADS, oldsignals, log, CONFIG, glob |
---|
463 | |
---|
464 | log.info("Starting analytics") |
---|
465 | |
---|
466 | mgr = Manager() |
---|
467 | glob = mgr.dict() |
---|
468 | |
---|
469 | glob['DAEMON_MODE'] = daemon_mode |
---|
470 | glob['config'] = CONFIG |
---|
471 | |
---|
472 | try: |
---|
473 | glob['release'], glob['flavour'] = get_llx_version() |
---|
474 | except Exception as e: |
---|
475 | log.error('Error getting llx version {}'.format(e)) |
---|
476 | glob['release'] = 'Unknown' |
---|
477 | glob['flavour'] = 'Unknown' |
---|
478 | |
---|
479 | try: |
---|
480 | glob['proxy'] = detect_proxy() |
---|
481 | if glob['proxy'] == '': |
---|
482 | log.info('Not using proxy') |
---|
483 | glob['use_proxy'] = False |
---|
484 | else: |
---|
485 | glob['use_proxy'] = True |
---|
486 | except Exception as e: |
---|
487 | log.warning('Error detecting proxy {}'.format(e)) |
---|
488 | glob['use_proxy'] = False |
---|
489 | |
---|
490 | pidfile = get_var_value('pidfile', glob['config']) |
---|
491 | |
---|
492 | try: |
---|
493 | server = get_var_value('server', glob['config'], section='Server') |
---|
494 | server_path = get_var_value('server-path', glob['config'], section='Server') |
---|
495 | if server.strip() == '' or server_path.strip() == '': |
---|
496 | raise Exception('Empty server or server-path') |
---|
497 | glob['server'] = server |
---|
498 | glob['server_path'] = server_path |
---|
499 | except Exception as e: |
---|
500 | log.critical('Error getting server url, {}'.format(e)) |
---|
501 | |
---|
502 | try: |
---|
503 | agent = get_var_value('user-agent', glob['config']) |
---|
504 | if agent.strip() == '' or agent == 'None': |
---|
505 | agent = 'lliurex-analytics-agent' |
---|
506 | glob['user_agent'] = agent |
---|
507 | except Exception as e: |
---|
508 | log.warning('Error getting user-agent, {}'.format(e)) |
---|
509 | |
---|
510 | # write pid |
---|
511 | try: |
---|
512 | with open(pidfile, 'w') as fp: |
---|
513 | fp.write(str(os.getpid())) |
---|
514 | except Exception as e: |
---|
515 | log.error('Error writting pidfile {}'.format(e)) |
---|
516 | |
---|
517 | glob['TERMINATE'] = False |
---|
518 | glob['PRINTING'] = False |
---|
519 | glob['LIST'] = {} |
---|
520 | |
---|
521 | glob['platform_data'] = get_platform_data() |
---|
522 | |
---|
523 | THREADS = dict() |
---|
524 | THREADS['monitor'] = Process(target=monitor, name='monitor') |
---|
525 | THREADS['monitor'].daemon = glob['DAEMON_MODE'] |
---|
526 | |
---|
527 | THREADS['timed_send'] = Process(target=timed_send, name='timed_send') |
---|
528 | THREADS['timed_send'].daemon = glob['DAEMON_MODE'] |
---|
529 | |
---|
530 | THREADS['update_list'] = Process(target=update_list, name='update_list') |
---|
531 | THREADS['update_list'].daemon = glob['DAEMON_MODE'] |
---|
532 | |
---|
533 | THREADS['monitor'].start() |
---|
534 | THREADS['timed_send'].start() |
---|
535 | THREADS['update_list'].start() |
---|
536 | |
---|
537 | signals = {'SIGTERM': interrupt, 'SIGINT': interrupt, 'SIGUSR1': clean_and_send, 'SIGUSR2': show_captured} |
---|
538 | for sig in oldsignals: |
---|
539 | if sig in signals: |
---|
540 | signal.signal(signal.__dict__[sig], signals[sig]) |
---|
541 | else: |
---|
542 | try: |
---|
543 | signal.signal(signal.__dict__[sig], oldsignals[sig]) |
---|
544 | except: |
---|
545 | continue |
---|
546 | |
---|
547 | |
---|
548 | def clean_and_send(*args, **kwargs): |
---|
549 | global glob, log |
---|
550 | |
---|
551 | override_send_permission = get_var_value('override_send_permission', glob['config'], mode='bool') |
---|
552 | |
---|
553 | if allow_send() or override_send_permission: |
---|
554 | send_data(glob['LIST']) |
---|
555 | else: |
---|
556 | log.info('Sending not allowed when try to send results') |
---|
557 | glob['LIST'] = {} |
---|
558 | |
---|
559 | |
---|
560 | def get_mac(): |
---|
561 | global log |
---|
562 | |
---|
563 | dirmac = '/sys/class/net' |
---|
564 | eth = 'eth0' |
---|
565 | filemac = 'address' |
---|
566 | file = '{}/{}/{}'.format(dirmac, eth, filemac) |
---|
567 | uid = None |
---|
568 | try: |
---|
569 | with open(file, 'r') as fp: |
---|
570 | uid = bin_to_ascii(fp.read()).strip() |
---|
571 | except: |
---|
572 | log.warning('Unable to read {}'.format(file)) |
---|
573 | eth = sorted(os.listdir(dirmac)) |
---|
574 | if len(eth) > 0: |
---|
575 | eth = eth[0] |
---|
576 | file = '{}/{}/{}'.format(dirmac, eth, filemac) |
---|
577 | try: |
---|
578 | with open(file, 'r') as fp: |
---|
579 | uid = bin_to_ascii(fp.read()).strip() |
---|
580 | except Exception as e: |
---|
581 | log.error('Unable to read mac address, {}'.format(e)) |
---|
582 | |
---|
583 | return str(uid) |
---|
584 | |
---|
585 | |
---|
586 | def get_cpu(): |
---|
587 | global log |
---|
588 | |
---|
589 | file = '/proc/cpuinfo' |
---|
590 | cpu = {} |
---|
591 | try: |
---|
592 | with open(file, 'r') as fp: |
---|
593 | for line in fp: |
---|
594 | if re.search('^processor\s+:\s+([0-9]+)$', line): |
---|
595 | m = re.findall('^processor\s+:\s+([0-9]+)', line) |
---|
596 | if m and len(m) > 0: |
---|
597 | cpu['ncpus'] = int(m[0]) + 1 |
---|
598 | if re.search('^model name\s+:\s+(.+)$', line): |
---|
599 | m = re.findall('^model name\s+:\s+(.+)$', line) |
---|
600 | if m and len(m) > 0: |
---|
601 | cpu['model'] = str(m[0]) |
---|
602 | except Exception as e: |
---|
603 | log.warning('Unable to read cpuinfo, {}'.format(e)) |
---|
604 | cpu = None |
---|
605 | return cpu |
---|
606 | |
---|
607 | |
---|
608 | def get_mem(): |
---|
609 | global log |
---|
610 | |
---|
611 | file = '/proc/meminfo' |
---|
612 | mem = None |
---|
613 | |
---|
614 | try: |
---|
615 | with open(file, 'r') as fp: |
---|
616 | for line in fp: |
---|
617 | if re.search('^MemTotal:\s+([0-9]+)\s+\S+$', line): |
---|
618 | m = re.findall('^MemTotal:\s+([0-9]+)\s+\S+$', line) |
---|
619 | if m and len(m) > 0: |
---|
620 | mem = int(m[0]) |
---|
621 | break |
---|
622 | except Exception as e: |
---|
623 | log.warning('Unable to read meminfo, {}'.format(e)) |
---|
624 | mem = None |
---|
625 | return str(mem) |
---|
626 | |
---|
627 | |
---|
628 | def get_vga(): |
---|
629 | global log |
---|
630 | |
---|
631 | vga = None |
---|
632 | try: |
---|
633 | out = bin_to_ascii(subprocess.check_output(['lspci'])).split('\n') |
---|
634 | for line in out: |
---|
635 | line_strip = line.strip() |
---|
636 | if re.search('VGA', line_strip, re.IGNORECASE): |
---|
637 | m = re.findall('^\S+\s(.+)$', line_strip) |
---|
638 | if m and len(m) > 0: |
---|
639 | vga = m[0] |
---|
640 | break |
---|
641 | except Exception as e: |
---|
642 | log.warning('Unable to read pciinfo, {}'.format(e)) |
---|
643 | vga = None |
---|
644 | return str(vga) |
---|
645 | |
---|
646 | |
---|
647 | def get_arch(): |
---|
648 | global log |
---|
649 | |
---|
650 | arch = None |
---|
651 | try: |
---|
652 | arch = bin_to_ascii(subprocess.check_output(['uname', '-m'])).strip() |
---|
653 | except Exception as e: |
---|
654 | log.warning('Unable to read architecture, {}'.format(e)) |
---|
655 | arch = None |
---|
656 | return str(arch) |
---|
657 | |
---|
658 | |
---|
659 | def get_platform_data(): |
---|
660 | global log |
---|
661 | |
---|
662 | data = {} |
---|
663 | data.setdefault('mac', get_mac()) |
---|
664 | data.setdefault('cpu', get_cpu()) |
---|
665 | data.setdefault('mem', get_mem()) |
---|
666 | data.setdefault('vga', get_vga()) |
---|
667 | data.setdefault('arch', get_arch()) |
---|
668 | |
---|
669 | log.debug("Detected mac='{}' arch='{}' cpu='{}' mem='{}' vga='{}'".format(data['mac'], data['arch'], data['cpu'], data['mem'], data['vga'])) |
---|
670 | return data |
---|
671 | |
---|
672 | |
---|
673 | def send_data(data): |
---|
674 | global log, glob |
---|
675 | |
---|
676 | log.debug('sending specs {}'.format(glob['platform_data'])) |
---|
677 | log.debug('sending data {}'.format(glob['LIST'])) |
---|
678 | |
---|
679 | agent = glob['user_agent'] |
---|
680 | url = 'http://' + glob['server'] + '/' + glob['server_path'] |
---|
681 | headers = {'user-agent': agent} |
---|
682 | |
---|
683 | version = glob['release'] |
---|
684 | flavour = glob['flavour'] |
---|
685 | |
---|
686 | list_data = data |
---|
687 | try: |
---|
688 | json_list_data = json.dumps(list_data) |
---|
689 | except Exception as e: |
---|
690 | log.error('Json error on internal data list') |
---|
691 | return None |
---|
692 | |
---|
693 | platform_data = glob['platform_data'] |
---|
694 | uid = platform_data['mac'] |
---|
695 | |
---|
696 | data_to_send = dict() |
---|
697 | data_to_send.setdefault('uid', uid) |
---|
698 | data_to_send.setdefault('vers', version) |
---|
699 | data_to_send.setdefault('sab', flavour) |
---|
700 | data_to_send.setdefault('specs', platform_data) |
---|
701 | data_to_send.setdefault('stats', json_list_data) |
---|
702 | |
---|
703 | try: |
---|
704 | json_data_to_send = json.dumps(data_to_send) |
---|
705 | except Exception as e: |
---|
706 | log.error('Json error on data to send') |
---|
707 | return None |
---|
708 | |
---|
709 | payload = {'stats': json_data_to_send} |
---|
710 | log.debug('Payload to send: {}'.format(payload)) |
---|
711 | |
---|
712 | sent = False |
---|
713 | rq = None |
---|
714 | if glob['use_proxy']: |
---|
715 | proxy_obj = dict() |
---|
716 | proxy_obj.setdefault('http', glob['proxy']) |
---|
717 | try: |
---|
718 | rq = requests.post(url, data=payload, headers=headers, proxies=proxy_obj, timeout=5) |
---|
719 | sent = True |
---|
720 | except Exception as e: |
---|
721 | log.error('Error sending data through proxy, {}'.format(e)) |
---|
722 | |
---|
723 | if not glob['use_proxy'] or sent == False: |
---|
724 | try: |
---|
725 | rq = requests.post(url, data=payload, headers=headers, timeout=5) |
---|
726 | sent = True |
---|
727 | except Exception as e: |
---|
728 | log.error('Error sending data, {}'.format(e)) |
---|
729 | |
---|
730 | if sent and rq: |
---|
731 | result = rq.text |
---|
732 | result = result.strip().lower() |
---|
733 | if result == 'ok': |
---|
734 | log.debug('Sending was success with reply OK ') |
---|
735 | elif result == 'nok': |
---|
736 | log.info('Sending was success but reply is NOK ') |
---|
737 | else: |
---|
738 | log.warning("Sending was success but reply is unknown '{}'".format(result)) |
---|
739 | else: |
---|
740 | log.warning('Unable to send data') |
---|
741 | |
---|
742 | |
---|
743 | def interrupt(*args, **kwargs): |
---|
744 | global glob, log, THREADS |
---|
745 | |
---|
746 | log.info('Interrupting analytics') |
---|
747 | try: |
---|
748 | clean_and_send() |
---|
749 | try: |
---|
750 | glob['TERMINATE'] = True |
---|
751 | except: |
---|
752 | log.error('Requested kill the program') |
---|
753 | sys.exit(1) |
---|
754 | for x in THREADS: |
---|
755 | THREADS[x].join() |
---|
756 | |
---|
757 | except Exception as e: |
---|
758 | log.error('Error while interrupting, {}'.format(e)) |
---|
759 | |
---|
760 | |
---|
761 | def show_captured(*args, **kwargs): |
---|
762 | global glob, log |
---|
763 | |
---|
764 | glob['PRINTING'] = True |
---|
765 | log.info('Requested to show list') |
---|
766 | |
---|
767 | list_items = glob['LIST'] |
---|
768 | if not isinstance(list_items, dict): |
---|
769 | log.warning('Error showing captured items, LIST is not a dictionary') |
---|
770 | |
---|
771 | listkeys_sorted = sorted(list_items, key=list_items.get, reverse=True) |
---|
772 | |
---|
773 | if len(listkeys_sorted) > 0: |
---|
774 | log.info('analytics is showing currently capture list in memory') |
---|
775 | for i in listkeys_sorted: |
---|
776 | log.info('{} = {}'.format(i, list_items.get(i))) |
---|
777 | else: |
---|
778 | log.info('analytics detect an empty capture list in memory') |
---|
779 | |
---|
780 | glob['PRINTING'] = False |
---|
781 | |
---|
782 | |
---|
783 | def check_server_acknowledge(): |
---|
784 | global log |
---|
785 | |
---|
786 | try: |
---|
787 | c = client.ServerProxy("https://server:9779/", |
---|
788 | verbose=False, |
---|
789 | use_datetime=True, |
---|
790 | context=ssl._create_unverified_context()) |
---|
791 | return c.get_variable("", "VariablesManager", "STATS_ENABLED") |
---|
792 | except Exception as e: |
---|
793 | log.error('Error getting variables, {}'.format(e)) |
---|
794 | return None |
---|
795 | |
---|
796 | |
---|
797 | def check_local_acknowledge(): |
---|
798 | global glob, log |
---|
799 | |
---|
800 | if glob['TERMINATE']: |
---|
801 | return None |
---|
802 | |
---|
803 | try: |
---|
804 | statusfile = get_var_value('statusfile', glob['config']) |
---|
805 | if str(statusfile) == 'None': |
---|
806 | statusfile = '/etc/lliurex-analytics/status' |
---|
807 | log.warning('Warning statusfile not set, defaulting to {}'.format(statusfile)) |
---|
808 | except Exception as e: |
---|
809 | log.error('Error getting value for statusfile, {}'.format(e)) |
---|
810 | |
---|
811 | answer = None |
---|
812 | try: |
---|
813 | |
---|
814 | if os.path.isfile(statusfile): |
---|
815 | fp = open(statusfile, 'r') |
---|
816 | answer = fp.readline() |
---|
817 | fp.close() |
---|
818 | else: |
---|
819 | log.error('wrong statusfile {}'.format(statusfile)) |
---|
820 | return None |
---|
821 | |
---|
822 | return answer.strip() |
---|
823 | except Exception as e: |
---|
824 | log.warning('Error reading status file, {}'.format(e)) |
---|
825 | return None |
---|
826 | |
---|
827 | |
---|
828 | def allow_send(): |
---|
829 | global glob, log |
---|
830 | |
---|
831 | if glob['TERMINATE']: |
---|
832 | return False |
---|
833 | |
---|
834 | if glob['flavour'].lower() == 'server': |
---|
835 | answer = str(check_server_acknowledge()) |
---|
836 | answer = answer.strip() |
---|
837 | if answer == '1': |
---|
838 | log.info('Allowed to send stats checking server acknowledge') |
---|
839 | return True |
---|
840 | elif answer == '0': |
---|
841 | log.info('Denied to send stats checking server acknowledge') |
---|
842 | return False |
---|
843 | elif answer == 'None': |
---|
844 | pass |
---|
845 | else: |
---|
846 | log.info('Unknown value checking server acknowledge, {}'.format(answer)) |
---|
847 | answer = str(check_local_acknowledge()).lower() |
---|
848 | answer = answer.strip() |
---|
849 | if answer == 'yes': |
---|
850 | log.info('Allowed to send stats checking local acknowledge') |
---|
851 | return True |
---|
852 | elif answer == 'no': |
---|
853 | log.info('Denied to send stats checking local acknowledge') |
---|
854 | return False |
---|
855 | elif answer == '': |
---|
856 | pass |
---|
857 | else: |
---|
858 | log.info('Unknown value checking local acknowledge, {}'.format(answer)) |
---|
859 | |
---|
860 | log.info('Denied to send stats by default') |
---|
861 | return False |
---|
862 | |
---|
863 | |
---|
864 | if __name__ == '__main__': |
---|
865 | exit = False |
---|
866 | keyword='analytics' |
---|
867 | for proc in psutil.process_iter(): |
---|
868 | for argument in proc.cmdline(): |
---|
869 | #print('{} {} {}'.format(proc.cmdline(),keyword,argument[-len(keyword):])) |
---|
870 | if keyword in argument[-len(keyword):]: |
---|
871 | exit = True |
---|
872 | break |
---|
873 | if exit: |
---|
874 | break |
---|
875 | if exit: |
---|
876 | log.error('Another daemon is running') |
---|
877 | sys.exit(1) |
---|
878 | |
---|
879 | try: |
---|
880 | CONFIG = init_config() |
---|
881 | except Exception as e: |
---|
882 | print('Error initializing config analytics {}'.format(e), file=sys.stderr) |
---|
883 | sys.exit(1) |
---|
884 | try: |
---|
885 | init_logging(CONFIG) |
---|
886 | except Exception as e: |
---|
887 | print('Error initializing logging analytics {}'.format(e), file=sys.stderr) |
---|
888 | sys.exit(1) |
---|
889 | |
---|
890 | THREADS = {} |
---|
891 | |
---|
892 | DAEMON_MODE = get_var_value('DAEMON_MODE', CONFIG, 'bool') |
---|
893 | |
---|
894 | if DAEMON_MODE: |
---|
895 | daemonize(daemon_mode=True) |
---|
896 | else: |
---|
897 | start(daemon_mode=False) |
---|
898 | |
---|
899 | log.debug('End main') |
---|
900 | ended = False |
---|
901 | while not ended: |
---|
902 | for t in THREADS: |
---|
903 | THREADS[t].join() |
---|
904 | if THREADS[t].is_alive(): |
---|
905 | ended = False |
---|
906 | break |
---|
907 | else: |
---|
908 | ended = True |
---|
909 | continue |
---|
910 | |
---|
911 | log.info('Exitting analytics') |
---|
912 | sys.exit(0) |
---|