def execute(self, runid): """Send files as an email. Keyword arguments: runid -- A UTC ID string which identifies the run. The following parameters are loaded from the Robot configuration: FileEmailer_Host e.g. localhost or localhost:25 FileEmailer_Type e.g. text or html FileEmailer_From e.g. [email protected] FileEmailer_Recipients e.g. [email protected], [email protected] FileEmailer_Subject e.g. Report ${runid}. FileEmailer_TextFile e.g. ~/gangadir/robot/report/${runid}.txt FileEmailer_HtmlFile e.g. ~/gangadir/robot/report/${runid}.html If Recipients are not specified then no email is sent. In Subject, TextFile and HtmlFile the token ${runid} is replaced by the runid argument. If Type is text, then TextFile is sent. If Type is html, then HtmlFile is sent, or if TextFile is also specified then a multipart message is sent containing TextFile and HtmlFile. """ # get configuration properties host = self.getoption('FileEmailer_Host') type = self.getoption('FileEmailer_Type') from_ = self.getoption('FileEmailer_From') # extract recipients ignoring blank entries recipients = [recipient.strip() for recipient in \ self.getoption('FileEmailer_Recipients').split(',') \ if recipient.strip()] subject = Utility.expand(self.getoption('FileEmailer_Subject'), runid = runid) textfilename = Utility.expand(self.getoption('FileEmailer_TextFile'), runid = runid) htmlfilename = Utility.expand(self.getoption('FileEmailer_HtmlFile'), runid = runid) if not recipients: logger.warn('No recipients specified. Email will not be sent.') return logger.info('Emailing files to %s.', recipients) # build message if type == 'html': msg = self._gethtmlmsg(textfilename, htmlfilename) else: msg = self._gettextmsg(textfilename) msg['Subject'] = subject msg['From'] = from_ msg['To'] = ', '.join(recipients) # send message session = SMTP() try: session.connect(host) session.sendmail(from_, recipients, msg.as_string()) session.quit() finally: session.close() logger.info('Files emailed.')
def test_utctime_from_utcid(self): """Test utctime() returns utctime corresponding to utcid parameter.""" utcid = Utility.utcid() utctime = Utility.utctime(utcid) assert isinstance(utctime, str), 'utctime is not a string' assert utcid == Utility.utcid( utctime), 'utcid -> utctime -> utcid conversion fails'
def setUp(self): """Create test actions.""" super(TestCore, self).setUp() self.runid = Utility.utcid() self.submitter = CoreSubmitter() #force submitter patterns self.submitter.options = { 'CoreSubmitter_Patterns': ['GangaRobot/old_test/Lib/Core/test-jobs.txt'] } self.finisher = CoreFinisher() #force finisher timeout to 5 mins self.finisher.options = {'BaseFinisher_Timeout': 300} #test extractor fakes save class TestCoreExtractor(CoreExtractor): def _saveextract(self, runnode, runid): self.runnode = runnode #can be accessed in the test self.extractor = TestCoreExtractor() #test reporter fakes load and save class TestCoreReporter(CoreReporter): def _loadextract(self, runid): return self.runnode #must be set in the test def _savereport(self, report, runid): self.report = report #can be accessed in the test self.reporter = TestCoreReporter()
def execute(self, runid): """Invoke each of the extractors in the chain. Keyword arguments: runid -- A UTC ID string which identifies the run. An empty run node is created as an instance of Extract.Node and passed to the handlerunnode() method of each extractor in the chain. An empty job node is created, as a subnode of the run node, for each job in the jobtree directory named by the runid, e.g. /2007-06-25_09.18.46, and passed to the handlejobnode() method of each extractor in the chain. The run node is saved as XML to the configurable BaseExtractor_XmlFile, replacing ${runid} with the current run id. e.g. ~/gangadir/robot/extract/${runid}.xml """ logger.info("Extracting data for run '%s'.", runid) runnode = Node('run') for extractor in self.chain: extractor.handlerunnode(runnode, runid) path = Utility.jobtreepath(runid) ids = jobtree.listjobs(path) ids.sort() for id in ids: jobnode = Node('job') job = jobs(id) for extractor in self.chain: extractor.handlejobnode(jobnode, job) runnode.nodes.append(jobnode) self._saveextract(runnode, runid) logger.info('Extract data saved.')
def dorun(self): """Executes a run of actions and sleep periods. Initialises runid to the current UTC ID. """ self.runid = Utility.utcid() while 1: logger.info("Start run %s with id '%s'.", self.run, self.runid) for action in self.run: try: self._doaction(action) except GangaRobotContinueError as e: logger.warning("Continue Error in Action '%s' with message '%s'. Run continued", action, e) except GangaRobotBreakError as e: logger.warning("Break Error in Action '%s' with message '%s'. Run ended", action, e) break except GangaRobotFatalError as e: logger.error("Fatal Error in Action '%s' with message '%s'. Run aborted", action, e) raise except Exception as e: config = getConfig('Robot') if (config['ExceptionBehaviour'] == 'Continue'): logger.error("Error in Action '%s' with message '%s'. Run continued", action, e) elif (config['ExceptionBehaviour'] == 'Break'): logger.error("Error in Action '%s' with message '%s'. Run continued", action, e) break else: logger.error("Abort run id '%s'. Action '%s' failed with message %s.", self.runid, action, e) raise logger.info("Finish run id '%s'.", self.runid) if not self.repeat: break
def test_submitter(self): """Test submitter submits 3 jobs and adds them to the jobtree.""" #execute action self.submitter.execute(self.runid) #check jobs are added to jobtree path = Utility.jobtreepath(self.runid) js = jobtree.getjobs(path) assert len(js) == 3, 'number of jobs added to jobtree path is not 3' for j in js: assert j.status != 'new', 'job status is new indicating that it may not have been submitted'
def _gethtmlmsg(self, textfilename, htmlfilename): html = Utility.readfile(htmlfilename) htmlmsg = MIMEText(html, 'html') if textfilename: textmsg = self._gettextmsg(textfilename) multimsg = MIMEMultipart('alternative') multimsg.attach(textmsg) multimsg.attach(htmlmsg) return multimsg else: return htmlmsg
def test_finisher(self): """Test finisher waits for jobs to finish.""" #submit jobs self.submitter.execute(self.runid) #execute action self.finisher.execute(self.runid) #get jobs from jobtree path = Utility.jobtreepath(self.runid) js = jobtree.getjobs(path) #check jobs are finished for j in js: assert j.status == 'completed', 'job status is not completed indicating that it may not have finished'
def handlerunnode(self, runnode, runid): """Extracts generic data for the given run. Keyword arguments: runnode -- An Extract.Node for the run. runid -- A UTC ID string which identifies the run. Example of nodes added to the run node: <core> <id>2007-06-22_13.17.51</id> <start-time>2007/06/22 13:17:51</start-time> <extract-time>2007/06/22 13:18:55</extract-time> </core> All nodes are guaranteed to be present. All times are in UTC. """ corenode = runnode.addnode('core') corenode.addnode('id', runid) corenode.addnode('start-time', Utility.utctime(runid)) corenode.addnode('extract-time', Utility.utctime())
def execute(self, runid): """Invoke each of the finishers in the chain. Keyword arguments: runid -- A UTC ID string which identifies the run. Loops until the run is finished or the elapsed time exceeds the configurable BaseFinisher_Timeout (seconds). Each job in the jobtree directory named by the runid, e.g. /2007-06-25_09.18.46, is passed to the handleisfinished() method of each finisher in the chain. If any finisher returns False, for any job, then the run is considered unfinished. The sleep period in the loop is 1/10th the timeout, constrained to the range [10, 60] seconds. """ logger.info("Finishing jobs for run '%s'.", runid) startsecs = time.time() timeoutsecs = self.getoption('BaseFinisher_Timeout') sleepsecs = timeoutsecs / 10 if sleepsecs < 10: sleepsecs = 10 if sleepsecs > 60: sleepsecs = 60 logger.info('Waiting for jobs to finish. Timeout %d seconds.' ' Sleep period %d seconds', timeoutsecs, sleepsecs) path = Utility.jobtreepath(runid) jobs = jobtree.getjobs(path) while True: #suppose finished is True finished = True for j in jobs: #if one job not finished then finished is False for finisher in self.chain: if not finisher.handleisfinished(j): finished = False break elapsedsecs = time.time() - startsecs # test break condition here to avoid sleep after satisfied condition if finished or timeoutsecs < elapsedsecs: break # break condition on satisfied, so sleep time.sleep(sleepsecs) if finished: logger.info('All jobs finished after %d seconds.', elapsedsecs) else: logger.info('Timeout after %d seconds.', elapsedsecs)
def _savereport(self, report, runid): #text textfilename = Utility.expand(self.getoption('BaseReporter_TextFile'), runid = runid) textcontent = str(report) Utility.writefile(textfilename, textcontent) #html htmlfilename = Utility.expand(self.getoption('BaseReporter_HtmlFile'), runid = runid) htmlcontent = report.tohtml() Utility.writefile(htmlfilename, htmlcontent)
def _savereport(self, report, runid): #text textfilename = Utility.expand(self.getoption('BaseReporter_TextFile'), runid=runid) textcontent = str(report) Utility.writefile(textfilename, textcontent) #html htmlfilename = Utility.expand(self.getoption('BaseReporter_HtmlFile'), runid=runid) htmlcontent = report.tohtml() Utility.writefile(htmlfilename, htmlcontent)
def dorun(self): """Executes a run of actions and sleep periods. Initialises runid to the current UTC ID. """ self.runid = Utility.utcid() while 1: logger.info("Start run %s with id '%s'.", self.run, self.runid) for action in self.run: try: self._doaction(action) except GangaRobotContinueError as e: logger.warning( "Continue Error in Action '%s' with message '%s'. Run continued", action, e) except GangaRobotBreakError as e: logger.warning( "Break Error in Action '%s' with message '%s'. Run ended", action, e) break except GangaRobotFatalError as e: logger.error( "Fatal Error in Action '%s' with message '%s'. Run aborted", action, e) raise except Exception as e: config = getConfig('Robot') if (config['ExceptionBehaviour'] == 'Continue'): logger.error( "Error in Action '%s' with message '%s'. Run continued", action, e) elif (config['ExceptionBehaviour'] == 'Break'): logger.error( "Error in Action '%s' with message '%s'. Run continued", action, e) break else: logger.error( "Abort run id '%s'. Action '%s' failed with message %s.", self.runid, action, e) raise logger.info("Finish run id '%s'.", self.runid) if not self.repeat: break
def loaddriver(): """Create new driver based on Robot configuration options. Example of relevant configuration options: [Robot] Driver_Run = ['submit', 30, 'extract', 'report'] Driver_Repeat = False Driver_Action_submit = GangaRobot.Lib.Core.CoreSubmitter.CoreSubmitter Driver_Action_extract = GangaRobot.Lib.Core.CoreExtractor.CoreExtractor Driver_Action_report = GangaRobot.Lib.Core.CoreReporter.CoreReporter """ KEY_RUN = 'Driver_Run' KEY_REPEAT = 'Driver_Repeat' KEY_ACTION_PREFIX = 'Driver_Action_' config = Utility.getconfig() run = config[KEY_RUN] repeat = config[KEY_REPEAT] actions = {} #load action classes for key in config: if key.startswith(KEY_ACTION_PREFIX): action = key[len(KEY_ACTION_PREFIX):] fqcn = config[key] try: actions[action] = _loadclass(fqcn) except Exception as e: raise ApplicationConfigurationError(e, "Cannot load class '%s'." % fqcn) #check actions exist for run for action in run: if not action in actions: try: int(action) except ValueError as e: raise ApplicationConfigurationError(e, "Unknown action '%s'." % action) return Driver(run = run, actions = actions, repeat = repeat)
def loaddriver(): """Create new driver based on Robot configuration options. Example of relevant configuration options: [Robot] Driver_Run = ['submit', 30, 'extract', 'report'] Driver_Repeat = False Driver_Action_submit = GangaRobot.Lib.Core.CoreSubmitter.CoreSubmitter Driver_Action_extract = GangaRobot.Lib.Core.CoreExtractor.CoreExtractor Driver_Action_report = GangaRobot.Lib.Core.CoreReporter.CoreReporter """ KEY_RUN = 'Driver_Run' KEY_REPEAT = 'Driver_Repeat' KEY_ACTION_PREFIX = 'Driver_Action_' config = Utility.getconfig() run = config[KEY_RUN] repeat = config[KEY_REPEAT] actions = {} #load action classes for key in config: if key.startswith(KEY_ACTION_PREFIX): action = key[len(KEY_ACTION_PREFIX):] fqcn = config[key] try: actions[action] = _loadclass(fqcn) except Exception as e: raise ApplicationConfigurationError( "Cannot load class '%s'. Exception: '%s'" % (fqcn, e)) #check actions exist for run for action in run: if not action in actions: try: int(action) except ValueError as e: raise ApplicationConfigurationError( "Unknown action '%s'. Exception: '%s'" % (action, e)) return Driver(run=run, actions=actions, repeat=repeat)
def setUp(self): """Create test actions.""" super(TestCore, self).setUp() self.runid = Utility.utcid() self.submitter = CoreSubmitter() #force submitter patterns self.submitter.options = {'CoreSubmitter_Patterns':['GangaRobot/old_test/Lib/Core/test-jobs.txt']} self.finisher = CoreFinisher() #force finisher timeout to 5 mins self.finisher.options = {'BaseFinisher_Timeout':300} #test extractor fakes save class TestCoreExtractor(CoreExtractor): def _saveextract(self, runnode, runid): self.runnode = runnode #can be accessed in the test self.extractor = TestCoreExtractor() #test reporter fakes load and save class TestCoreReporter(CoreReporter): def _loadextract(self, runid): return self.runnode #must be set in the test def _savereport(self, report, runid): self.report = report #can be accessed in the test self.reporter = TestCoreReporter()
def getoption(self, key): """Return the configuration option value. If the instance has an attribute 'options' containing the given key, then the corresponding value is returned, otherwise the value is retrieved from the [Robot] section of the global configuration. This provides a single point of access to configuration options and allows users the possibility to override options programmatically for a given instance. Example: c = CoreFinisher() c.options = {'BaseFinisher_Timeout':3600} """ if hasattr(self, 'options') and key in self.options: return self.options[key] else: try: return Utility.getconfig()[key] except ConfigError: return ''
def execute(self, runid): """Invoke each of the submitters in the chain. Keyword arguments: runid -- A UTC ID string which identifies the run. An empty list of jobids is created and passed to the handlesubmit() method of each the submitter in the chain. Any job, whose id is added to the argument jobids, is added to the jobtree in the directory named by the runid, e.g. /2007-06-25_09.18.46. """ logger.info("Submitting jobs for run '%s'.", runid) jobids = [] for submitter in self.chain: submitter.handlesubmit(jobids, runid) path = Utility.jobtreepath(runid) jobtree.mkdir(path) for id in jobids: jobtree.add(jobs(id), path) logger.info("%d submitted jobs added to jobtree path '%s'.", len(jobids), path)
def _gettextmsg(self, textfilename): text = Utility.readfile(textfilename) textmsg = MIMEText(text) return textmsg
def _loadextract(self, runid): filename = Utility.expand(self.getoption('BaseExtractor_XmlFile'), runid = runid) content = Utility.readfile(filename) return Node.fromxml(content)
def test_expand_multiple_occurrences(self): """Test expand() replaces multiple tokens in text.""" text = 'The following token ${mytoken1} should be replaced, as should ${mytoken2}.' expected = 'The following token 1111 should be replaced, as should 2222.' actual = Utility.expand(text, mytoken1='1111', mytoken2='2222') assert expected == actual, 'the text was not modified as expected'
def test_expand_repeated_occurrences(self): """Test expand() replaces repeated tokens in text.""" text = 'The following token ${mytoken1} should be replaced, as should ${mytoken1}.' expected = 'The following token 1111 should be replaced, as should 1111.' actual = Utility.expand(text, mytoken1='1111') assert expected == actual, 'the text was not modified as expected'
def test_expand_no_replacements(self): """Test expand() does not replace tokens in text when no replacements parameter.""" text = 'The following token ${runid} should not be replaced.' expected = text actual = Utility.expand(text) assert expected == actual, 'the text was modified'
def test_utctime(self): """Test utctime() returns a string value when no parameter.""" utctime = Utility.utctime() assert isinstance(utctime, str), 'utctime is not a string'
def handlereport(self, report, runnode): """Add statistics on generic data to the report. Keyword arguments: report -- A Report.Report for the run, to be emailed. runnode -- A the extracted data for the run. If the report title is undefined a title 'CoreReporter ${runid}' is set. If the configurable option CoreReporter_ExtractUrl is defined then a link is added from 'Run id', replacing ${runid} with the current run id. e.g. http://localhost/robot/extract/${runid}.xml Example of report generated (plain text version): CoreReporter 2007-06-27_10.49.40 ******************************** Core Analysis ************* Run id : 2007-06-27_10.49.40 (http://localhost/robot/extract/2007-06-27_10.49.40.xml) Start time : 2007/06/27 10:49:40 Extract time : 2007/06/27 10:49:55 Status | Subtotal ------------------------------- completed | 3 submitted | 1 failed | 1 Total | 5 ActualCE | Completed | Total ------------------------------------------------------------------------------- lx09.hep.ph.ic.ac.uk | 3 | 5 Non-completed Jobs ================== Id | Status | Backend | Backend.id | ActualCE ------------------------------------------------------------------------------- 51 | failed | Local | 13418 | lx09.hep.ph.ic.ac.uk 53 | submitted | Local | None | lx09.hep.ph.ic.ac.uk """ runid = runnode.getvalue('core.id') # get configuration options extracturl = Utility.expand(self.getoption('CoreReporter_ExtractUrl'), runid=runid) #CoreReporter id if not report.title: report.title = 'CoreReporter ' + runid #Core Analysis report.addline(Heading('Core Analysis', 2)) report.addline() #Run id : ... report.addline('Run id :') if extracturl: report.addelement(Link(runid, extracturl)) else: report.addelement(runid) #Start time : ... #Extract time : ... report.addline('Start time : ' + runnode.getvalue('core.start-time')) report.addline('Extract time : ' + runnode.getvalue('core.extract-time')) report.addline() #Status | Subtotal #... #Total 10 report.addline(self._getstatustable(runnode)) report.addline() #ActualCE | Completed | Total #... report.addline(self._getcetable(runnode)) report.addline() #Non-completed Jobs report.addline(Heading('Non-completed Jobs')) #Id | Status | Backend | Backend.id | ActualCE #... report.addline(self._getnoncompletedtable(runnode)) report.addline()
def test_expand_multiple_occurrences(self): """Test expand() replaces multiple tokens in text.""" text = 'The following token ${mytoken1} should be replaced, as should ${mytoken2}.' expected = 'The following token 1111 should be replaced, as should 2222.' actual = Utility.expand(text, mytoken1 = '1111', mytoken2 = '2222') assert expected == actual, 'the text was not modified as expected'
def test_expand_repeated_occurrences(self): """Test expand() replaces repeated tokens in text.""" text = 'The following token ${mytoken1} should be replaced, as should ${mytoken1}.' expected = 'The following token 1111 should be replaced, as should 1111.' actual = Utility.expand(text, mytoken1 = '1111') assert expected == actual, 'the text was not modified as expected'
def execute(self, runid): """Send files as an email. Keyword arguments: runid -- A UTC ID string which identifies the run. The following parameters are loaded from the Robot configuration: FileEmailer_Host e.g. localhost or localhost:25 FileEmailer_Type e.g. text or html FileEmailer_From e.g. [email protected] FileEmailer_Recipients e.g. [email protected], [email protected] FileEmailer_Subject e.g. Report ${runid}. FileEmailer_TextFile e.g. ~/gangadir/robot/report/${runid}.txt FileEmailer_HtmlFile e.g. ~/gangadir/robot/report/${runid}.html If Recipients are not specified then no email is sent. In Subject, TextFile and HtmlFile the token ${runid} is replaced by the runid argument. If Type is text, then TextFile is sent. If Type is html, then HtmlFile is sent, or if TextFile is also specified then a multipart message is sent containing TextFile and HtmlFile. """ # get configuration properties host = self.getoption('FileEmailer_Host') type = self.getoption('FileEmailer_Type') from_ = self.getoption('FileEmailer_From') # extract recipients ignoring blank entries recipients = [recipient.strip() for recipient in \ self.getoption('FileEmailer_Recipients').split(',') \ if recipient.strip()] subject = Utility.expand(self.getoption('FileEmailer_Subject'), runid=runid) textfilename = Utility.expand(self.getoption('FileEmailer_TextFile'), runid=runid) htmlfilename = Utility.expand(self.getoption('FileEmailer_HtmlFile'), runid=runid) if not recipients: logger.warn('No recipients specified. Email will not be sent.') return logger.info('Emailing files to %s.', recipients) # build message if type == 'html': msg = self._gethtmlmsg(textfilename, htmlfilename) else: msg = self._gettextmsg(textfilename) msg['Subject'] = subject msg['From'] = from_ msg['To'] = ', '.join(recipients) # send message session = SMTP() try: session.connect(host) session.sendmail(from_, recipients, msg.as_string()) session.quit() finally: session.close() logger.info('Files emailed.')
def _loadextract(self, runid): filename = Utility.expand(self.getoption('BaseExtractor_XmlFile'), runid=runid) content = Utility.readfile(filename) return Node.fromxml(content)
def test_utctime_from_utcid(self): """Test utctime() returns utctime corresponding to utcid parameter.""" utcid = Utility.utcid() utctime = Utility.utctime(utcid) assert isinstance(utctime, str), 'utctime is not a string' assert utcid == Utility.utcid(utctime), 'utcid -> utctime -> utcid conversion fails'
def _saveextract(self, runnode, runid): filename = Utility.expand(self.getoption('BaseExtractor_XmlFile'), runid = runid) content = runnode.toprettyxml() Utility.writefile(filename, content)
def handlereport(self, report, runnode): """Add statistics on generic data to the report. Keyword arguments: report -- A Report.Report for the run, to be emailed. runnode -- A the extracted data for the run. If the report title is undefined a title 'CoreReporter ${runid}' is set. If the configurable option CoreReporter_ExtractUrl is defined then a link is added from 'Run id', replacing ${runid} with the current run id. e.g. http://localhost/robot/extract/${runid}.xml Example of report generated (plain text version): CoreReporter 2007-06-27_10.49.40 ******************************** Core Analysis ************* Run id : 2007-06-27_10.49.40 (http://localhost/robot/extract/2007-06-27_10.49.40.xml) Start time : 2007/06/27 10:49:40 Extract time : 2007/06/27 10:49:55 Status | Subtotal ------------------------------- completed | 3 submitted | 1 failed | 1 Total | 5 ActualCE | Completed | Total ------------------------------------------------------------------------------- lx09.hep.ph.ic.ac.uk | 3 | 5 Non-completed Jobs ================== Id | Status | Backend | Backend.id | ActualCE ------------------------------------------------------------------------------- 51 | failed | Local | 13418 | lx09.hep.ph.ic.ac.uk 53 | submitted | Local | None | lx09.hep.ph.ic.ac.uk """ runid = runnode.getvalue('core.id') # get configuration options extracturl = Utility.expand(self.getoption('CoreReporter_ExtractUrl'), runid = runid) #CoreReporter id if not report.title: report.title = 'CoreReporter ' + runid #Core Analysis report.addline(Heading('Core Analysis', 2)) report.addline() #Run id : ... report.addline('Run id :') if extracturl: report.addelement(Link(runid, extracturl)) else: report.addelement(runid) #Start time : ... #Extract time : ... report.addline('Start time : ' + runnode.getvalue('core.start-time')) report.addline('Extract time : ' + runnode.getvalue('core.extract-time')) report.addline() #Status | Subtotal #... #Total 10 report.addline(self._getstatustable(runnode)) report.addline() #ActualCE | Completed | Total #... report.addline(self._getcetable(runnode)) report.addline() #Non-completed Jobs report.addline(Heading('Non-completed Jobs')) #Id | Status | Backend | Backend.id | ActualCE #... report.addline(self._getnoncompletedtable(runnode)) report.addline()