#!/usr/bin/env python import sys if(len(sys.argv)>1): PROJECT_PATH = sys.argv[1] sys.path.append(PROJECT_PATH) else: print("Error initializing Manager, no path given!"); sys.exit(1) import datetime import hashlib import importlib import json import logging import logging.config import os import pika import threading import time from tools import config from tools import utils from tools.db import database as db from sqlalchemy import text class Manager: def __init__(self): try: #TODO: this should be nicer... logging.config.fileConfig(os.path.join(PROJECT_PATH, 'logging.conf'), defaults={'logfilename': 'manager.log'}) except Exception as e: print("Error while trying to load config file for logging") logging.info("Initializing manager") try: config.load(PROJECT_PATH +"/manager/config.json") except ValueError: # Config file can't be loaded, e.g. no valid JSON logging.exception("Wasn't able to load config file, exiting...") quit() try: db.connect(PROJECT_PATH) db.setup() except: logging.exception("Couldn't connect to database!") quit() self.notifiers = [] self.received_data_counter = 0 self.alarm_dir = "/var/tmp/secpi/alarms" self.current_alarm_dir = "/var/tmp/secpi/alarms" try: self.data_timeout = int(config.get("data_timeout")) except Exception: # if not specified in the config file we set a default value self.data_timeout = 180 logging.debug("Couldn't find or use config parameter for data timeout in manager config file. Setting default value: %d" % self.data_timeout) try: self.holddown_timer = int(config.get("holddown_timer")) except Exception: # if not specified in the config file we set a default value self.holddown_timer = 210 logging.debug("Couldn't find or use config parameter for holddown timer in manager config file. Setting default value: %d" % self.holddown_timer) self.holddown_state = False self.num_of_workers = 0 self.connect() # debug output, setups & state setups = db.session.query(db.objects.Setup).all() rebooted = False for setup in setups: logging.debug("name: %s active:%s" % (setup.name, setup.active_state)) if setup.active_state: rebooted = True if rebooted: self.setup_notifiers() self.num_of_workers = db.session.query(db.objects.Worker).join((db.objects.Action, db.objects.Worker.actions)).filter(db.objects.Worker.active_state == True).filter(db.objects.Action.active_state == True).count() logging.info("Setup done!") def connect(self): logging.debug("Initializing connection to rabbitmq service") credentials = pika.PlainCredentials(config.get('rabbitmq')['user'], config.get('rabbitmq')['password']) parameters = pika.ConnectionParameters(credentials=credentials, host=config.get('rabbitmq')['master_ip'], port=5671, ssl=True, socket_timeout=10, ssl_options = { "ca_certs":PROJECT_PATH+"/certs/"+config.get('rabbitmq')['cacert'], "certfile":PROJECT_PATH+"/certs/"+config.get('rabbitmq')['certfile'], "keyfile":PROJECT_PATH+"/certs/"+config.get('rabbitmq')['keyfile'] } ) connected = False while not connected: #retry if establishing a connection fails try: self.connection = pika.BlockingConnection(parameters=parameters) self.channel = self.connection.channel() connected = True logging.info("Connection to rabbitmq service established") except pika.exceptions.AMQPConnectionError as pe: # if connection can't be established logging.error("Wasn't able to connect to rabbitmq service: %s" % pe) time.sleep(30) #define exchange self.channel.exchange_declare(exchange=utils.EXCHANGE, exchange_type='direct') #define queues: data, alarm and action & config for every pi self.channel.queue_declare(queue=utils.QUEUE_DATA) self.channel.queue_declare(queue=utils.QUEUE_ALARM) self.channel.queue_declare(queue=utils.QUEUE_ON_OFF) self.channel.queue_declare(queue=utils.QUEUE_LOG) self.channel.queue_declare(queue=utils.QUEUE_INIT_CONFIG) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_ON_OFF) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_DATA) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_ALARM) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_LOG) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_INIT_CONFIG) # load workers from db workers = db.session.query(db.objects.Worker).all() for pi in workers: self.channel.queue_declare(queue=utils.QUEUE_ACTION+str(pi.id)) self.channel.queue_declare(queue=utils.QUEUE_CONFIG+str(pi.id)) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_ACTION+str(pi.id)) self.channel.queue_bind(exchange=utils.EXCHANGE, queue=utils.QUEUE_CONFIG+str(pi.id)) #define callbacks for alarm and data queues self.channel.basic_consume(self.got_alarm, queue=utils.QUEUE_ALARM, no_ack=True) self.channel.basic_consume(self.got_on_off, queue=utils.QUEUE_ON_OFF, no_ack=True) self.channel.basic_consume(self.got_data, queue=utils.QUEUE_DATA, no_ack=True) self.channel.basic_consume(self.got_log, queue=utils.QUEUE_LOG, no_ack=True) self.channel.basic_consume(self.got_config_request, queue=utils.QUEUE_INIT_CONFIG, no_ack=True) def start(self): disconnected = True while disconnected: try: disconnected = False self.channel.start_consuming() # blocking call except pika.exceptions.ConnectionClosed: # when connection is lost, e.g. rabbitmq not running logging.error("Lost connection to rabbitmq service") disconnected = True time.sleep(10) # reconnect timer logging.info("Trying to reconnect...") self.connect() def __del__(self): try: self.connection.close() except AttributeError: #If there is no connection object closing won't work logging.info("No connection cleanup possible") # see: http://stackoverflow.com/questions/1176136/convert-string-to-python-class-object def class_for_name(self, module_name, class_name): try: # load the module, will raise ImportError if module cannot be loaded m = importlib.import_module(module_name) # get the class, will raise AttributeError if class cannot be found c = getattr(m, class_name) return c except ImportError as ie: self.log_err("Couldn't import module %s: %s"%(module_name, ie)) except AttributeError as ae: self.log_err("Couldn't find class %s: %s"%(class_name, ae)) # this method is used to send messages to a queue def send_message(self, rk, body, **kwargs): try: self.channel.basic_publish(exchange=utils.EXCHANGE, routing_key=rk, body=body, **kwargs) logging.info("Sending data to %s" % rk) return True except Exception as e: logging.exception("Error while sending data to queue:\n%s" % e) return False # this method is used to send json messages to a queue def send_json_message(self, rk, body, **kwargs): try: properties = pika.BasicProperties(content_type='application/json') self.channel.basic_publish(exchange=utils.EXCHANGE, routing_key=rk, body=json.dumps(body), properties=properties, **kwargs) logging.info("Sending json data to %s" % rk) return True except Exception as e: logging.exception("Error while sending json data to queue:\n%s" % e) return False # helper method to create error log entry def log_err(self, msg): logging.exception(msg) log_entry = db.objects.LogEntry(level=utils.LEVEL_ERR, message=str(msg), sender="Manager") db.session.add(log_entry) db.session.commit() # helper method to create error log entry def log_msg(self, msg, level): logging.info(msg) log_entry = db.objects.LogEntry(level=level, message=str(msg), sender="Manager") db.session.add(log_entry) db.session.commit() def got_config_request(self, ch, method, properties, body): ip_addresses = json.loads(body) logging.info("Got config request with following IP addresses: %s" % ip_addresses) pi_id = None worker = db.session.query(db.objects.Worker).filter(db.objects.Worker.address.in_(ip_addresses)).first() if worker: pi_id = worker.id logging.debug("Found worker id %s for IP address %s" % (pi_id, worker.address)) else: # wasn't able to find worker with given ip address(es) logging.error("Wasn't able to find worker for given IP adress(es)") reply_properties = pika.BasicProperties(correlation_id=properties.correlation_id) self.channel.basic_publish(exchange=utils.EXCHANGE, properties=reply_properties, routing_key=properties.reply_to, body="") return config = self.prepare_config(pi_id) logging.info("Sending intial config to worker with id %s" % pi_id) reply_properties = pika.BasicProperties(correlation_id=properties.correlation_id, content_type='application/json') self.channel.basic_publish(exchange=utils.EXCHANGE, properties=reply_properties, routing_key=properties.reply_to, body=json.dumps(config)) # callback method for when the manager recieves data after a worker executed its actions def got_data(self, ch, method, properties, body): logging.info("Got data") newFile_bytes = bytearray(body) if newFile_bytes: #only write data when body is not empty try: newFile = open("%s/%s.zip" % (self.current_alarm_dir, hashlib.md5(newFile_bytes).hexdigest()), "wb") newFile.write(newFile_bytes) logging.info("Data written") except IOError as ie: # File can't be written, e.g. permissions wrong, directory doesn't exist logging.exception("Wasn't able to write received data: %s" % ie) self.received_data_counter += 1 # callback for log messages def got_log(self, ch, method, properties, body): log = json.loads(body) logging.debug("Got log message from %s: %s"%(log['sender'], log['msg'])) log_entry = db.objects.LogEntry(level=log['level'], message=str(log['msg']), sender=log['sender'], logtime=utils.str_to_value(log['datetime'])) db.session.add(log_entry) db.session.commit() # callback for when a setup gets activated/deactivated def got_on_off(self, ch, method, properties, body): msg = json.loads(body) self.cleanup_notifiers() if(msg['active_state'] == True): self.setup_notifiers() logging.info("Activating setup: %s" % msg['setup_name']) workers = db.session.query(db.objects.Worker).filter(db.objects.Worker.active_state == True).all() for pi in workers: config = self.prepare_config(pi.id) # check if we are deactivating --> worker should be deactivated! if(msg['active_state'] == False): config["active"] = False logging.info("Deactivating setup: %s" % msg['setup_name']) self.send_json_message(utils.QUEUE_CONFIG+str(pi.id), config) logging.info("Sent config to worker %s"%pi.name) # callback method which gets called when a worker raises an alarm def got_alarm(self, ch, method, properties, body): msg = json.loads(body) late_arrival = utils.check_late_arrival(datetime.datetime.strptime(msg["datetime"], "%Y-%m-%d %H:%M:%S")) if not late_arrival: logging.info("Received alarm: %s"%body) else: logging.info("Received old alarm: %s"%body) if not self.holddown_state: # put into holddown holddown_thread = threading.Thread(name="thread-holddown", target=self.holddown) holddown_thread.start() self.current_alarm_dir = "%s/%s" % (self.alarm_dir, time.strftime("/%Y%m%d_%H%M%S")) try: os.makedirs(self.current_alarm_dir) logging.debug("Created directory for alarm: %s" % self.current_alarm_dir) except OSError as oe: # directory can't be created, e.g. permissions wrong, or already exists logging.exception("Wasn't able to create directory for current alarm: %s" % oe) self.received_data_counter = 0 # iterate over workers and send "execute" workers = db.session.query(db.objects.Worker).join((db.objects.Action, db.objects.Worker.actions)).filter(db.objects.Worker.active_state == True).filter(db.objects.Action.active_state == True).all() self.num_of_workers = len(workers) action_message = { "msg": "execute", "datetime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "late_arrival":late_arrival} for pi in workers: self.send_json_message(utils.QUEUE_ACTION+str(pi.id), action_message) worker = db.session.query(db.objects.Worker).filter(db.objects.Worker.id == msg['pi_id']).first() sensor = db.session.query(db.objects.Sensor).filter(db.objects.Sensor.id == msg['sensor_id']).first() # create log entry for db if not late_arrival: al = db.objects.Alarm(sensor_id=msg['sensor_id'], message=msg['message']) self.log_msg("New alarm from %s on sensor %s: %s"%( (worker.name if worker else msg['pi_id']) , (sensor.name if sensor else msg['sensor_id']) , msg['message']), utils.LEVEL_WARN) else: al = db.objects.Alarm(sensor_id=msg['sensor_id'], message="Late Alarm: %s" %msg['message']) self.log_msg("Old alarm from %s on sensor %s: %s"%( (worker.name if worker else msg['pi_id']) , (sensor.name if sensor else msg['sensor_id']) , msg['message']), utils.LEVEL_WARN) db.session.add(al) db.session.commit() # TODO: add information about late arrival of alarm notif_info = { "message": msg['message'], "sensor": (sensor.name if sensor else msg['sensor_id']), "sensor_id": msg['sensor_id'], "worker": (worker.name if worker else msg['pi_id']), "worker_id": msg['pi_id'] } # start timeout thread for workers to reply timeout_thread = threading.Thread(name="thread-timeout", target=self.notify, args=[notif_info]) timeout_thread.start() else: # --> holddown state self.log_msg("Alarm during holddown state from %s on sensor %s: %s"%(msg['pi_id'], msg['sensor_id'], msg['message']), utils.LEVEL_INFO) al = db.objects.Alarm(sensor_id=msg['sensor_id'], message="Alarm during holddown state: %s" % msg['message']) db.session.add(al) db.session.commit() # initialize the notifiers def setup_notifiers(self): notifiers = db.session.query(db.objects.Notifier).filter(db.objects.Notifier.active_state == True).all() for notifier in notifiers: params = {} for p in notifier.params: params[p.key] = p.value n = self.class_for_name(notifier.module, notifier.cl) noti = n(notifier.id, params) self.notifiers.append(noti) logging.info("Set up notifier %s" % notifier.cl) # timeout thread which sends the received data from workers def notify(self, info): for i in range(0, self.data_timeout): if self.received_data_counter < self.num_of_workers: #not all data here yet logging.debug("Waiting for data from workers: data counter: %d, #workers: %d" % (self.received_data_counter, self.num_of_workers)) time.sleep(1) else: logging.debug("Received all data from workers, canceling the timeout") break # continue code execution if self.received_data_counter < self.num_of_workers: self.log_msg("TIMEOUT: Only %d out of %d workers replied with data"%(self.received_data_counter, self.num_of_workers), utils.LEVEL_INFO) # let the notifiers do their work for notifier in self.notifiers: try: notifier.notify(info) except Exception as e: self.log_err("Error notifying %u: %s" % (notifier.id, e)) # go into holddown state, while in this state subsequent alarms are interpreted as one alarm def holddown(self): self.holddown_state = True for i in range(0, self.holddown_timer): time.sleep(1) logging.debug("Holddown is over") self.holddown_state = False # cleanup the notifiers def cleanup_notifiers(self): for n in self.notifiers: n.cleanup() self.notifiers = [] def prepare_config(self, pi_id): logging.info("Preparing config for worker with id %s" % pi_id) conf = { "pi_id": pi_id, "active": False, # default to false, will be overriden if should be true } sensors = db.session.query(db.objects.Sensor).join(db.objects.Zone).join((db.objects.Setup, db.objects.Zone.setups)).filter(db.objects.Setup.active_state == True).filter(db.objects.Sensor.worker_id == pi_id).all() # if we have sensors we are active if(len(sensors)>0): conf['active'] = True conf_sensors = [] for sen in sensors: para = {} # create params array for p in sen.params: para[p.key] = p.value conf_sen = { "id": sen.id, "name": sen.name, "module": sen.module, "class": sen.cl, "params": para } conf_sensors.append(conf_sen) conf['sensors'] = conf_sensors actions = db.session.query(db.objects.Action).join((db.objects.Worker, db.objects.Action.workers)).filter(db.objects.Worker.id == pi_id).filter(db.objects.Action.active_state == True).all() # if we have actions we are also active if(len(actions)>0): conf['active'] = True conf_actions = [] # iterate over all actions for act in actions: para = {} # create params array for p in act.params: para[p.key] = p.value conf_act = { "id": act.id, "module": act.module, "class": act.cl, "params": para } conf_actions.append(conf_act) conf['actions'] = conf_actions logging.info("Generated config: %s" % conf) return conf if __name__ == '__main__': try: mg = Manager() mg.start() except KeyboardInterrupt: logging.info("Shutting down manager!") # TODO: cleanup? if(mg): mg.cleanup_notifiers() try: sys.exit(0) except SystemExit: os._exit(0)