def route_create_ensemble_workflow(ensemble): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, ensemble) name = request.form.get("name", None) if name is None: raise EMError("Specify ensemble workflow 'name'") priority = request.form.get("priority", 0) basedir = request.form.get("basedir") if basedir is None: raise EMError( "Specify 'basedir' where plan command should be executed") plan_command = request.form.get("plan_command") if plan_command is None: raise EMError( "Specify 'plan_command' that should be executed to plan workflow") dao.create_ensemble_workflow(e.id, name, basedir, priority, plan_command) g.session.commit() return api.json_created( url_for("route_get_ensemble_workflow", ensemble=ensemble, workflow=name))
def route_analyze_ensemble_workflow(ensemble, workflow): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, ensemble) w = dao.get_ensemble_workflow(e.id, workflow) report = "".join(analyze(w)) resp = make_response(report, 200) resp.headers["Content-Type"] = "text/plain" return resp
def route_create_ensemble(): name = request.form.get("name", None) if name is None: raise EMError("Specify ensemble name") max_running = request.form.get("max_running", 1) max_planning = request.form.get("max_planning", 1) dao = Ensembles(g.session) dao.create_ensemble(g.user.username, name, max_running, max_planning) g.session.commit() return api.json_created( url_for("route_get_ensemble", name=name, _external=True))
def route_update_ensemble_workflow(ensemble, workflow): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, ensemble) w = dao.get_ensemble_workflow(e.id, workflow) priority = request.form.get("priority", None) if priority is not None: w.set_priority(priority) state = request.form.get("state", None) if state is not None: w.change_state(state) w.set_updated() g.session.commit() return api.json_response(w.get_detail_object())
def route_delete_trigger(ensemble, trigger): # verify that ensemble exists for user e_dao = Ensembles(g.session) # raises EMError code 404 if does not exist ensemble_id = e_dao.get_ensemble(g.user.username, ensemble).id # update trigger state to be STOPPED so that the TriggerManager can # handle it appropriately t_dao = Triggers(g.session) # make sure get_trigger raises 404 if nothing found trigger_id = t_dao.get_trigger(ensemble_id, trigger).id t_dao.update_state(ensemble_id, trigger_id) # TODO: what to return here # return HTTP code that represents that it was successful and that nothing # is to returned # status code 204, nothing else to return return "hello world from delete_trigger!"
def loop_forever(self): while True: u = user.get_user_by_uid(os.getuid()) session = connection.connect( u.get_master_db_url(), connect_args={"check_same_thread": False} ) try: dao = Ensembles(session) self.loop_once(dao) finally: session.close() time.sleep(self.interval)
def route_create_trigger(ensemble, trigger): # verify that ensemble exists for user e_dao = Ensembles(g.session) # raises EMError code 404 if does not exist ensemble_id = e_dao.get_ensemble(g.user.username, ensemble).id # create trigger entry in db t_dao = Triggers(g.session) trigger_type = request.form.get("type") kwargs = { "ensemble_id": ensemble_id, "trigger": trigger, "trigger_type": trigger_type, "workflow_script": request.form.get("workflow_script"), "workflow_args": json.loads(request.form.get("workflow_args")), } if trigger_type == TriggerType.CRON.value: # add cron trigger specific parameters kwargs["interval"] = request.form.get("interval") kwargs["timeout"] = request.form.get("timeout") elif trigger_type == TriggerType.FILE_PATTERN.value: # add file pattern specific parameters kwargs["interval"] = request.form.get("interval") kwargs["timeout"] = request.form.get("timeout") kwargs["file_patterns"] = json.loads(request.form.get("file_patterns")) else: raise NotImplementedError( "encountered unsupported trigger type: {}".format(trigger_type)) t_dao.insert_trigger(**kwargs) # TODO: what to return here # return ID that was created, in this case trigger name is sufficient # probably code 201 # use Flask response object and a json object representing an id of the entity return "hello world from create_trigger!"
def run(self): """Trigger manager main loop.""" self.log.info("trigger manager starting") while True: # TODO: user will always be the same.., keep this in the loop or move it out u = user.get_user_by_uid(os.getuid()) session = connection.connect( u.get_master_db_url(), connect_args={"check_same_thread": False}) try: self.trigger_dao = Triggers(session) self.ensemble_dao = Ensembles(session) triggers = self.trigger_dao.list_triggers() self.log.info("processing {} triggers".format(len(triggers))) for t in triggers: t_name = TriggerManager.get_tname(t) if t.state == "READY": self.start_trigger(t) elif t.state == "RUNNING" and t_name not in self.running: # restart self.log.debug( "{} not in memory, restarting it".format(t_name)) self.start_trigger(t) elif t.state == "RUNNING" and not self.running[ t_name].is_alive(): # exited self.log.debug( "{} exited, removing references to it".format( t_name)) self.stop_trigger(t) elif t.state == "STOPPED": self.stop_trigger(t) finally: session.close() time.sleep(self.polling_rate)
def route_update_ensemble(name): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, name) max_running = request.form.get("max_running", None) if max_running is not None: e.set_max_running(max_running) max_planning = request.form.get("max_planning", None) if max_planning is not None: e.set_max_planning(max_planning) state = request.form.get("state", None) if state is not None: if state != e.state: # TODO Do the necessary state transition e.set_state(state) e.set_updated() g.session.commit() return api.json_response(e.get_object())
def route_get_ensemble(name): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, name) result = e.get_object() return api.json_response(result)
def route_list_ensembles(): dao = Ensembles(g.session) ensembles = dao.list_ensembles(g.user.username) result = [e.get_object() for e in ensembles] return api.json_response(result)
def route_get_ensemble_workflow(ensemble, workflow): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, ensemble) w = dao.get_ensemble_workflow(e.id, workflow) result = w.get_detail_object() return api.json_response(result)
def route_list_ensemble_workflows(name): dao = Ensembles(g.session) e = dao.get_ensemble(g.user.username, name) result = [w.get_object() for w in dao.list_ensemble_workflows(e.id)] return api.json_response(result)
class TriggerManager(threading.Thread): """ Manages workflow triggers. Work is done based on the contents of the "trigger" table, and the state of each trigger in that table. """ def __init__(self, ): threading.Thread.__init__(self, daemon=True) self.log = logging.getLogger("trigger.manager") # interval in seconds at which the trigger manager will query the # database to see if there is work to do (start, stop, restart) triggers self.polling_rate = 15 # references to currently running trigger threads # key: (<ensenble_id>, <trigger_name>), value: handle to trigger thread self.running = dict() self.trigger_dao = None self.ensemble_dao = None def run(self): """Trigger manager main loop.""" self.log.info("trigger manager starting") while True: # TODO: user will always be the same.., keep this in the loop or move it out u = user.get_user_by_uid(os.getuid()) session = connection.connect( u.get_master_db_url(), connect_args={"check_same_thread": False}) try: self.trigger_dao = Triggers(session) self.ensemble_dao = Ensembles(session) triggers = self.trigger_dao.list_triggers() self.log.info("processing {} triggers".format(len(triggers))) for t in triggers: t_name = TriggerManager.get_tname(t) if t.state == "READY": self.start_trigger(t) elif t.state == "RUNNING" and t_name not in self.running: # restart self.log.debug( "{} not in memory, restarting it".format(t_name)) self.start_trigger(t) elif t.state == "RUNNING" and not self.running[ t_name].is_alive(): # exited self.log.debug( "{} exited, removing references to it".format( t_name)) self.stop_trigger(t) elif t.state == "STOPPED": self.stop_trigger(t) finally: session.close() time.sleep(self.polling_rate) def start_trigger(self, trigger: Trigger): """Given a trigger, start the appropriate trigger thread. :param trigger: the trigger to be started :type trigger: Trigger """ trigger_name = TriggerManager.get_tname(trigger) self.log.debug("starting {}".format(trigger_name)) workflow = json.loads(trigger.workflow) required_args = { "ensemble_id": trigger.ensemble_id, "ensemble": self.ensemble_dao.get_ensemble_name(trigger.ensemble_id), "trigger": trigger.name, "workflow_script": workflow["script"], "workflow_args": workflow["args"] if workflow["args"] else [], } trigger_specific_kwargs = json.loads(trigger.args) # create trigger thread if trigger._type == TriggerType.CRON.value: t = CronTrigger(**required_args, **trigger_specific_kwargs) elif trigger._type == TriggerType.FILE_PATTERN.value: t = FilePatternTrigger(**required_args, **trigger_specific_kwargs) else: raise NotImplementedError("unsupported trigger type: {}".format( trigger.type)) # keep ref to trigger thread self.running[trigger_name] = t t.start() # update state self.log.debug( "changing {name} state: {old_state} -> {new_state}".format( name=trigger_name, old_state=trigger.state, new_state="RUNNING", )) self.trigger_dao.update_state(ensemble_id=trigger.ensemble_id, trigger_id=trigger._id, new_state="RUNNING") def stop_trigger(self, trigger: Trigger): """Stop a trigger thread. :param trigger: the trigger to be stopped :type trigger: Trigger """ # using reference to trigger, tell trigger to shutdown target_trigger = TriggerManager.get_tname(trigger) self.log.debug("stopping {}".format(target_trigger)) self.running[target_trigger].shutdown() del self.running[target_trigger] # remove entry from database self.trigger_dao.delete_trigger(trigger.ensemble_id, trigger.name) @staticmethod def get_tname(trigger: Trigger) -> tuple: """Given a trigger object, get its name as a pair (<ensemble_id>, <trigger_name>) :param trigger: the trigger :type trigger: Trigger :return: pair (<ensemble_id>, <trigger_name>) :rtype: tuple """ return (trigger.ensemble_id, trigger.name)