def __init__(self, *args, **kwargs): Runner.__init__(self, *args, **kwargs) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_run_cleaner = SuiteRunCleaner( event_handler=self.event_handler, host_selector=self.host_selector, suite_engine_proc=self.suite_engine_proc)
def __init__(self, event_handler=None): self.event_handler = event_handler self.popen = RosePopener(self.event_handler) self.fs_util = FileSystemUtil(self.event_handler) self.config_pm = ConfigProcessorsManager(self.event_handler, self.popen, self.fs_util) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=self.event_handler, popen=self.popen, fs_util=self.fs_util)
def __init__(self, *args, **kwargs): self.exposed = True self.props = {} rose_conf = ResourceLocator.default().get_conf() self.props["title"] = rose_conf.get_value(["rosie-disco", "title"], self.TITLE) self.props["host_name"] = rose_conf.get_value(["rosie-disco", "host"]) if self.props["host_name"] is None: self.props["host_name"] = HostSelector().get_local_host() if self.props["host_name"] and "." in self.props["host_name"]: self.props["host_name"] = (self.props["host_name"].split( ".", 1)[0]) self.props["rose_version"] = ResourceLocator.default().get_version() self.props["template_env"] = jinja2.Environment( loader=jinja2.FileSystemLoader(ResourceLocator.default( ).get_util_home("lib", "html", "template", "rosie-disco"))) db_url_map = {} for key, node in rose_conf.get(["rosie-db"]).value.items(): if key.startswith("db.") and key[3:]: db_url_map[key[3:]] = node.value self.db_url_map = db_url_map if not self.db_url_map: self.db_url_map = {} for key, db_url in self.db_url_map.items(): setattr(self, key, RosieDiscoService(self.props, key, db_url))
def __init__(self, opts, reporter=None, popen=None, fs_util=None): self.opts = opts if reporter is None: self.reporter = Reporter(opts.verbosity - opts.quietness) else: self.reporter = reporter if popen is None: self.popen = RosePopener(event_handler=self.reporter) else: self.popen = popen if fs_util is None: self.fs_util = FileSystemUtil(event_handler=self.reporter) else: self.fs_util = fs_util self.host_selector = HostSelector(event_handler=self.reporter, popen=self.popen)
def __init__(self, *args, **kwargs): self.exposed = True self.suite_engine_proc = SuiteEngineProcessor.get_processor() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) self.template_env = template_env
def __init__(self, *args, **kwargs): self.exposed = True self.bush_dao = RoseBushDAO() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment( loader=jinja2.FileSystemLoader(ResourceLocator.default( ).get_util_home("lib", "html", "template", "rose-bush"))) template_env.filters['urlise'] = self.url2hyperlink self.template_env = template_env
def __init__(self, event_handler=None): self.event_handler = event_handler self.popen = RosePopener(self.event_handler) self.fs_util = FileSystemUtil(self.event_handler) self.config_pm = ConfigProcessorsManager( self.event_handler, self.popen, self.fs_util) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=self.event_handler, popen=self.popen, fs_util=self.fs_util)
def __init__(self, event_handler=None, host_selector=None, suite_engine_proc=None): if event_handler is None: event_handler = Reporter() self.event_handler = event_handler if host_selector is None: host_selector = HostSelector(event_handler=event_handler) self.host_selector = host_selector if suite_engine_proc is None: suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=event_handler) self.suite_engine_proc = suite_engine_proc
def __init__(self, event_handler=None, popen=None, fs_util=None, host_selector=None, **kwargs): self.event_handler = event_handler if popen is None: popen = RosePopener(event_handler) self.popen = popen if fs_util is None: fs_util = FileSystemUtil(event_handler) self.fs_util = fs_util if host_selector is None: host_selector = HostSelector(event_handler, popen) self.host_selector = host_selector self.date_time_oper = RoseDateTimeOperator()
def __init__(self, *args, **kwargs): self.exposed = True self.suite_engine_proc = SuiteEngineProcessor.get_processor() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment( loader=jinja2.FileSystemLoader(ResourceLocator.default( ).get_util_home("lib", "html", "template", "rose-bush"))) def urlise(text): pattern = '((https?):\/\/[^\s\(\)&\[\]\{\}]+)' replacement = '<a href="\g<1>">\g<1></a>' text = re.sub(pattern, replacement, text) return text template_env.filters['urlise'] = urlise self.template_env = template_env
def __init__(self, event_handler=None, popen=None, suite_engine_proc=None, host_selector=None): self.event_handler = event_handler if popen is None: popen = RosePopener(event_handler) self.popen = popen if suite_engine_proc is None: suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=event_handler, popen=popen) self.suite_engine_proc = suite_engine_proc if host_selector is None: host_selector = HostSelector(event_handler, popen) self.host_selector = host_selector
def __init__(self, *args, **kwargs): self.exposed = True self.bush_dao = RoseBushDAO() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) template_env.filters['urlise'] = self.url2hyperlink self.template_env = template_env
def __init__(self, *args, **kwargs): self.exposed = True self.suite_engine_proc = SuiteEngineProcessor.get_processor() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) def urlise(text): pattern = '((https?):\/\/[^\s\(\)&\[\]\{\}]+)' replacement = '<a href="\g<1>">\g<1></a>' text = re.sub(pattern, replacement, text) return text template_env.filters['urlise'] = urlise self.template_env = template_env
class RoseBushService(object): """Rose Bush Service.""" NS = "rose" UTIL = "bush" TITLE = "Rose Bush" CYCLES_PER_PAGE = 100 JOBS_PER_PAGE = 15 JOBS_PER_PAGE_MAX = 300 MIME_TEXT_PLAIN = "text/plain" REC_URL = re.compile(r"((https?):\/\/[^\s\(\)&\[\]\{\}]+)") SEARCH_MODE_REGEX = "REGEX" SEARCH_MODE_TEXT = "TEXT" SUITES_PER_PAGE = 100 VIEW_SIZE_MAX = 10 * 1024 * 1024 # 10MB def __init__(self, *args, **kwargs): self.exposed = True self.bush_dao = RoseBushDAO() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) template_env.filters['urlise'] = self.url2hyperlink self.template_env = template_env @classmethod def url2hyperlink(cls, text): """Turn http or https link into a hyperlink.""" return cls.REC_URL.sub(r'<a href="\g<1>">\g<1></a>', text) @cherrypy.expose def index(self, form=None): """Display a page to input user ID and suite ID.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, } if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template("index.html").render(**data) except jinja2.TemplateError: traceback.print_exc() @cherrypy.expose def broadcast_states(self, user, suite, form=None): """List current broadcasts of a running or completed suite.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_states", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), } data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = ( self.get_last_activity_time(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_states"] = ( self.bush_dao.get_suite_broadcast_states(user, suite)) if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template( "broadcast-states.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return simplejson.dumps(data) @cherrypy.expose def broadcast_events(self, user, suite, form=None): """List broadcasts history of a running or completed suite.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_events", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) } data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_events"] = ( self.bush_dao.get_suite_broadcast_events(user, suite)) if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template( "broadcast-events.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return simplejson.dumps(data) @cherrypy.expose def cycles( self, user, suite, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List cycles of a running or completed suite.""" conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "cycles-per-page"], self.CYCLES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "is_option_on": ( order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "order": order, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "cycles", "no_fuzzy_time": no_fuzzy_time, "states": {}, "per_page": per_page, "per_page_default": per_page_default, "page": page, "task_status_groups": self.bush_dao.TASK_STATUS_GROUPS, } data["entries"], data["of_n_entries"] = ( self.bush_dao.get_suite_cycles_summary( user, suite, order, per_page, (page - 1) * per_page)) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = ( self.get_last_activity_time(user, suite)) data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template("cycles.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return simplejson.dumps(data) @cherrypy.expose def taskjobs( self, user, suite, page=1, cycles=None, tasks=None, task_status=None, job_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """List task jobs. user -- A string containing a valid user ID suite -- A string containing a valid suite ID page -- The page number to display cycles -- Display only task jobs matching these cycles. A value in the list can be a cycle, the string "before|after CYCLE", or a glob to match cycles. tasks -- Display only jobs for task names matching a list of names. The list should be specified as a string which will be shlex.split by this method. Values can be a valid task name or a glob like pattern for matching valid task names. task_status -- Select by task statuses. job_status -- Select by job status. See RoseBushDAO.JOB_STATUS_COMBOS for detail. order -- Order search in a predetermined way. A valid value is one of "time_desc", "time_asc", "cycle_desc_name_desc", "cycle_desc_name_asc", "cycle_asc_name_desc", "cycle_asc_name_asc", "name_asc_cycle_asc", "name_desc_cycle_asc", "name_asc_cycle_desc", "name_desc_cycle_desc", "time_submit_desc", "time_submit_asc", "time_run_desc", "time_run_asc", "time_run_exit_desc", "time_run_exit_asc", "duration_queue_desc", "duration_queue_asc", "duration_run_desc", "duration_run_asc", "duration_queue_run_desc", "duration_queue_run_asc" per_page -- Number of entries to display per page (defualt=32) no_fuzzy_time -- Don't display fuzzy time if this is True. form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "jobs-per-page"], self.JOBS_PER_PAGE)) per_page_max = int(conf.get_value( ["rose-bush", "jobs-per-page-max"], self.JOBS_PER_PAGE_MAX)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default is_option_on = ( cycles or tasks or task_status or job_status or order is not None and order != "time_desc" or per_page != per_page_default ) if page and per_page: page = int(page) else: page = 1 task_statuses = ( [[item, ""] for item in self.bush_dao.TASK_STATUSES]) if task_status: if not isinstance(task_status, list): task_status = [task_status] for item in task_statuses: if not task_status or item[0] in task_status: item[1] = "1" all_task_statuses = all([status[1] == "1" for status in task_statuses]) if all_task_statuses: task_status = [] data = { "cycles": cycles, "host": self.host_name, "is_option_on": is_option_on, "logo": self.logo, "method": "taskjobs", "no_fuzzy_time": no_fuzzy_time, "all_task_statuses": all_task_statuses, "task_statuses": task_statuses, "job_status": job_status, "order": order, "page": page, "per_page": per_page, "per_page_default": per_page_default, "per_page_max": per_page_max, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "states": {}, "suite": suite, "tasks": tasks, "task_status_groups": self.bush_dao.TASK_STATUS_GROUPS, "title": self.title, "user": user, } if cycles: cycles = shlex.split(str(cycles)) if tasks: tasks = shlex.split(str(tasks)) data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = ( self.get_last_activity_time(user, suite)) entries, of_n_entries = self.bush_dao.get_suite_job_entries( user, suite, cycles, tasks, task_status, job_status, order, per_page, (page - 1) * per_page) data["entries"] = entries data["of_n_entries"] = of_n_entries if per_page: data["n_pages"] = of_n_entries / per_page if of_n_entries % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template("taskjobs.html").render( **data) except jinja2.TemplateError: traceback.print_exc() @cherrypy.expose def jobs(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """(Deprecated) Redirect to self.taskjobs. Convert "no_status" to "task_status" argument of self.taskjobs. """ task_status = None if no_status: task_status = [] if not isinstance(no_status, list): no_status = [no_status] for key, values in self.bush_dao.TASK_STATUS_GROUPS.items(): if key not in no_status: task_status += values return self.taskjobs( user, suite, page, cycles, tasks, task_status, None, order, per_page, no_fuzzy_time, form) @cherrypy.expose def suites(self, user, names=None, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List (installed) suites of a user. user -- A string containing a valid user ID form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir_root = self._get_user_suite_dir_root(user) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "suites-per-page"], self.SUITES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "suites", "no_fuzzy_time": no_fuzzy_time, "user": user, "is_option_on": ( names and shlex.split(str(names)) != ["*"] or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "names": names, "page": page, "order": order, "per_page": per_page, "per_page_default": per_page_default, "entries": [], } name_globs = ["*"] if names: name_globs = shlex.split(str(names)) # Get entries sub_names = [ ".service", "log", "share", "work", self.bush_dao.SUITE_CONF] for dirpath, dnames, fnames in os.walk(user_suite_dir_root): if any([name in dnames or name in fnames for name in sub_names]): dnames[:] = [] else: continue item = os.path.relpath(dirpath, user_suite_dir_root) if not any([fnmatch(item, glob_) for glob_ in name_globs]): continue try: data["entries"].append({ "name": item, "info": {}, "last_activity_time": ( self.get_last_activity_time(user, item))}) except OSError: continue if order == "name_asc": data["entries"].sort(key=lambda entry: entry["name"]) elif order == "name_desc": data["entries"].sort(key=lambda entry: entry["name"], reverse=True) elif order == "time_asc": data["entries"].sort(self._sort_summary_entries, reverse=True) else: # order == "time_desc" data["entries"].sort(self._sort_summary_entries) data["of_n_entries"] = len(data["entries"]) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 offset = (page - 1) * per_page data["entries"] = data["entries"][offset:offset + per_page] else: data["n_pages"] = 1 # Get suite info for each entry for entry in data["entries"]: user_suite_dir = os.path.join(user_suite_dir_root, entry["name"]) rose_suite_info = os.path.join(user_suite_dir, "rose-suite.info") try: info_root = rose.config.load(rose_suite_info) for key, node in info_root.value.items(): if (node.is_ignored() or not isinstance(node.value, str)): continue entry["info"][key] = node.value except (IOError, rose.config.ConfigSyntaxError): pass data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return simplejson.dumps(data) template = self.template_env.get_template("suites.html") return template.render(**data) def get_file(self, user, suite, path, path_in_tar=None, mode=None): """Returns file information / content or a cherrypy response.""" f_name = self._get_user_suite_dir(user, suite, path) conf = ResourceLocator.default().get_conf() view_size_max = int(conf.get_value( ["rose-bush", "view-size-max"], self.VIEW_SIZE_MAX)) if path_in_tar: tar_f = tarfile.open(f_name, "r:gz") try: tar_info = tar_f.getmember(path_in_tar) except KeyError: raise cherrypy.HTTPError(404) f_size = tar_info.size handle = tar_f.extractfile(path_in_tar) if handle.read(2) == "#!": mime = self.MIME_TEXT_PLAIN else: mime = mimetypes.guess_type( urllib.pathname2url(path_in_tar))[0] handle.seek(0) if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): temp_f = NamedTemporaryFile() f_bsize = os.fstatvfs(temp_f.fileno()).f_bsize while True: bytes_ = handle.read(f_bsize) if not bytes_: break temp_f.write(bytes_) cherrypy.response.headers["Content-Type"] = mime try: return cherrypy.lib.static.serve_file(temp_f.name, mime) finally: temp_f.close() text = handle.read() else: f_size = os.stat(f_name).st_size if open(f_name).read(2) == "#!": mime = self.MIME_TEXT_PLAIN else: mime = mimetypes.guess_type(urllib.pathname2url(f_name))[0] if not mime: mime = self.MIME_TEXT_PLAIN if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): cherrypy.response.headers["Content-Type"] = mime return cherrypy.lib.static.serve_file(f_name, mime) text = open(f_name).read() try: if mode in [None, "text"]: text = jinja2.escape(text) lines = [unicode(line) for line in text.splitlines()] except UnicodeDecodeError: if path_in_tar: handle.seek(0) # file closed by cherrypy return cherrypy.lib.static.serve_fileobj( handle, self.MIME_TEXT_PLAIN) else: return cherrypy.lib.static.serve_file( f_name, self.MIME_TEXT_PLAIN) else: if path_in_tar: handle.close() name = path if path_in_tar: name = "log/" + path_in_tar job_entry = None if name.startswith("log/job"): names = self.bush_dao.parse_job_log_rel_path(name) if len(names) == 4: cycle, task, submit_num, _ = names entries = self.bush_dao.get_suite_job_entries( user, suite, [cycle], [task], None, None, None, None, None)[0] for entry in entries: if entry["submit_num"] == int(submit_num): job_entry = entry break if fnmatch(os.path.basename(path), "rose*.conf"): file_content = "rose-conf" else: file_content = self.bush_dao.is_conf(path) return lines, job_entry, file_content, f_name def get_last_activity_time(self, user, suite): """Returns last activity time for a suite based on database stat""" for name in [os.path.join("log", "db"), "cylc-suite.db"]: fname = os.path.join(self._get_user_suite_dir(user, suite), name) try: return strftime( "%Y-%m-%dT%H:%M:%SZ", gmtime(os.stat(fname).st_mtime)) except OSError: continue @cherrypy.expose def viewsearch(self, user, suite, path=None, path_in_tar=None, mode=None, search_string=None, search_mode=None): """Search a text log file.""" # get file or serve raw data file_output = self.get_file( user, suite, path, path_in_tar=path_in_tar, mode=mode) if isinstance(file_output, tuple): lines, _, file_content, _ = self.get_file( user, suite, path, path_in_tar=path_in_tar, mode=mode) else: return file_output template = self.template_env.get_template("view-search.html") if search_string: results = [] line_numbers = [] # perform search for i, line in enumerate(lines): if search_mode is None or search_mode == self.SEARCH_MODE_TEXT: match = line.find(search_string) if match == -1: continue start = match end = start + len(search_string) elif search_mode == self.SEARCH_MODE_REGEX: match = re.search(search_string, line) if not match: continue start, end = match.span() else: # ERROR: un-reccognised search_mode break # if line matches search string include in results results.append([line[:start], line[start:end], line[end:]]) if mode in [None, "text"]: line_numbers.append(i + 1) # line numbers start from 1 lines = results else: # no search is being performed, client is requesting the whole # page if mode in [None, "text"]: line_numbers = range(1, len(lines) + 1) else: line_numbers = [] lines = [[line] for line in lines] return template.render( lines=lines, line_numbers=line_numbers, file_content=file_content ) @cherrypy.expose def view(self, user, suite, path, path_in_tar=None, mode=None, no_fuzzy_time="0"): """View a text log file.""" # get file or serve raw data file_output = self.get_file( user, suite, path, path_in_tar=path_in_tar, mode=mode) if isinstance(file_output, tuple): lines, job_entry, file_content, f_name = self.get_file( user, suite, path, path_in_tar=path_in_tar, mode=mode) else: return file_output template = self.template_env.get_template("view.html") data = {} data.update(self._get_suite_logs_info(user, suite)) return template.render( rose_version=self.rose_version, script=cherrypy.request.script_name, method="view", time=strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), logo=self.logo, title=self.title, host=self.host_name, user=user, suite=suite, path=path, path_in_tar=path_in_tar, f_name=f_name, mode=mode, no_fuzzy_time=no_fuzzy_time, file_content=file_content, lines=lines, entry=job_entry, task_status_groups=self.bush_dao.TASK_STATUS_GROUPS, **data) def _get_suite_logs_info(self, user, suite): """Return a dict with suite logs and Rosie suite info.""" data = {"info": {}, "files": {}} user_suite_dir = self._get_user_suite_dir(user, suite) # rose-suite.info info_name = os.path.join(user_suite_dir, "rose-suite.info") if os.path.isfile(info_name): try: info_root = rose.config.load(info_name) for key, node in info_root.value.items(): if node.is_ignored() or not isinstance(node.value, str): continue data["info"][key] = node.value except rose.config.ConfigSyntaxError: pass # rose-suite-run.conf, rose-suite-run.log, rose-suite-run.version data["files"]["rose"] = {} for key in ["conf", "log", "version"]: f_name = os.path.join(user_suite_dir, "log/rose-suite-run." + key) if os.path.isfile(f_name): stat = os.stat(f_name) data["files"]["rose"]["log/rose-suite-run." + key] = { "path": "log/rose-suite-run." + key, "mtime": stat.st_mtime, "size": stat.st_size} # Other version files for f_name in glob(os.path.join(user_suite_dir, "log/*.version")): if os.path.basename(f_name).startswith("rose-"): continue name = os.path.join("log", os.path.basename(f_name)) stat = os.stat(f_name) data["files"]["rose"]["other:" + name] = { "path": name, "mtime": stat.st_mtime, "size": stat.st_size} k, logs_info = self.bush_dao.get_suite_logs_info(user, suite) data["files"][k] = logs_info return data @classmethod def _check_dir_access(cls, path): """Check directory is accessible. Raise 404 if path does not exist, or 403 if path not accessible. Return path on success. """ if not os.path.exists(path): raise cherrypy.HTTPError(404) if not os.access(path, os.R_OK): raise cherrypy.HTTPError(403) return path @staticmethod def _get_user_home(user): """Return, e.g. ~/cylc-run/ for a cylc suite. N.B. os.path.expanduser does not fail if ~user is invalid. """ try: return pwd.getpwnam(user).pw_dir except KeyError: raise cherrypy.HTTPError(404) def _get_user_suite_dir_root(self, user): """Return, e.g. ~user/cylc-run/ for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.bush_dao.SUITE_DIR_REL_ROOT)) def _get_user_suite_dir(self, user, suite, *paths): """Return, e.g. ~user/cylc-run/suite/... for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.bush_dao.SUITE_DIR_REL_ROOT, suite, *paths)) @staticmethod def _sort_summary_entries(suite1, suite2): """Sort suites by last_activity_time.""" return (cmp(suite2.get("last_activity_time"), suite1.get("last_activity_time")) or cmp(suite1["name"], suite2["name"]))
class SuiteRunner(Runner): """Invoke a Rose suite.""" SLEEP_PIPE = 0.05 NAME = "suite" OPTIONS = ["conf_dir", "defines", "defines_suite", "gcontrol_mode", "host", "install_only_mode", "local_install_only_mode", "log_archive_mode", "log_keep", "log_name", "name", "new_mode", "no_overwrite_mode", "opt_conf_keys", "reload_mode", "remote", "restart_mode", "run_mode", "strict_mode"] REC_DONT_SYNC = re.compile( r"\A(?:\..*|cylc-suite\.db.*|log(?:\..*)*|state|share|work)\Z") def __init__(self, *args, **kwargs): Runner.__init__(self, *args, **kwargs) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_run_cleaner = SuiteRunCleaner( event_handler=self.event_handler, host_selector=self.host_selector, suite_engine_proc=self.suite_engine_proc) def run_impl(self, opts, args, uuid, work_files): # Log file, temporary if hasattr(self.event_handler, "contexts"): t_file = TemporaryFile() log_context = ReporterContext(None, self.event_handler.VV, t_file) self.event_handler.contexts[uuid] = log_context # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() # Suite name from the current working directory if opts.conf_dir: self.fs_util.chdir(opts.conf_dir) opts.conf_dir = os.getcwd() if opts.defines_suite: suite_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF if not opts.defines: opts.defines = [] for define in opts.defines_suite: opts.defines.append("[" + suite_section + "]" + define) # --remote=KEY=VALUE,... if opts.remote: # opts.name always set for remote. return self._run_remote(opts, opts.name) conf_tree = self.config_load(opts) self.fs_util.chdir(conf_tree.conf_dirs[0]) suite_name = opts.name if not opts.name: suite_name = os.path.basename(os.getcwd()) # Automatic Rose constants # ROSE_ORIG_HOST: originating host # ROSE_VERSION: Rose version (not retained in run_mode=="reload") # Suite engine version jinja2_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF my_rose_version = ResourceLocator.default().get_version() suite_engine_key = self.suite_engine_proc.get_version_env_name() if opts.run_mode == "reload": prev_config_path = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") prev_config = ConfigLoader()(prev_config_path) suite_engine_version = prev_config.get_value( ["env", suite_engine_key]) else: suite_engine_version = self.suite_engine_proc.get_version() auto_items = {"ROSE_ORIG_HOST": socket.gethostname(), "ROSE_VERSION": ResourceLocator.default().get_version(), suite_engine_key: suite_engine_version} for key, val in auto_items.items(): requested_value = conf_tree.node.get_value(["env", key]) if requested_value: if key == "ROSE_VERSION" and val != requested_value: exc = VersionMismatchError(requested_value, val) raise ConfigValueError(["env", key], requested_value, exc) val = requested_value else: conf_tree.node.set(["env", key], val, state=conf_tree.node.STATE_NORMAL) conf_tree.node.set([jinja2_section, key], '"' + val + '"') # See if suite is running or not hosts = [] if opts.host: hosts.append(opts.host) conf = ResourceLocator.default().get_conf() known_hosts = self.host_selector.expand( conf.get_value(["rose-suite-run", "hosts"], "").split() + conf.get_value(["rose-suite-run", "scan-hosts"], "").split() + ["localhost"])[0] known_hosts = list(set(known_hosts)) for known_host in known_hosts: if known_host not in hosts: hosts.append(known_host) if opts.run_mode == "reload": suite_run_hosts = self.suite_engine_proc.ping(suite_name, hosts) if not suite_run_hosts: raise NotRunningError(suite_name) hosts = suite_run_hosts else: self.suite_engine_proc.check_suite_not_running(suite_name, hosts) # Install the suite to its run location suite_dir_rel = self._suite_dir_rel(suite_name) suite_dir = os.path.join(os.path.expanduser("~"), suite_dir_rel) suite_conf_dir = os.getcwd() locs_conf = ConfigNode() if opts.new_mode: if os.getcwd() == suite_dir: raise NewModeError("PWD", os.getcwd()) elif opts.run_mode in ["reload", "restart"]: raise NewModeError("--run", opts.run_mode) self.suite_run_cleaner.clean(suite_name) if os.getcwd() != suite_dir: if opts.run_mode == "run": self._run_init_dir(opts, suite_name, conf_tree, locs_conf=locs_conf) os.chdir(suite_dir) # Housekeep log files if not opts.install_only_mode and not opts.local_install_only_mode: self._run_init_dir_log(opts) self.fs_util.makedirs("log/suite") # Rose configuration and version logs self.fs_util.makedirs("log/rose-conf") run_mode = opts.run_mode if run_mode not in ["reload", "restart", "run"]: run_mode = "run" mode = run_mode if opts.install_only_mode: mode = "install-only" elif opts.local_install_only_mode: mode = "local-install-only" prefix = "rose-conf/%s-%s" % (strftime("%Y%m%dT%H%M%S"), mode) # Dump the actual configuration as rose-suite-run.conf ConfigDumper()(conf_tree.node, "log/" + prefix + ".conf") # Install version information file write_source_vc_info( suite_conf_dir, "log/" + prefix + ".version", self.popen) # If run through rose-stem, install version information files for # each source tree if they're a working copy if hasattr(opts, 'source') and hasattr(opts, 'project'): for i, url in enumerate(opts.source): if os.path.isdir(url): write_source_vc_info( url, "log/" + opts.project[i] + "-" + str(i) + ".version", self.popen) for ext in [".conf", ".version"]: self.fs_util.symlink(prefix + ext, "log/rose-suite-run" + ext) # Move temporary log to permanent log if hasattr(self.event_handler, "contexts"): log_file_path = os.path.abspath( os.path.join("log", "rose-suite-run.log")) log_file = open(log_file_path, "ab") temp_log_file = self.event_handler.contexts[uuid].handle temp_log_file.seek(0) log_file.write(temp_log_file.read()) self.event_handler.contexts[uuid].handle = log_file temp_log_file.close() # Create the suite log view self.suite_engine_proc.job_logs_db_create(suite_name, close=True) # Install share/work directories (local) for name in ["share", "work"]: self._run_init_dir_work(opts, suite_name, name, conf_tree, locs_conf=locs_conf) # Process Environment Variables environ = self.config_pm(conf_tree, "env") # Process Files cwd = os.getcwd() for rel_path, conf_dir in conf_tree.files.items(): if (conf_dir == cwd or self.REC_DONT_SYNC.match(rel_path) or conf_tree.node.get(["jinja2:" + rel_path]) is not None): continue # No sub-directories, very slow otherwise if os.sep in rel_path: rel_path = rel_path.split(os.sep, 1)[0] target_key = self.config_pm.get_handler("file").PREFIX + rel_path target_node = conf_tree.node.get([target_key]) if target_node is None: conf_tree.node.set([target_key]) target_node = conf_tree.node.get([target_key]) elif target_node.is_ignored(): continue source_node = target_node.get("source") if source_node is None: target_node.set(["source"], os.path.join(conf_dir, rel_path)) elif source_node.is_ignored(): continue self.config_pm(conf_tree, "file", no_overwrite_mode=opts.no_overwrite_mode) # Process Jinja2 configuration self.config_pm(conf_tree, "jinja2") # Ask suite engine to parse suite configuration # and determine if it is up to date (unchanged) suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf( suite_name, opts.strict_mode, opts.debug_mode) if opts.local_install_only_mode: return # Install suite files to each remote [user@]host for name in ["", "log/", "share/", "work/"]: uuid_file = os.path.abspath(name + uuid) open(uuid_file, "w").close() work_files.append(uuid_file) # Install items to user@host auths = self.suite_engine_proc.get_tasks_auths(suite_name) queue = [] # [[pipe, command, "ssh"|"rsync", auth], ...] for auth in sorted(auths): host = auth if "@" in auth: host = auth.split("@", 1)[1] command = self.popen.get_cmd("ssh", auth, "bash", "--login", "-c") rose_bin = "rose" for name in [host, "*"]: rose_home_node = conf.get(["rose-home-at", name], no_ignore=True) if rose_home_node is not None: rose_bin = "%s/bin/rose" % (rose_home_node.value) break # Build remote "rose suite-run" command rose_sr = "ROSE_VERSION=%s %s" % (my_rose_version, rose_bin) rose_sr += " suite-run -v -v --name=%s" % suite_name for key in ["new", "debug", "install-only"]: attr = key.replace("-", "_") + "_mode" if getattr(opts, attr, None) is not None: rose_sr += " --" + key if opts.log_keep: rose_sr += " --log-keep=" + opts.log_keep if opts.log_name: rose_sr += " --log-name=" + opts.log_name if not opts.log_archive_mode: rose_sr += " --no-log-archive" rose_sr += " --run=" + opts.run_mode host_confs = ["root-dir", "root-dir-share", "root-dir-work"] rose_sr += " --remote=uuid=" + uuid locs_conf.set([auth]) for key in host_confs: value = self._run_conf(key, host=host, conf_tree=conf_tree) if value is not None: val = self.popen.list_to_shell_str([str(value)]) rose_sr += "," + key + "=" + val locs_conf.set([auth, key], value) command += ["'" + rose_sr + "'"] pipe = self.popen.run_bg(*command) queue.append([pipe, command, "ssh", auth]) while queue: sleep(self.SLEEP_PIPE) pipe, command, command_name, auth = queue.pop(0) if pipe.poll() is None: queue.append([pipe, command, command_name, auth]) # put it back continue ret_code = pipe.wait() out, err = pipe.communicate() if ret_code: raise RosePopenError(command, ret_code, out, err) if command_name == "rsync": self.handle_event(out, level=Event.VV) continue else: self.handle_event(out, level=Event.VV, prefix="[%s] " % auth) for line in out.split("\n"): if "/" + uuid == line.strip(): locs_conf.unset([auth]) break else: filters = {"excludes": [], "includes": []} for name in ["", "log/", "share/", "work/"]: filters["excludes"].append(name + uuid) target = auth + ":" + suite_dir_rel cmd = self._get_cmd_rsync(target, **filters) queue.append([self.popen.run_bg(*cmd), cmd, "rsync", auth]) # Install ends ConfigDumper()(locs_conf, os.path.join("log", "rose-suite-run.locs")) if opts.install_only_mode: return elif opts.run_mode == "reload" and suite_conf_unchanged: conf_name = self.suite_engine_proc.SUITE_CONF self.handle_event(SkipReloadEvent(suite_name, conf_name)) return # Start the suite self.fs_util.chdir("log") ret = 0 host = hosts[0] # FIXME: should sync files to suite host? if opts.host: hosts = [host] # For run and restart, get host for running the suite if opts.run_mode != "reload" and not opts.host: hosts = [] val = conf.get_value(["rose-suite-run", "hosts"], "localhost") known_hosts = self.host_selector.expand(val.split())[0] for known_host in known_hosts: if known_host not in hosts: hosts.append(known_host) if hosts == ["localhost"]: host = hosts[0] else: host = self.host_selector(hosts)[0][0] self.handle_event(SuiteHostSelectEvent(suite_name, run_mode, host)) # FIXME: values in environ were expanded in the localhost self.suite_engine_proc.run( suite_name, host, environ, opts.run_mode, args) open("rose-suite-run.host", "w").write(host + "\n") # Disconnect log file handle, so monitoring tool command will no longer # be associated with the log file. self.event_handler.contexts[uuid].handle.close() self.event_handler.contexts.pop(uuid) # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and opts.gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return ret @classmethod def _run_conf( cls, key, default=None, host=None, conf_tree=None, r_opts=None): """Return the value of a setting given by a key for a given host. If r_opts is defined, we are alerady in a remote host, so there is no need to do a host match. Otherwise, the setting may be found in the run time configuration, or the default (i.e. site/user configuration). The value of each setting in the configuration would be in a line delimited list of PATTERN=VALUE pairs. """ if r_opts is not None: return r_opts.get(key, default) if host is None: host = "localhost" for conf, keys in [ (conf_tree.node, []), (ResourceLocator.default().get_conf(), ["rose-suite-run"])]: if conf is None: continue node_value = conf.get_value(keys + [key]) if node_value is None: continue for line in node_value.strip().splitlines(): pattern, value = line.strip().split("=", 1) if pattern.startswith("jinja2:"): section, name = pattern.rsplit(":", 1) p_node = conf.get([section, name], no_ignore=True) # Values in "jinja2:*" section are quoted. pattern = ast.literal_eval(p_node.value) if fnmatchcase(host, pattern): return value.strip() return default def _run_init_dir(self, opts, suite_name, conf_tree=None, r_opts=None, locs_conf=None): """Create the suite's directory.""" suite_dir_rel = self._suite_dir_rel(suite_name) home = os.path.expanduser("~") suite_dir_root = self._run_conf("root-dir", conf_tree=conf_tree, r_opts=r_opts) if suite_dir_root: if locs_conf is not None: locs_conf.set(["localhost", "root-dir"], suite_dir_root) suite_dir_root = env_var_process(suite_dir_root) suite_dir_home = os.path.join(home, suite_dir_rel) if (suite_dir_root and os.path.realpath(home) != os.path.realpath(suite_dir_root)): suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel) self.fs_util.makedirs(suite_dir_real) self.fs_util.symlink(suite_dir_real, suite_dir_home, opts.no_overwrite_mode) else: self.fs_util.makedirs(suite_dir_home) def _run_init_dir_log(self, opts): """Create the suite's log/ directory. Housekeep, archive old ones.""" # Do nothing in log append mode if log directory already exists if opts.run_mode in ["reload", "restart"] and os.path.isdir("log"): return # Log directory of this run now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") now_log = "log." + now_str self.fs_util.makedirs(now_log) self.fs_util.symlink(now_log, "log") now_log_name = getattr(opts, "log_name", None) if now_log_name: self.fs_util.symlink(now_log, "log." + now_log_name) # Keep log for this run and named logs logs = set(glob("log.*") + ["log"]) for log in list(logs): if os.path.islink(log): logs.remove(log) log_link = os.readlink(log) if log_link in logs: logs.remove(log_link) # Housekeep old logs, if necessary log_keep = getattr(opts, "log_keep", None) if log_keep: t_threshold = time() - abs(float(log_keep)) * 86400.0 for log in list(logs): if os.path.isfile(log): if t_threshold > os.stat(log).st_mtime: self.fs_util.delete(log) logs.remove(log) else: for root, _, files in os.walk(log): keep = False for file_ in files: path = os.path.join(root, file_) if (os.path.exists(path) and os.stat(path).st_mtime >= t_threshold): keep = True break if keep: break else: self.fs_util.delete(log) logs.remove(log) # Archive old logs, if necessary if getattr(opts, "log_archive_mode", True): for log in list(logs): if os.path.isfile(log): continue log_tar = log + ".tar" f_bsize = os.statvfs(log).f_bsize tar_handle = tarfile.open(log_tar, "w", bufsize=f_bsize) tar_handle.add(log) tar_handle.close() # N.B. Python's gzip is slow self.popen.run_simple("gzip", "-f", log_tar) self.handle_event(SuiteLogArchiveEvent(log_tar + ".gz", log)) self.fs_util.delete(log) def _run_init_dir_work(self, opts, suite_name, name, conf_tree=None, r_opts=None, locs_conf=None): """Create a named suite's directory.""" item_path = os.path.realpath(name) item_path_source = item_path key = "root-dir-" + name item_root = self._run_conf(key, conf_tree=conf_tree, r_opts=r_opts) if item_root is not None: if locs_conf is not None: locs_conf.set(["localhost", key], item_root) item_root = env_var_process(item_root) suite_dir_rel = self._suite_dir_rel(suite_name) item_path_source = os.path.join(item_root, suite_dir_rel, name) item_path_source = os.path.realpath(item_path_source) if item_path == item_path_source: if opts.new_mode: self.fs_util.delete(name) self.fs_util.makedirs(name) else: if opts.new_mode: self.fs_util.delete(item_path_source) self.fs_util.makedirs(item_path_source) self.fs_util.symlink(item_path_source, name, opts.no_overwrite_mode) def _run_remote(self, opts, suite_name): """rose suite-run --remote=KEY=VALUE,...""" suite_dir_rel = self._suite_dir_rel(suite_name) r_opts = {} for item in opts.remote.split(","): key, val = item.split("=", 1) r_opts[key] = val uuid_file = os.path.join(suite_dir_rel, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("/" + r_opts["uuid"] + "\n", level=0) elif opts.new_mode: self.fs_util.delete(suite_dir_rel) if opts.run_mode == "run": self._run_init_dir(opts, suite_name, r_opts=r_opts) os.chdir(suite_dir_rel) for name in ["share", "work"]: uuid_file = os.path.join(name, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event(name + "/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_work(opts, suite_name, name, r_opts=r_opts) if not opts.install_only_mode: uuid_file = os.path.join("log", r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("log/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_log(opts) self.fs_util.makedirs("log/suite") def _get_cmd_rsync(self, target, excludes=None, includes=None): """rsync relevant suite items to target.""" if excludes is None: excludes = [] if includes is None: includes = [] cmd = self.popen.get_cmd("rsync", "--delete-excluded") for exclude in excludes: cmd.append("--exclude=" + exclude) for include in includes: cmd.append("--include=" + include) for item in os.listdir("."): if not self.REC_DONT_SYNC.match(item) or item in excludes: cmd.append(item) cmd.append(target) return cmd def _suite_dir_rel(self, suite_name): """Return the relative path to the suite running directory.""" return self.suite_engine_proc.get_suite_dir_rel(suite_name)
class StemRunner(object): """Set up options for running a STEM job through Rose.""" def __init__(self, opts, reporter=None, popen=None, fs_util=None): self.opts = opts if reporter is None: self.reporter = Reporter(opts.verbosity - opts.quietness) else: self.reporter = reporter if popen is None: self.popen = RosePopener(event_handler=self.reporter) else: self.popen = popen if fs_util is None: self.fs_util = FileSystemUtil(event_handler=self.reporter) else: self.fs_util = fs_util self.host_selector = HostSelector(event_handler=self.reporter, popen=self.popen) def _add_define_option(self, var, val): """Add a define option passed to the SuiteRunner.""" if self.opts.defines: self.opts.defines.append(SUITE_RC_PREFIX + var + '=' + val) else: self.opts.defines = [SUITE_RC_PREFIX + var + '=' + val] self.reporter(ConfigVariableSetEvent(var, val)) return def _get_base_dir(self, item): """Given a source tree return the following from 'fcm loc-layout': * url * sub_tree * peg_rev * root * project """ ret_code, output, stderr = self.popen.run('fcm', 'loc-layout', item) if ret_code != 0: raise ProjectNotFoundException(item, stderr) ret = {} for line in output.splitlines(): if ":" not in line: continue key, value = line.split(":", 1) if key: if value: ret[key] = value.strip() return ret def _get_project_from_url(self, source_dict): """Run 'fcm keyword-print' to work out the project name.""" repo = source_dict['root'] if source_dict['project']: repo += '/' + source_dict['project'] kpoutput = self.popen.run('fcm', 'kp', source_dict['url'])[1] project = None for line in kpoutput.splitlines(): if line.rstrip().endswith(repo): kpresult = re.search(r'^location{primary}\[(.*)\]', line) if kpresult: project = kpresult.group(1) break return project def _deduce_mirror(self, source_dict, project): """Deduce the mirror location of this source tree.""" # Root location for project proj_root = source_dict['root'] + '/' + source_dict['project'] # Swap project to mirror project = re.sub(r'\.x$', r'.xm', project) mirror_repo = "fcm:" + project # Generate mirror location mirror = re.sub(proj_root, mirror_repo, source_dict['url']) # Remove any sub-tree mirror = re.sub(source_dict['sub_tree'], r'', mirror) mirror = re.sub(r'/@', r'@', mirror) # Add forwards slash after .xm if missing if '.xm/' not in mirror: mirror = re.sub(r'\.xm', r'.xm/', mirror) return mirror def _ascertain_project(self, item): """Set the project name and top-level from 'fcm loc-layout'. Returns: * project name * top-level location of the source tree with revision number * top-level location of the source tree without revision number * revision number """ project = None try: project, item = item.split("=", 1) except ValueError: pass if re.search(r'^\.', item): item = os.path.abspath(os.path.join(os.getcwd(), item)) if project is not None: print "[WARN] Forcing project for '{0}' to be '{1}'".format( item, project) return project, item, item, '', '' source_dict = self._get_base_dir(item) project = self._get_project_from_url(source_dict) if not project: raise ProjectNotFoundException(item) mirror = self._deduce_mirror(source_dict, project) if 'peg_rev' in source_dict and '@' in item: revision = '@' + source_dict['peg_rev'] base = re.sub(r'@.*', r'', item) else: revision = '' base = item # Remove subtree from base and item if 'sub_tree' in source_dict: item = re.sub(r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', item, count=1) base = re.sub(r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', base, count=1) # Remove trailing forwards-slash item = re.sub(r'/$', r'', item) base = re.sub(r'/$', r'', base) # Remove anything after a point project = re.sub(r'\..*', r'', project) return project, item, base, revision, mirror def _generate_name(self): """Generate a suite name from the name of the first source tree.""" try: basedir = self._ascertain_project(os.getcwd())[1] except ProjectNotFoundException: if self.opts.conf_dir: basedir = os.path.abspath(self.opts.conf_dir) else: basedir = os.getcwd() name = os.path.basename(basedir) self.reporter(NameSetEvent(name)) return name def _this_suite(self): """Find the location of the suite in the first source tree.""" # Get base of first source basedir = '' if self.opts.source: basedir = self.opts.source[0] else: basedir = self._ascertain_project(os.getcwd())[1] suitedir = os.path.join(basedir, DEFAULT_TEST_DIR) suitefile = os.path.join(suitedir, "rose-suite.conf") if not os.path.isfile(suitefile): raise RoseSuiteConfNotFoundException(suitedir) self._check_suite_version(suitefile) return suitedir def _read_auto_opts(self): """Read the site rose.conf file.""" return ResourceLocator.default().get_conf().get_value( ["rose-stem", "automatic-options"]) def _check_suite_version(self, fname): """Check the suite is compatible with this version of rose-stem.""" if not os.path.isfile(fname): raise RoseSuiteConfNotFoundException(os.path.dirname(fname)) config = rose.config.load(fname) suite_rose_stem_version = config.get(['ROSE_STEM_VERSION']) if suite_rose_stem_version: suite_rose_stem_version = int(suite_rose_stem_version.value) else: suite_rose_stem_version = None if not suite_rose_stem_version == ROSE_STEM_VERSION: raise RoseStemVersionException(suite_rose_stem_version) def _prepend_localhost(self, url): """Prepend the local hostname to urls which do not point to repository locations.""" if ':' not in url or url.split( ':', 1)[0] not in ['svn', 'fcm', 'http', 'https', 'svn+ssh']: url = self.host_selector.get_local_host() + ':' + url return url def process(self): """Process STEM options into 'rose suite-run' options.""" # Generate options for source trees repos = {} repos_with_hosts = {} if not self.opts.source: self.opts.source = ['.'] self.opts.project = list() for i, url in enumerate(self.opts.source): project, url, base, rev, mirror = self._ascertain_project(url) self.opts.source[i] = url self.opts.project.append(project) # Versions of variables with hostname prepended for working copies url_host = self._prepend_localhost(url) base_host = self._prepend_localhost(base) if project in repos: repos[project].append(url) repos_with_hosts[project].append(url_host) else: repos[project] = [url] repos_with_hosts[project] = [url_host] self._add_define_option('SOURCE_' + project.upper() + '_REV', '"' + rev + '"') self._add_define_option('SOURCE_' + project.upper() + '_BASE', '"' + base + '"') self._add_define_option( 'HOST_SOURCE_' + project.upper() + '_BASE', '"' + base_host + '"') self._add_define_option( 'SOURCE_' + project.upper() + '_MIRROR', '"' + mirror + '"') self.reporter(SourceTreeAddedAsBranchEvent(url)) for project, branches in repos.iteritems(): var = 'SOURCE_' + project.upper() branchstring = RosePopener.list_to_shell_str(branches) self._add_define_option(var, '"' + branchstring + '"') for project, branches in repos_with_hosts.iteritems(): var_host = 'HOST_SOURCE_' + project.upper() branchstring = RosePopener.list_to_shell_str(branches) self._add_define_option(var_host, '"' + branchstring + '"') # Generate the variable containing tasks to run if self.opts.group: if not self.opts.defines: self.opts.defines = [] expanded_groups = [] for i in self.opts.group: expanded_groups.extend(i.split(',')) self.opts.defines.append(SUITE_RC_PREFIX + 'RUN_NAMES=' + str(expanded_groups)) # Load the config file and return any automatic-options auto_opts = self._read_auto_opts() if auto_opts: automatic_options = auto_opts.split() for option in automatic_options: elements = option.split("=") if len(elements) == 2: self._add_define_option(elements[0], '"' + elements[1] + '"') # Change into the suite directory if self.opts.conf_dir: self.reporter(SuiteSelectionEvent(self.opts.conf_dir)) self._check_suite_version( os.path.join(self.opts.conf_dir, 'rose-suite.conf')) else: thissuite = self._this_suite() self.fs_util.chdir(thissuite) self.reporter(SuiteSelectionEvent(thissuite)) # Create a default name for the suite; allow override by user if not self.opts.name: self.opts.name = self._generate_name() return self.opts
def run(self, app_runner, conf_tree, opts, args, uuid, work_files): """Suite housekeeping application. This application is designed to work under "rose task-run" in a cycling suite. """ suite_name = os.getenv("ROSE_SUITE_NAME") if not suite_name: return # Tar-gzip job logs on suite host # Prune job logs on remote hosts and suite host prune_remote_logs_cycles = self._get_conf(app_runner, conf_tree, "prune-remote-logs-at") prune_server_logs_cycles = self._get_conf(app_runner, conf_tree, "prune-server-logs-at") archive_logs_cycles = self._get_conf(app_runner, conf_tree, "archive-logs-at") if (prune_remote_logs_cycles or prune_server_logs_cycles or archive_logs_cycles): tmp_prune_remote_logs_cycles = [] for cycle in prune_remote_logs_cycles: if cycle not in archive_logs_cycles: tmp_prune_remote_logs_cycles.append(cycle) prune_remote_logs_cycles = tmp_prune_remote_logs_cycles tmp_prune_server_logs_cycles = [] for cycle in prune_server_logs_cycles: if cycle not in archive_logs_cycles: tmp_prune_server_logs_cycles.append(cycle) prune_server_logs_cycles = tmp_prune_server_logs_cycles if prune_remote_logs_cycles: app_runner.suite_engine_proc.job_logs_pull_remote( suite_name, prune_remote_logs_cycles, prune_remote_mode=True) if prune_server_logs_cycles: app_runner.suite_engine_proc.job_logs_remove_on_server( suite_name, prune_server_logs_cycles) if archive_logs_cycles: app_runner.suite_engine_proc.job_logs_archive( suite_name, archive_logs_cycles) # Prune other directories globs, cycle_set = self._get_prune_globs(app_runner, conf_tree) if not globs: return suite_engine_proc = app_runner.suite_engine_proc hosts = suite_engine_proc.get_suite_jobs_auths( suite_name, [(cycle, None) for cycle in cycle_set]) # A shuffle here should allow the load for doing "rm -rf" to be shared # between job hosts who share a file system. shuffle(hosts) suite_dir_rel = suite_engine_proc.get_suite_dir_rel(suite_name) form_dict = {"d": suite_dir_rel, "g": " ".join(globs)} sh_cmd_head = r"set -e; cd %(d)s; " % form_dict sh_cmd = (r"set +e; ls -d %(g)s; " + r"set -e; rm -fr %(g)s") % form_dict cwd = os.getcwd() host_selector = HostSelector(app_runner.event_handler, app_runner.popen) for host in hosts + [host_selector.get_local_host()]: sdir = None try: if host_selector.is_local_host(host): sdir = suite_engine_proc.get_suite_dir(suite_name) app_runner.fs_util.chdir(sdir) out = app_runner.popen.run_ok("bash", "-O", "extglob", "-c", sh_cmd)[0] else: cmd = app_runner.popen.get_cmd( "ssh", host, "bash -O extglob -c '" + sh_cmd_head + sh_cmd + "'") out = app_runner.popen.run_ok(*cmd)[0] except RosePopenError as exc: app_runner.handle_event(exc) else: if sdir is None: event = FileSystemEvent(FileSystemEvent.CHDIR, host + ":" + suite_dir_rel) app_runner.handle_event(event) for line in sorted(out.splitlines()): if not host_selector.is_local_host(host): line = host + ":" + line event = FileSystemEvent(FileSystemEvent.DELETE, line) app_runner.handle_event(event) finally: if sdir: app_runner.fs_util.chdir(cwd) return
class SuiteRunner(Runner): """Invoke a Rose suite.""" SLEEP_PIPE = 0.05 NAME = "suite" OPTIONS = [ "conf_dir", "defines", "defines_suite", "gcontrol_mode", "host", "install_only_mode", "local_install_only_mode", "log_archive_mode", "log_keep", "log_name", "name", "new_mode", "no_overwrite_mode", "opt_conf_keys", "reload_mode", "remote", "restart_mode", "run_mode", "strict_mode" ] # Lists of rsync (always) exclude globs SYNC_EXCLUDES = ( "/.*", "/cylc-suite.db", "/log", "/log.*", "/state", "/share", "/work", ) def __init__(self, *args, **kwargs): Runner.__init__(self, *args, **kwargs) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_run_cleaner = SuiteRunCleaner( event_handler=self.event_handler, host_selector=self.host_selector, suite_engine_proc=self.suite_engine_proc) def run_impl(self, opts, args, uuid, work_files): # Log file, temporary if hasattr(self.event_handler, "contexts"): t_file = TemporaryFile() log_context = ReporterContext(None, self.event_handler.VV, t_file) self.event_handler.contexts[uuid] = log_context # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() # Suite name from the current working directory if opts.conf_dir: self.fs_util.chdir(opts.conf_dir) opts.conf_dir = os.getcwd() if opts.defines_suite: suite_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF if not opts.defines: opts.defines = [] for define in opts.defines_suite: opts.defines.append("[" + suite_section + "]" + define) # --remote=KEY=VALUE,... if opts.remote: # opts.name always set for remote. return self._run_remote(opts, opts.name) conf_tree = self.config_load(opts) self.fs_util.chdir(conf_tree.conf_dirs[0]) suite_name = opts.name if not opts.name: suite_name = os.path.basename(os.getcwd()) # Automatic Rose constants # ROSE_ORIG_HOST: originating host # ROSE_VERSION: Rose version (not retained in run_mode=="reload") # Suite engine version jinja2_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF my_rose_version = ResourceLocator.default().get_version() suite_engine_key = self.suite_engine_proc.get_version_env_name() if opts.run_mode in ["reload", "restart"]: prev_config_path = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") prev_config = ConfigLoader()(prev_config_path) suite_engine_version = prev_config.get_value( ["env", suite_engine_key]) else: suite_engine_version = self.suite_engine_proc.get_version() auto_items = { "ROSE_ORIG_HOST": self.host_selector.get_local_host(), "ROSE_VERSION": ResourceLocator.default().get_version(), suite_engine_key: suite_engine_version } for key, val in auto_items.items(): requested_value = conf_tree.node.get_value(["env", key]) if requested_value: if key == "ROSE_VERSION" and val != requested_value: exc = VersionMismatchError(requested_value, val) raise ConfigValueError(["env", key], requested_value, exc) val = requested_value else: conf_tree.node.set(["env", key], val, state=conf_tree.node.STATE_NORMAL) conf_tree.node.set([jinja2_section, key], '"' + val + '"') # See if suite is running or not hosts = [] if opts.host: hosts.append(opts.host) if opts.run_mode == "reload": suite_run_hosts = self.suite_engine_proc.get_suite_run_hosts( None, suite_name, hosts) if not suite_run_hosts: raise SuiteNotRunningError(suite_name) hosts = suite_run_hosts else: self.suite_engine_proc.check_suite_not_running(suite_name, hosts) # Install the suite to its run location suite_dir_rel = self._suite_dir_rel(suite_name) suite_dir = os.path.join(os.path.expanduser("~"), suite_dir_rel) suite_conf_dir = os.getcwd() locs_conf = ConfigNode() if opts.new_mode: if os.getcwd() == suite_dir: raise NewModeError("PWD", os.getcwd()) elif opts.run_mode in ["reload", "restart"]: raise NewModeError("--run", opts.run_mode) self.suite_run_cleaner.clean(suite_name) if os.getcwd() != suite_dir: if opts.run_mode == "run": self._run_init_dir(opts, suite_name, conf_tree, locs_conf=locs_conf) os.chdir(suite_dir) # Housekeep log files if not opts.install_only_mode and not opts.local_install_only_mode: self._run_init_dir_log(opts) self.fs_util.makedirs("log/suite") # Rose configuration and version logs self.fs_util.makedirs("log/rose-conf") run_mode = opts.run_mode if run_mode not in ["reload", "restart", "run"]: run_mode = "run" mode = run_mode if opts.install_only_mode: mode = "install-only" elif opts.local_install_only_mode: mode = "local-install-only" prefix = "rose-conf/%s-%s" % (strftime("%Y%m%dT%H%M%S"), mode) # Dump the actual configuration as rose-suite-run.conf ConfigDumper()(conf_tree.node, "log/" + prefix + ".conf") # Install version information file write_source_vc_info(suite_conf_dir, "log/" + prefix + ".version", self.popen) # If run through rose-stem, install version information files for # each source tree if they're a working copy if hasattr(opts, 'source') and hasattr(opts, 'project'): for i, url in enumerate(opts.source): if os.path.isdir(url): write_source_vc_info( url, "log/" + opts.project[i] + "-" + str(i) + ".version", self.popen) for ext in [".conf", ".version"]: self.fs_util.symlink(prefix + ext, "log/rose-suite-run" + ext) # Move temporary log to permanent log if hasattr(self.event_handler, "contexts"): log_file_path = os.path.abspath( os.path.join("log", "rose-suite-run.log")) log_file = open(log_file_path, "ab") temp_log_file = self.event_handler.contexts[uuid].handle temp_log_file.seek(0) log_file.write(temp_log_file.read()) self.event_handler.contexts[uuid].handle = log_file temp_log_file.close() # Install share/work directories (local) for name in ["share", "share/cycle", "work"]: self._run_init_dir_work(opts, suite_name, name, conf_tree, locs_conf=locs_conf) # Process Environment Variables environ = self.config_pm(conf_tree, "env") # Process Files cwd = os.getcwd() for rel_path, conf_dir in conf_tree.files.items(): if (conf_dir == cwd or any([ fnmatchcase(os.sep + rel_path, exclude) for exclude in self.SYNC_EXCLUDES ]) or conf_tree.node.get(["jinja2:" + rel_path]) is not None): continue # No sub-directories, very slow otherwise if os.sep in rel_path: rel_path = rel_path.split(os.sep, 1)[0] target_key = self.config_pm.get_handler("file").PREFIX + rel_path target_node = conf_tree.node.get([target_key]) if target_node is None: conf_tree.node.set([target_key]) target_node = conf_tree.node.get([target_key]) elif target_node.is_ignored(): continue source_node = target_node.get("source") if source_node is None: target_node.set(["source"], os.path.join(conf_dir, rel_path)) elif source_node.is_ignored(): continue self.config_pm(conf_tree, "file", no_overwrite_mode=opts.no_overwrite_mode) # Process Jinja2 configuration self.config_pm(conf_tree, "jinja2") # Ask suite engine to parse suite configuration # and determine if it is up to date (unchanged) suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf( suite_name, opts.run_mode, opts.strict_mode, opts.debug_mode) if opts.local_install_only_mode: return # Install suite files to each remote [user@]host for name in ["", "log/", "share/", "share/cycle/", "work/"]: uuid_file = os.path.abspath(name + uuid) open(uuid_file, "w").close() work_files.append(uuid_file) # Install items to user@host conf = ResourceLocator.default().get_conf() auths = self.suite_engine_proc.get_tasks_auths(suite_name) proc_queue = [] # [[proc, command, "ssh"|"rsync", auth], ...] for auth in sorted(auths): host = auth if "@" in auth: host = auth.split("@", 1)[1] # Remote shell command = self.popen.get_cmd("ssh", "-n", auth) # Provide ROSE_VERSION and CYLC_VERSION in the environment shcommand = "env ROSE_VERSION=%s %s=%s" % ( my_rose_version, suite_engine_key, suite_engine_version) # Use login shell? no_login_shell = self._run_conf("remote-no-login-shell", host=host, conf_tree=conf_tree) if not no_login_shell or no_login_shell.lower() != "true": shcommand += r""" bash -l -c '"$0" "$@"'""" # Path to "rose" command, if applicable rose_bin = self._run_conf("remote-rose-bin", host=host, conf_tree=conf_tree, default="rose") # Build remote "rose suite-run" command shcommand += " %s suite-run -vv -n %s" % (rose_bin, suite_name) for key in ["new", "debug", "install-only"]: attr = key.replace("-", "_") + "_mode" if getattr(opts, attr, None) is not None: shcommand += " --%s" % key if opts.log_keep: shcommand += " --log-keep=%s" % opts.log_keep if opts.log_name: shcommand += " --log-name=%s" % opts.log_name if not opts.log_archive_mode: shcommand += " --no-log-archive" shcommand += " --run=%s" % opts.run_mode # Build --remote= option shcommand += " --remote=uuid=%s" % uuid host_confs = [ "root-dir", "root-dir{share}", "root-dir{share/cycle}", "root-dir{work}" ] locs_conf.set([auth]) for key in host_confs: value = self._run_conf(key, host=host, conf_tree=conf_tree) if value is not None: val = self.popen.list_to_shell_str([str(value)]) shcommand += ",%s=%s" % (key, val) locs_conf.set([auth, key], value) command.append(shcommand) proc = self.popen.run_bg(*command) proc_queue.append([proc, command, "ssh", auth]) while proc_queue: sleep(self.SLEEP_PIPE) proc, command, command_name, auth = proc_queue.pop(0) if proc.poll() is None: # put it back in proc_queue proc_queue.append([proc, command, command_name, auth]) continue ret_code = proc.wait() out, err = proc.communicate() if ret_code: raise RosePopenError(command, ret_code, out, err) if command_name == "rsync": self.handle_event(out, level=Event.VV) continue else: self.handle_event(out, level=Event.VV, prefix="[%s] " % auth) for line in out.split("\n"): if "/" + uuid == line.strip(): locs_conf.unset([auth]) break else: filters = {"excludes": [], "includes": []} for name in ["", "log/", "share/", "share/cycle/", "work/"]: filters["excludes"].append(name + uuid) target = auth + ":" + suite_dir_rel cmd = self._get_cmd_rsync(target, **filters) proc_queue.append( [self.popen.run_bg(*cmd), cmd, "rsync", auth]) # Install ends ConfigDumper()(locs_conf, os.path.join("log", "rose-suite-run.locs")) if opts.install_only_mode: return elif opts.run_mode == "reload" and suite_conf_unchanged: conf_name = self.suite_engine_proc.SUITE_CONF self.handle_event(SkipReloadEvent(suite_name, conf_name)) return # Start the suite self.fs_util.chdir("log") ret = 0 # FIXME: should sync files to suite host? if opts.run_mode != "reload": if opts.host: hosts = [opts.host] else: names = shlex.split( conf.get_value(["rose-suite-run", "hosts"], "")) if names: hosts += self.host_selector.expand(names)[0] if (hosts and len(hosts) == 1 and self.host_selector.is_local_host(hosts[0])): host = "localhost" elif hosts: host = self.host_selector(hosts)[0][0] else: host = "localhost" self.handle_event(SuiteHostSelectEvent(suite_name, run_mode, host)) # FIXME: values in environ were expanded in the localhost self.suite_engine_proc.run(suite_name, host, environ, opts.run_mode, args) open("rose-suite-run.host", "w").write(host + "\n") # Disconnect log file handle, so monitoring tool command will no longer # be associated with the log file. self.event_handler.contexts[uuid].handle.close() self.event_handler.contexts.pop(uuid) # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and opts.gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return ret @classmethod def _run_conf(cls, key, default=None, host=None, conf_tree=None, r_opts=None): """Return the value of a setting given by a key for a given host. If r_opts is defined, we are alerady in a remote host, so there is no need to do a host match. Otherwise, the setting may be found in the run time configuration, or the default (i.e. site/user configuration). The value of each setting in the configuration would be in a line delimited list of PATTERN=VALUE pairs. """ if r_opts is not None: return r_opts.get(key, default) if host is None: host = "localhost" for conf, keys in [(conf_tree.node, []), (ResourceLocator.default().get_conf(), ["rose-suite-run"])]: if conf is None: continue node_value = conf.get_value(keys + [key]) if node_value is None: continue for line in node_value.strip().splitlines(): pattern, value = line.strip().split("=", 1) if pattern.startswith("jinja2:"): section, name = pattern.rsplit(":", 1) p_node = conf.get([section, name], no_ignore=True) # Values in "jinja2:*" section are quoted. pattern = ast.literal_eval(p_node.value) if fnmatchcase(host, pattern): return value.strip() return default def _run_init_dir(self, opts, suite_name, conf_tree=None, r_opts=None, locs_conf=None): """Create the suite's directory.""" suite_dir_rel = self._suite_dir_rel(suite_name) home = os.path.expanduser("~") suite_dir_root = self._run_conf("root-dir", conf_tree=conf_tree, r_opts=r_opts) if suite_dir_root: if locs_conf is not None: locs_conf.set(["localhost", "root-dir"], suite_dir_root) suite_dir_root = env_var_process(suite_dir_root) suite_dir_home = os.path.join(home, suite_dir_rel) if (suite_dir_root and os.path.realpath(home) != os.path.realpath(suite_dir_root)): suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel) self.fs_util.makedirs(suite_dir_real) self.fs_util.symlink(suite_dir_real, suite_dir_home, opts.no_overwrite_mode) else: self.fs_util.makedirs(suite_dir_home) def _run_init_dir_log(self, opts): """Create the suite's log/ directory. Housekeep, archive old ones.""" # Do nothing in log append mode if log directory already exists if opts.run_mode in ["reload", "restart"] and os.path.isdir("log"): return # Log directory of this run now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") now_log = "log." + now_str self.fs_util.makedirs(now_log) self.fs_util.symlink(now_log, "log") now_log_name = getattr(opts, "log_name", None) if now_log_name: self.fs_util.symlink(now_log, "log." + now_log_name) # Keep log for this run and named logs logs = set(glob("log.*") + ["log"]) for log in list(logs): if os.path.islink(log): logs.remove(log) log_link = os.readlink(log) if log_link in logs: logs.remove(log_link) # Housekeep old logs, if necessary log_keep = getattr(opts, "log_keep", None) if log_keep: t_threshold = time() - abs(float(log_keep)) * 86400.0 for log in list(logs): if os.path.isfile(log): if t_threshold > os.stat(log).st_mtime: self.fs_util.delete(log) logs.remove(log) else: for root, _, files in os.walk(log): keep = False for file_ in files: path = os.path.join(root, file_) if (os.path.exists(path) and os.stat(path).st_mtime >= t_threshold): keep = True break if keep: break else: self.fs_util.delete(log) logs.remove(log) # Archive old logs, if necessary if getattr(opts, "log_archive_mode", True): for log in list(logs): if os.path.isfile(log): continue log_tar_gz = log + ".tar.gz" try: self.popen.run_simple("tar", "-czf", log_tar_gz, log) except RosePopenError: try: self.fs_util.delete(log_tar_gz) except OSError: pass raise else: self.handle_event(SuiteLogArchiveEvent(log_tar_gz, log)) self.fs_util.delete(log) def _run_init_dir_work(self, opts, suite_name, name, conf_tree=None, r_opts=None, locs_conf=None): """Create a named suite's directory.""" item_path = os.path.realpath(name) item_path_source = item_path key = "root-dir{" + name + "}" item_root = self._run_conf(key, conf_tree=conf_tree, r_opts=r_opts) if item_root is None: # backward compat item_root = self._run_conf("root-dir-" + name, conf_tree=conf_tree, r_opts=r_opts) if item_root: if locs_conf is not None: locs_conf.set(["localhost", key], item_root) item_root = env_var_process(item_root) suite_dir_rel = self._suite_dir_rel(suite_name) if os.path.isabs(item_root): item_path_source = os.path.join(item_root, suite_dir_rel, name) else: item_path_source = item_root item_path_source = os.path.realpath(item_path_source) if item_path == item_path_source: if opts.new_mode: self.fs_util.delete(name) self.fs_util.makedirs(name) else: if opts.new_mode: self.fs_util.delete(item_path_source) self.fs_util.makedirs(item_path_source) if os.sep in name: dirname_of_name = os.path.dirname(name) self.fs_util.makedirs(dirname_of_name) item_path_source_rel = os.path.relpath( item_path_source, os.path.realpath(dirname_of_name)) else: item_path_source_rel = os.path.relpath(item_path_source) if len(item_path_source_rel) < len(item_path_source): self.fs_util.symlink(item_path_source_rel, name, opts.no_overwrite_mode) else: self.fs_util.symlink(item_path_source, name, opts.no_overwrite_mode) def _run_remote(self, opts, suite_name): """rose suite-run --remote=KEY=VALUE,...""" suite_dir_rel = self._suite_dir_rel(suite_name) r_opts = {} for item in opts.remote.split(","): key, val = item.split("=", 1) r_opts[key] = val uuid_file = os.path.join(suite_dir_rel, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("/" + r_opts["uuid"] + "\n", level=0) elif opts.new_mode: self.fs_util.delete(suite_dir_rel) if opts.run_mode == "run" or not os.path.exists(suite_dir_rel): self._run_init_dir(opts, suite_name, r_opts=r_opts) os.chdir(suite_dir_rel) for name in ["share", "share/cycle", "work"]: uuid_file = os.path.join(name, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event(name + "/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_work(opts, suite_name, name, r_opts=r_opts) if not opts.install_only_mode: uuid_file = os.path.join("log", r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("log/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_log(opts) self.fs_util.makedirs("log/suite") def _get_cmd_rsync(self, target, excludes=None, includes=None): """rsync relevant suite items to target.""" if excludes is None: excludes = [] if includes is None: includes = [] cmd = self.popen.get_cmd("rsync") for exclude in excludes + list(self.SYNC_EXCLUDES): cmd.append("--exclude=" + exclude) for include in includes: cmd.append("--include=" + include) cmd.append("./") cmd.append(target) return cmd def _suite_dir_rel(self, suite_name): """Return the relative path to the suite running directory.""" return self.suite_engine_proc.get_suite_dir_rel(suite_name)
class RoseBushService(object): """Serves the index page.""" NS = "rose" UTIL = "bush" TITLE = "Rose Bush" CYCLES_PER_PAGE = 100 JOBS_PER_PAGE = 15 JOBS_PER_PAGE_MAX = 300 SUITES_PER_PAGE = 100 VIEW_SIZE_MAX = 10 * 1024 * 1024 # 10MB def __init__(self, *args, **kwargs): self.exposed = True self.suite_engine_proc = SuiteEngineProcessor.get_processor() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) def urlise(text): pattern = '((https?):\/\/[^\s\(\)&\[\]\{\}]+)' replacement = '<a href="\g<1>">\g<1></a>' text = re.sub(pattern, replacement, text) return text template_env.filters['urlise'] = urlise self.template_env = template_env @cherrypy.expose def index(self, form=None): """Display a page to input user ID and suite ID.""" # TODO: some way to allow autocomplete of user field? data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, } if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template("index.html").render(**data) except Exception as exc: traceback.print_exc(exc) @cherrypy.expose def broadcast_states(self, user, suite, form=None): """List current broadcasts of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_states", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()), } data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_states"] = ( self.suite_engine_proc.get_suite_broadcast_states(user, suite)) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("broadcast-states.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def broadcast_events(self, user, suite, form=None): """List broadcasts history of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_events", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) } data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_events"] = ( self.suite_engine_proc.get_suite_broadcast_events(user, suite)) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("broadcast-events.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def cycles( self, user, suite, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List cycles of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "cycles-per-page"], self.CYCLES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "is_option_on": ( order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "order": order, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "cycles", "no_fuzzy_time": no_fuzzy_time, "states": {}, "per_page": per_page, "per_page_default": per_page_default, "page": page, } data["entries"], data["of_n_entries"] = ( self.suite_engine_proc.get_suite_cycles_summary( user, suite, order, per_page, (page - 1) * per_page)) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("cycles.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def jobs(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """List jobs of a running or completed suite. user -- A string containing a valid user ID suite -- A string containing a valid suite ID page -- The page number to display cycles -- Display only task jobs matching these cycles. A value in the list can be a cycle, the string "before|after CYCLE", or a glob to match cycles. tasks -- Display only jobs for task names matching a list of names. The list should be specified as a string which will be shlex.split by this method. Values can be a valid task name or a glob like pattern for matching valid task names. no_status -- Do not display jobs of tasks matching these statuses. The values in the list should be "active", "success" or "fail". order -- Order search in a predetermined way. A valid value is one of "time_desc", "time_asc", "cycle_desc_name_desc", "cycle_desc_name_asc", "cycle_asc_name_desc", "cycle_asc_name_asc", "name_asc_cycle_asc", "name_desc_cycle_asc", "name_asc_cycle_desc", "name_desc_cycle_desc", "time_submit_desc", "time_submit_asc", "time_run_desc", "time_run_asc", "time_run_exit_desc", "time_run_exit_asc", "duration_queue_desc", "duration_queue_asc", "duration_run_desc", "duration_run_asc", "duration_queue_run_desc", "duration_queue_run_asc" per_page -- Number of entries to display per page (defualt=32) no_fuzzy_time -- Don't display fuzzy time if this is True. form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir = self._get_user_suite_dir(user, suite) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "jobs-per-page"], self.JOBS_PER_PAGE)) per_page_max = int(conf.get_value( ["rose-bush", "jobs-per-page-max"], self.JOBS_PER_PAGE_MAX)) is_option_on = ( cycles or tasks or no_status is not None or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 no_statuses = no_status if no_status and not isinstance(no_status, list): no_statuses = [no_status] data = { "cycles": cycles, "host": self.host_name, "is_option_on": is_option_on, "logo": self.logo, "method": "jobs", "no_fuzzy_time": no_fuzzy_time, "no_statuses": no_statuses, "order": order, "page": page, "per_page": per_page, "per_page_default": per_page_default, "per_page_max": per_page_max, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "states": {}, "suite": suite, "tasks": tasks, "title": self.title, "user": user, } # TODO: add paths to other suite files if cycles: cycles = shlex.split(str(cycles)) if tasks: tasks = shlex.split(str(tasks)) data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) entries, of_n_entries = self.suite_engine_proc.get_suite_job_events( user, suite, cycles, tasks, no_statuses, order, per_page, (page - 1) * per_page) data["entries"] = entries data["of_n_entries"] = of_n_entries if per_page: data["n_pages"] = of_n_entries / per_page if of_n_entries % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("jobs.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) @cherrypy.expose def list(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, form=None): return self.jobs(user, suite, page, cycles, tasks, no_status, order, per_page, form) @cherrypy.expose def suites(self, user, names=None, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List (installed) suites of a user. user -- A string containing a valid user ID form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir_root = self._get_user_suite_dir_root(user) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "suites-per-page"], self.SUITES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "suites", "no_fuzzy_time": no_fuzzy_time, "user": user, "is_option_on": ( names and shlex.split(str(names)) != ["*"] or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "names": names, "page": page, "order": order, "per_page": per_page, "per_page_default": per_page_default, "entries": [], } name_globs = ["*"] if names: name_globs = shlex.split(str(names)) # Get entries try: items = os.listdir(user_suite_dir_root) except OSError: items = [] for item in items: if not any([fnmatch(item, glob_) for glob_ in name_globs]): continue user_suite_dir = os.path.join(user_suite_dir_root, item) suite_conf = os.path.join( user_suite_dir, self.suite_engine_proc.SUITE_CONF) job_logs_db = os.path.join( user_suite_dir, self.suite_engine_proc.JOB_LOGS_DB) if (not os.path.exists(job_logs_db) and not os.path.exists(suite_conf)): continue suite_db = os.path.join( user_suite_dir, self.suite_engine_proc.SUITE_DB) try: last_activity_time = strftime( "%Y-%m-%dT%H:%M:%S+0000", gmtime(os.stat(suite_db).st_mtime)) except OSError: last_activity_time = None data["entries"].append({ "name": item, "info": {}, "last_activity_time": last_activity_time}) if order == "name_asc": data["entries"].sort(key=lambda entry: entry["name"]) elif order == "name_desc": data["entries"].sort(key=lambda entry: entry["name"], reverse=True) elif order == "time_asc": data["entries"].sort(self._sort_summary_entries, reverse=True) else: # order == "time_desc" data["entries"].sort(self._sort_summary_entries) data["of_n_entries"] = len(data["entries"]) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 offset = (page - 1) * per_page data["entries"] = data["entries"][offset:offset + per_page] else: data["n_pages"] = 1 # Get suite info for each entry for entry in data["entries"]: user_suite_dir = os.path.join(user_suite_dir_root, entry["name"]) rose_suite_info = os.path.join(user_suite_dir, "rose-suite.info") try: info_root = rose.config.load(rose_suite_info) for key, node in info_root.value.items(): if (node.is_ignored() or not isinstance(node.value, str)): continue entry["info"][key] = node.value except (IOError, rose.config.ConfigSyntaxError): pass data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) template = self.template_env.get_template("suites.html") return template.render(**data) @cherrypy.expose def summary(self, user, form=None): return self.suites(user, form) @cherrypy.expose def view(self, user, suite, path, path_in_tar=None, mode=None): """View a text log file.""" f_name = self._get_user_suite_dir(user, suite, path) conf = ResourceLocator.default().get_conf() view_size_max = int(conf.get_value( ["rose-bush", "view-size-max"], self.VIEW_SIZE_MAX)) if path_in_tar: tar_f = tarfile.open(f_name, 'r:gz') try: tar_info = tar_f.getmember(path_in_tar) except KeyError: raise cherrypy.HTTPError(404) f_size = tar_info.size f = tar_f.extractfile(path_in_tar) if f.read(2) == "#!": mime = MIME_TEXT_PLAIN else: mime = mimetypes.guess_type( urllib.pathname2url(path_in_tar))[0] f.seek(0) if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): t = NamedTemporaryFile() f_bsize = os.fstatvfs(t.fileno()).f_bsize while True: bytes = f.read(f_bsize) if not bytes: break t.write(bytes) cherrypy.response.headers["Content-Type"] = mime try: return cherrypy.lib.static.serve_file(t.name, mime) finally: t.close() s = f.read() f.close() else: f_size = os.stat(f_name).st_size if open(f_name).read(2) == "#!": mime = MIME_TEXT_PLAIN else: mime = mimetypes.guess_type(urllib.pathname2url(f_name))[0] if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): cherrypy.response.headers["Content-Type"] = mime return cherrypy.lib.static.serve_file(f_name, mime) s = open(f_name).read() try: if mode in [None, "text"]: s = jinja2.escape(s) lines = [unicode(line) for line in s.splitlines()] except UnicodeDecodeError: return cherrypy.lib.static.serve_file(f_name, MIME_TEXT_PLAIN) name = path if path_in_tar: name = path_in_tar job_entry = None if name.startswith("log/job/"): names = self.suite_engine_proc.parse_job_log_rel_path(name) if len(names) == 4: cycle, task, submit_num, ext = names entries = self.suite_engine_proc.get_suite_job_events( user, suite, [cycle], [task], None, None, None, None)[0] for entry in entries: if entry["submit_num"] == int(submit_num): job_entry = entry break if fnmatch(os.path.basename(path), "rose*.conf"): file_content = "rose-conf" else: file_content = self.suite_engine_proc.is_conf(path) template = self.template_env.get_template("view.html") data = {} data.update(self._get_suite_logs_info(user, suite)) return template.render( rose_version=self.rose_version, script=cherrypy.request.script_name, method="view", time=strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()), logo=self.logo, title=self.title, host=self.host_name, user=user, suite=suite, path=path, path_in_tar=path_in_tar, f_name=f_name, mode=mode, file_content=file_content, lines=lines, entry=job_entry, **data) def _get_suite_logs_info(self, user, suite): data = {"info": {}, "files": {}} user_suite_dir = self._get_user_suite_dir(user, suite) # rose-suite.info info_name = os.path.join(user_suite_dir, "rose-suite.info") if os.path.isfile(info_name): try: info_root = rose.config.load(info_name) for key, node in info_root.value.items(): if node.is_ignored() or not isinstance(node.value, str): continue data["info"][key] = node.value except rose.config.ConfigSyntaxError as err: pass # rose-suite-run.conf, rose-suite-run.log, rose-suite-run.version data["files"]["rose"] = {} for key in ["conf", "log", "version"]: f_name = os.path.join(user_suite_dir, "log/rose-suite-run." + key) if os.path.isfile(f_name): s = os.stat(f_name) data["files"]["rose"]["log/rose-suite-run." + key] = { "path": "log/rose-suite-run." + key, "mtime": s.st_mtime, "size": s.st_size} # Other version files for f_name in glob(os.path.join(user_suite_dir, "log/*.version")): if os.path.basename(f_name).startswith("rose-"): continue name = os.path.join("log", os.path.basename(f_name)) s = os.stat(f_name) data["files"]["rose"]["other:" + name] = { "path": name, "mtime": s.st_mtime, "size": s.st_size} k, logs_info = self.suite_engine_proc.get_suite_logs_info(user, suite) data["files"][k] = logs_info return data @classmethod def _check_dir_access(cls, path): """Check directory is accessible. Raise 404 if path does not exist, or 403 if path not accessible. Return path on success. """ if not os.path.exists(path): raise cherrypy.HTTPError(404) if not os.access(path, os.R_OK): raise cherrypy.HTTPError(403) return path def _get_user_home(self, user): """Return, e.g. ~/cylc-run/ for a cylc suite. N.B. os.path.expanduser does not fail if ~user is invalid. """ try: return pwd.getpwnam(user).pw_dir except KeyError: raise cherrypy.HTTPError(404) def _get_user_suite_dir_root(self, user): """Return, e.g. ~user/cylc-run/ for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.suite_engine_proc.SUITE_DIR_REL_ROOT)) def _get_user_suite_dir(self, user, suite, *paths): """Return, e.g. ~user/cylc-run/suite/... for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.suite_engine_proc.SUITE_DIR_REL_ROOT, suite, *paths)) def _sort_summary_entries(self, a, b): return (cmp(b.get("last_activity_time"), a.get("last_activity_time")) or cmp(a["name"], b["name"]))
class SuiteRestarter(object): """Wrap "cylc restart".""" def __init__(self, event_handler=None): self.event_handler = event_handler self.popen = RosePopener(self.event_handler) self.fs_util = FileSystemUtil(self.event_handler) self.config_pm = ConfigProcessorsManager( self.event_handler, self.popen, self.fs_util) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=self.event_handler, popen=self.popen, fs_util=self.fs_util) def handle_event(self, *args, **kwargs): """Handle event.""" if callable(self.event_handler): self.event_handler(*args, **kwargs) def restart( self, suite_name=None, host=None, gcontrol_mode=None, args=None): """Restart a "cylc" suite.""" # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() if not suite_name: suite_name = get_suite_name(self.event_handler) suite_dir = self.suite_engine_proc.get_suite_dir(suite_name) if not os.path.exists(suite_dir): raise SuiteNotFoundError(suite_dir) # Ensure suite is not running hosts = [] if host: hosts.append(host) self.suite_engine_proc.check_suite_not_running(suite_name, hosts) # Determine suite host to restart suite if host: hosts = [host] else: hosts = [] val = ResourceLocator.default().get_conf().get_value( ["rose-suite-run", "hosts"], "localhost") known_hosts = self.host_selector.expand(val.split())[0] for known_host in known_hosts: if known_host not in hosts: hosts.append(known_host) if hosts == ["localhost"]: host = hosts[0] else: host = self.host_selector(hosts)[0][0] self.handle_event(SuiteHostSelectEvent(suite_name, "restart", host)) # Suite host environment run_conf_file_name = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") try: run_conf = ConfigLoader().load(run_conf_file_name) except (ConfigSyntaxError, IOError): environ = None else: run_conf_tree = ConfigTree() run_conf_tree.node = run_conf environ = self.config_pm(run_conf_tree, "env") # Restart the suite self.suite_engine_proc.run(suite_name, host, environ, "restart", args) # Write suite host name to host file host_file_name = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.host") open(host_file_name, "w").write(host + "\n") # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return
def run(self, app_runner, conf_tree, opts, args, uuid, work_files): """Suite housekeeping application. This application is designed to work under "rose task-run" in a cycling suite. """ suite_name = os.getenv("ROSE_SUITE_NAME") if not suite_name: return # Tar-gzip job logs on suite host # Prune job logs on remote hosts and suite host prune_remote_logs_cycles = self._get_conf( app_runner, conf_tree, "prune-remote-logs-at") prune_server_logs_cycles = self._get_conf( app_runner, conf_tree, "prune-server-logs-at") archive_logs_cycles = self._get_conf( app_runner, conf_tree, "archive-logs-at") if (prune_remote_logs_cycles or prune_server_logs_cycles or archive_logs_cycles): tmp_prune_remote_logs_cycles = [] for cycle in prune_remote_logs_cycles: if cycle not in archive_logs_cycles: tmp_prune_remote_logs_cycles.append(cycle) prune_remote_logs_cycles = tmp_prune_remote_logs_cycles tmp_prune_server_logs_cycles = [] for cycle in prune_server_logs_cycles: if cycle not in archive_logs_cycles: tmp_prune_server_logs_cycles.append(cycle) prune_server_logs_cycles = tmp_prune_server_logs_cycles if prune_remote_logs_cycles: app_runner.suite_engine_proc.job_logs_pull_remote( suite_name, prune_remote_logs_cycles, prune_remote_mode=True) if prune_server_logs_cycles: app_runner.suite_engine_proc.job_logs_remove_on_server( suite_name, prune_server_logs_cycles) if archive_logs_cycles: app_runner.suite_engine_proc.job_logs_archive( suite_name, archive_logs_cycles) # Prune other directories globs, cycle_set = self._get_prune_globs(app_runner, conf_tree) if not globs: return suite_engine_proc = app_runner.suite_engine_proc hosts = suite_engine_proc.get_suite_jobs_auths( suite_name, [(cycle, None) for cycle in cycle_set]) # A shuffle here should allow the load for doing "rm -rf" to be shared # between job hosts who share a file system. shuffle(hosts) suite_dir_rel = suite_engine_proc.get_suite_dir_rel(suite_name) form_dict = {"d": suite_dir_rel, "g": " ".join(globs)} sh_cmd_head = r"set -e; cd %(d)s; " % form_dict sh_cmd = ( r"set +e; ls -d %(g)s; " + r"set -e; rm -fr %(g)s") % form_dict cwd = os.getcwd() host_selector = HostSelector( app_runner.event_handler, app_runner.popen) for host in hosts + [host_selector.get_local_host()]: sdir = None try: if host_selector.is_local_host(host): sdir = suite_engine_proc.get_suite_dir(suite_name) app_runner.fs_util.chdir(sdir) out = app_runner.popen.run_ok( "bash", "-O", "extglob", "-c", sh_cmd)[0] else: cmd = app_runner.popen.get_cmd( "ssh", host, "bash -O extglob -c '" + sh_cmd_head + sh_cmd + "'") out = app_runner.popen.run_ok(*cmd)[0] except RosePopenError as exc: app_runner.handle_event(exc) else: if sdir is None: event = FileSystemEvent(FileSystemEvent.CHDIR, host + ":" + suite_dir_rel) app_runner.handle_event(event) for line in sorted(out.splitlines()): if not host_selector.is_local_host(host): line = host + ":" + line event = FileSystemEvent(FileSystemEvent.DELETE, line) app_runner.handle_event(event) finally: if sdir: app_runner.fs_util.chdir(cwd) return
class RoseBushService(object): """Serves the index page.""" NS = "rose" UTIL = "bush" TITLE = "Rose Bush" CYCLES_PER_PAGE = 100 JOBS_PER_PAGE = 15 JOBS_PER_PAGE_MAX = 300 SUITES_PER_PAGE = 100 VIEW_SIZE_MAX = 10 * 1024 * 1024 # 10MB def __init__(self, *args, **kwargs): self.exposed = True self.suite_engine_proc = SuiteEngineProcessor.get_processor() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment(loader=jinja2.FileSystemLoader( ResourceLocator.default().get_util_home( "lib", "html", "template", "rose-bush"))) self.template_env = template_env @cherrypy.expose def index(self, form=None): """Display a page to input user ID and suite ID.""" # TODO: some way to allow autocomplete of user field? data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, } if form == "json": return simplejson.dumps(data) try: return self.template_env.get_template("index.html").render(**data) except Exception as exc: traceback.print_exc(exc) @cherrypy.expose def broadcast_states(self, user, suite, form=None): """List current broadcasts of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_states", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()), } data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_states"] = ( self.suite_engine_proc.get_suite_broadcast_states(user, suite)) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("broadcast-states.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def broadcast_events(self, user, suite, form=None): """List broadcasts history of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_events", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) } data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_events"] = ( self.suite_engine_proc.get_suite_broadcast_events(user, suite)) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("broadcast-events.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def cycles( self, user, suite, page=1, order=None, per_page=None, form=None): """List cycles of a running or completed suite.""" user_suite_dir = self._get_user_suite_dir(user, suite) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "cycles-per-page"], self.CYCLES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "is_option_on": ( order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "order": order, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "cycles", "states": {}, "per_page": per_page, "per_page_default": per_page_default, "page": page, } data["entries"], data["of_n_entries"] = ( self.suite_engine_proc.get_suite_cycles_summary( user, suite, order, per_page, (page - 1) * per_page)) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("cycles.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) return simplejson.dumps(data) @cherrypy.expose def jobs(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """List jobs of a running or completed suite. user -- A string containing a valid user ID suite -- A string containing a valid suite ID page -- The page number to display cycles -- Display only task jobs matching these cycles. A value in the list can be a cycle, the string "before|after CYCLE", or a glob to match cycles. tasks -- Display only jobs for task names matching a list of names. The list should be specified as a string which will be shlex.split by this method. Values can be a valid task name or a glob like pattern for matching valid task names. no_status -- Do not display jobs of tasks matching these statuses. The values in the list should be "active", "success" or "fail". order -- Order search in a predetermined way. A valid value is one of "time_desc", "time_asc", "cycle_desc_name_desc", "cycle_desc_name_asc", "cycle_asc_name_desc", "cycle_asc_name_asc", "name_asc_cycle_asc", "name_desc_cycle_asc", "name_asc_cycle_desc", "name_desc_cycle_desc", "time_submit_desc", "time_submit_asc", "time_run_desc", "time_run_asc", "time_run_exit_desc", "time_run_exit_asc", "duration_queue_desc", "duration_queue_asc", "duration_run_desc", "duration_run_asc", "duration_queue_run_desc", "duration_queue_run_asc" per_page -- Number of entries to display per page (defualt=32) no_fuzzy_time -- Don't display fuzzy time if this is True. form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir = self._get_user_suite_dir(user, suite) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "jobs-per-page"], self.JOBS_PER_PAGE)) per_page_max = int(conf.get_value( ["rose-bush", "jobs-per-page-max"], self.JOBS_PER_PAGE_MAX)) is_option_on = ( cycles or tasks or no_status is not None or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 no_statuses = no_status if no_status and not isinstance(no_status, list): no_statuses = [no_status] data = { "cycles": cycles, "host": self.host_name, "is_option_on": is_option_on, "logo": self.logo, "method": "jobs", "no_fuzzy_time": no_fuzzy_time, "no_statuses": no_statuses, "order": order, "page": page, "per_page": per_page, "per_page_default": per_page_default, "per_page_max": per_page_max, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "states": {}, "suite": suite, "tasks": tasks, "title": self.title, "user": user, } # TODO: add paths to other suite files if cycles: cycles = shlex.split(str(cycles)) if tasks: tasks = shlex.split(str(tasks)) data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.suite_engine_proc.get_suite_state_summary(user, suite)) entries, of_n_entries = self.suite_engine_proc.get_suite_job_events( user, suite, cycles, tasks, no_statuses, order, per_page, (page - 1) * per_page) data["entries"] = entries data["of_n_entries"] = of_n_entries if per_page: data["n_pages"] = of_n_entries / per_page if of_n_entries % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) try: template = self.template_env.get_template("jobs.html") return template.render(**data) except Exception as exc: traceback.print_exc(exc) @cherrypy.expose def list(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, form=None): return self.jobs(user, suite, page, cycles, tasks, no_status, order, per_page, form) @cherrypy.expose def suites(self, user, names=None, page=1, order=None, per_page=None, form=None): """List (installed) suites of a user. user -- A string containing a valid user ID form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir_root = self._get_user_suite_dir_root(user) conf = ResourceLocator.default().get_conf() per_page_default = int(conf.get_value( ["rose-bush", "suites-per-page"], self.SUITES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "suites", "user": user, "is_option_on": ( names and shlex.split(str(names)) != ["*"] or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default ), "names": names, "page": page, "order": order, "per_page": per_page, "per_page_default": per_page_default, "entries": [], } name_globs = ["*"] if names: name_globs = shlex.split(str(names)) # Get entries try: items = os.listdir(user_suite_dir_root) except OSError: items = [] for item in items: if not any([fnmatch(item, glob_) for glob_ in name_globs]): continue user_suite_dir = os.path.join(user_suite_dir_root, item) suite_conf = os.path.join( user_suite_dir, self.suite_engine_proc.SUITE_CONF) job_logs_db = os.path.join( user_suite_dir, self.suite_engine_proc.JOB_LOGS_DB) if (not os.path.exists(job_logs_db) and not os.path.exists(suite_conf)): continue suite_db = os.path.join( user_suite_dir, self.suite_engine_proc.SUITE_DB) try: last_activity_time = strftime( "%Y-%m-%dT%H:%M:%S+0000", gmtime(os.stat(suite_db).st_mtime)) except OSError: last_activity_time = None data["entries"].append({ "name": item, "info": {}, "last_activity_time": last_activity_time}) if order == "name_asc": data["entries"].sort(key=lambda entry: entry["name"]) elif order == "name_desc": data["entries"].sort(key=lambda entry: entry["name"], reverse=True) elif order == "time_asc": data["entries"].sort(self._sort_summary_entries, reverse=True) else: # order == "time_desc" data["entries"].sort(self._sort_summary_entries) data["of_n_entries"] = len(data["entries"]) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 offset = (page - 1) * per_page data["entries"] = data["entries"][offset:offset + per_page] else: data["n_pages"] = 1 # Get suite info for each entry for entry in data["entries"]: user_suite_dir = os.path.join(user_suite_dir_root, entry["name"]) rose_suite_info = os.path.join(user_suite_dir, "rose-suite.info") try: info_root = rose.config.load(rose_suite_info) for key, node in info_root.value.items(): if (node.is_ignored() or not isinstance(node.value, str)): continue entry["info"][key] = node.value except (IOError, rose.config.ConfigSyntaxError): pass data["time"] = strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()) if form == "json": return simplejson.dumps(data) template = self.template_env.get_template("suites.html") return template.render(**data) @cherrypy.expose def summary(self, user, form=None): return self.suites(user, form) @cherrypy.expose def view(self, user, suite, path, path_in_tar=None, mode=None): """View a text log file.""" f_name = self._get_user_suite_dir(user, suite, path) conf = ResourceLocator.default().get_conf() view_size_max = int(conf.get_value( ["rose-bush", "view-size-max"], self.VIEW_SIZE_MAX)) if path_in_tar: tar_f = tarfile.open(f_name, 'r:gz') try: tar_info = tar_f.getmember(path_in_tar) except KeyError: raise cherrypy.HTTPError(404) f_size = tar_info.size f = tar_f.extractfile(path_in_tar) if f.read(2) == "#!": mime = MIME_TEXT_PLAIN else: mime = mimetypes.guess_type( urllib.pathname2url(path_in_tar))[0] f.seek(0) if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): t = NamedTemporaryFile() f_bsize = os.fstatvfs(t.fileno()).f_bsize while True: bytes = f.read(f_bsize) if not bytes: break t.write(bytes) cherrypy.response.headers["Content-Type"] = mime try: return cherrypy.lib.static.serve_file(t.name, mime) finally: t.close() s = f.read() f.close() else: f_size = os.stat(f_name).st_size if open(f_name).read(2) == "#!": mime = MIME_TEXT_PLAIN else: mime = mimetypes.guess_type(urllib.pathname2url(f_name))[0] if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): cherrypy.response.headers["Content-Type"] = mime return cherrypy.lib.static.serve_file(f_name, mime) s = open(f_name).read() if mode == "text": s = jinja2.escape(s) try: lines = [unicode(line) for line in s.splitlines()] except UnicodeDecodeError: return cherrypy.lib.static.serve_file(f_name, MIME_TEXT_PLAIN) name = path if path_in_tar: name = path_in_tar job_entry = None if name.startswith("log/job/"): names = self.suite_engine_proc.parse_job_log_rel_path(name) if len(names) == 4: cycle, task, submit_num, ext = names entries = self.suite_engine_proc.get_suite_job_events( user, suite, [cycle], [task], None, None, None, None)[0] for entry in entries: if entry["submit_num"] == int(submit_num): job_entry = entry break if fnmatch(os.path.basename(path), "rose*.conf"): file_content = "rose-conf" else: file_content = self.suite_engine_proc.is_conf(path) template = self.template_env.get_template("view.html") data = {} data.update(self._get_suite_logs_info(user, suite)) return template.render( rose_version=self.rose_version, script=cherrypy.request.script_name, method="view", time=strftime("%Y-%m-%dT%H:%M:%S+0000", gmtime()), logo=self.logo, title=self.title, host=self.host_name, user=user, suite=suite, path=path, path_in_tar=path_in_tar, f_name=f_name, mode=mode, file_content=file_content, lines=lines, entry=job_entry, **data) def _get_suite_logs_info(self, user, suite): data = {"info": {}, "files": {}} user_suite_dir = self._get_user_suite_dir(user, suite) # rose-suite.info info_name = os.path.join(user_suite_dir, "rose-suite.info") if os.path.isfile(info_name): try: info_root = rose.config.load(info_name) for key, node in info_root.value.items(): if node.is_ignored() or not isinstance(node.value, str): continue data["info"][key] = node.value except rose.config.ConfigSyntaxError as err: pass # rose-suite-run.conf, rose-suite-run.log, rose-suite-run.version data["files"]["rose"] = {} for key in ["conf", "log", "version"]: f_name = os.path.join(user_suite_dir, "log/rose-suite-run." + key) if os.path.isfile(f_name): s = os.stat(f_name) data["files"]["rose"]["log/rose-suite-run." + key] = { "path": "log/rose-suite-run." + key, "mtime": s.st_mtime, "size": s.st_size} # Other version files for f_name in glob(os.path.join(user_suite_dir, "log/*.version")): if os.path.basename(f_name).startswith("rose-"): continue name = os.path.join("log", os.path.basename(f_name)) s = os.stat(f_name) data["files"]["rose"]["other:" + name] = { "path": name, "mtime": s.st_mtime, "size": s.st_size} k, logs_info = self.suite_engine_proc.get_suite_logs_info(user, suite) data["files"][k] = logs_info return data @classmethod def _check_dir_access(cls, path): """Check directory is accessible. Raise 404 if path does not exist, or 403 if path not accessible. Return path on success. """ if not os.path.exists(path): raise cherrypy.HTTPError(404) if not os.access(path, os.R_OK): raise cherrypy.HTTPError(403) return path def _get_user_home(self, user): """Return, e.g. ~/cylc-run/ for a cylc suite. N.B. os.path.expanduser does not fail if ~user is invalid. """ try: return pwd.getpwnam(user).pw_dir except KeyError: raise cherrypy.HTTPError(404) def _get_user_suite_dir_root(self, user): """Return, e.g. ~user/cylc-run/ for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.suite_engine_proc.SUITE_DIR_REL_ROOT)) def _get_user_suite_dir(self, user, suite, *paths): """Return, e.g. ~user/cylc-run/suite/... for a cylc suite.""" return self._check_dir_access(os.path.join( self._get_user_home(user), self.suite_engine_proc.SUITE_DIR_REL_ROOT, suite, *paths)) def _sort_summary_entries(self, a, b): return (cmp(b.get("last_activity_time"), a.get("last_activity_time")) or cmp(a["name"], b["name"]))
class RoseBushService(object): """Rose Bush Service.""" NS = "rose" UTIL = "bush" TITLE = "Rose Bush" CYCLES_PER_PAGE = 100 JOBS_PER_PAGE = 15 JOBS_PER_PAGE_MAX = 300 MIME_TEXT_PLAIN = "text/plain" REC_URL = re.compile(r"((https?):\/\/[^\s\(\)&\[\]\{\}]+)") SEARCH_MODE_REGEX = "REGEX" SEARCH_MODE_TEXT = "TEXT" SUITES_PER_PAGE = 100 VIEW_SIZE_MAX = 10 * 1024 * 1024 # 10MB def __init__(self, *args, **kwargs): self.exposed = True self.bush_dao = RoseBushDAO() rose_conf = ResourceLocator.default().get_conf() self.logo = rose_conf.get_value(["rose-bush", "logo"]) self.title = rose_conf.get_value(["rose-bush", "title"], self.TITLE) self.host_name = rose_conf.get_value(["rose-bush", "host"]) if self.host_name is None: self.host_name = HostSelector().get_local_host() if self.host_name and "." in self.host_name: self.host_name = self.host_name.split(".", 1)[0] self.rose_version = ResourceLocator.default().get_version() template_env = jinja2.Environment( loader=jinja2.FileSystemLoader(ResourceLocator.default( ).get_util_home("lib", "html", "template", "rose-bush"))) template_env.filters['urlise'] = self.url2hyperlink self.template_env = template_env @classmethod def url2hyperlink(cls, text): """Turn http or https link into a hyperlink.""" return cls.REC_URL.sub(r'<a href="\g<1>">\g<1></a>', text) @cherrypy.expose def index(self, form=None): """Display a page to input user ID and suite ID.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, } if form == "json": return json.dumps(data) try: return self.template_env.get_template("index.html").render(**data) except jinja2.TemplateError: traceback.print_exc() @cherrypy.expose def broadcast_states(self, user, suite, form=None): """List current broadcasts of a running or completed suite.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_states", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), } data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = (self.get_last_activity_time( user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_states"] = (self.bush_dao.get_suite_broadcast_states( user, suite)) if form == "json": return json.dumps(data) try: return self.template_env.get_template( "broadcast-states.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return json.dumps(data) @cherrypy.expose def broadcast_events(self, user, suite, form=None): """List broadcasts history of a running or completed suite.""" data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "broadcast_events", "states": {}, "time": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) } data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data.update(self._get_suite_logs_info(user, suite)) data["broadcast_events"] = (self.bush_dao.get_suite_broadcast_events( user, suite)) if form == "json": return json.dumps(data) try: return self.template_env.get_template( "broadcast-events.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return json.dumps(data) @cherrypy.expose def cycles(self, user, suite, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List cycles of a running or completed suite.""" conf = ResourceLocator.default().get_conf() per_page_default = int( conf.get_value(["rose-bush", "cycles-per-page"], self.CYCLES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "user": user, "suite": suite, "is_option_on": (order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default), "order": order, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "cycles", "no_fuzzy_time": no_fuzzy_time, "states": {}, "per_page": per_page, "per_page_default": per_page_default, "page": page, "task_status_groups": self.bush_dao.TASK_STATUS_GROUPS, } data["entries"], data["of_n_entries"] = ( self.bush_dao.get_suite_cycles_summary(user, suite, order, per_page, (page - 1) * per_page)) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = (self.get_last_activity_time( user, suite)) data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return json.dumps(data) try: return self.template_env.get_template("cycles.html").render(**data) except jinja2.TemplateError: traceback.print_exc() return json.dumps(data) @cherrypy.expose def taskjobs(self, user, suite, page=1, cycles=None, tasks=None, task_status=None, job_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """List task jobs. user -- A string containing a valid user ID suite -- A string containing a valid suite ID page -- The page number to display cycles -- Display only task jobs matching these cycles. A value in the list can be a cycle, the string "before|after CYCLE", or a glob to match cycles. tasks -- Display only jobs for task names matching a list of names. The list should be specified as a string which will be shlex.split by this method. Values can be a valid task name or a glob like pattern for matching valid task names. task_status -- Select by task statuses. job_status -- Select by job status. See RoseBushDAO.JOB_STATUS_COMBOS for detail. order -- Order search in a predetermined way. A valid value is one of "time_desc", "time_asc", "cycle_desc_name_desc", "cycle_desc_name_asc", "cycle_asc_name_desc", "cycle_asc_name_asc", "name_asc_cycle_asc", "name_desc_cycle_asc", "name_asc_cycle_desc", "name_desc_cycle_desc", "time_submit_desc", "time_submit_asc", "time_run_desc", "time_run_asc", "time_run_exit_desc", "time_run_exit_asc", "duration_queue_desc", "duration_queue_asc", "duration_run_desc", "duration_run_asc", "duration_queue_run_desc", "duration_queue_run_asc" per_page -- Number of entries to display per page (defualt=32) no_fuzzy_time -- Don't display fuzzy time if this is True. form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ conf = ResourceLocator.default().get_conf() per_page_default = int( conf.get_value(["rose-bush", "jobs-per-page"], self.JOBS_PER_PAGE)) per_page_max = int( conf.get_value(["rose-bush", "jobs-per-page-max"], self.JOBS_PER_PAGE_MAX)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default is_option_on = (cycles or tasks or task_status or job_status or order is not None and order != "time_desc" or per_page != per_page_default) if page and per_page: page = int(page) else: page = 1 task_statuses = ([[item, ""] for item in self.bush_dao.TASK_STATUSES]) if task_status: if not isinstance(task_status, list): task_status = [task_status] for item in task_statuses: if not task_status or item[0] in task_status: item[1] = "1" all_task_statuses = all([status[1] == "1" for status in task_statuses]) if all_task_statuses: task_status = [] data = { "cycles": cycles, "host": self.host_name, "is_option_on": is_option_on, "logo": self.logo, "method": "taskjobs", "no_fuzzy_time": no_fuzzy_time, "all_task_statuses": all_task_statuses, "task_statuses": task_statuses, "job_status": job_status, "order": order, "page": page, "per_page": per_page, "per_page_default": per_page_default, "per_page_max": per_page_max, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "states": {}, "suite": suite, "tasks": tasks, "task_status_groups": self.bush_dao.TASK_STATUS_GROUPS, "title": self.title, "user": user, } if cycles: cycles = shlex.split(str(cycles)) if tasks: tasks = shlex.split(str(tasks)) data.update(self._get_suite_logs_info(user, suite)) data["states"].update( self.bush_dao.get_suite_state_summary(user, suite)) data["states"]["last_activity_time"] = (self.get_last_activity_time( user, suite)) entries, of_n_entries = self.bush_dao.get_suite_job_entries( user, suite, cycles, tasks, task_status, job_status, order, per_page, (page - 1) * per_page) data["entries"] = entries data["of_n_entries"] = of_n_entries if per_page: data["n_pages"] = of_n_entries / per_page if of_n_entries % per_page != 0: data["n_pages"] += 1 else: data["n_pages"] = 1 data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return json.dumps(data) try: return self.template_env.get_template("taskjobs.html").render( **data) except jinja2.TemplateError: traceback.print_exc() @cherrypy.expose def jobs(self, user, suite, page=1, cycles=None, tasks=None, no_status=None, order=None, per_page=None, no_fuzzy_time="0", form=None): """(Deprecated) Redirect to self.taskjobs. Convert "no_status" to "task_status" argument of self.taskjobs. """ task_status = None if no_status: task_status = [] if not isinstance(no_status, list): no_status = [no_status] for key, values in self.bush_dao.TASK_STATUS_GROUPS.items(): if key not in no_status: task_status += values return self.taskjobs(user, suite, page, cycles, tasks, task_status, None, order, per_page, no_fuzzy_time, form) @cherrypy.expose def suites(self, user, names=None, page=1, order=None, per_page=None, no_fuzzy_time="0", form=None): """List (installed) suites of a user. user -- A string containing a valid user ID form -- Specify return format. If None, display HTML page. If "json", return a JSON data structure. """ user_suite_dir_root = self._get_user_suite_dir_root(user) conf = ResourceLocator.default().get_conf() per_page_default = int( conf.get_value(["rose-bush", "suites-per-page"], self.SUITES_PER_PAGE)) if not isinstance(per_page, int): if per_page: per_page = int(per_page) else: per_page = per_page_default if page and per_page: page = int(page) else: page = 1 data = { "logo": self.logo, "title": self.title, "host": self.host_name, "rose_version": self.rose_version, "script": cherrypy.request.script_name, "method": "suites", "no_fuzzy_time": no_fuzzy_time, "user": user, "is_option_on": (names and shlex.split(str(names)) != ["*"] or order is not None and order != "time_desc" or per_page is not None and per_page != per_page_default), "names": names, "page": page, "order": order, "per_page": per_page, "per_page_default": per_page_default, "entries": [], } name_globs = ["*"] if names: name_globs = shlex.split(str(names)) # Get entries sub_names = [ ".service", "log", "share", "work", self.bush_dao.SUITE_CONF ] for dirpath, dnames, fnames in os.walk(user_suite_dir_root, followlinks=True): if dirpath != user_suite_dir_root and (any( name in dnames or name in fnames for name in sub_names)): dnames[:] = [] else: continue item = os.path.relpath(dirpath, user_suite_dir_root) if not any(fnmatch(item, glob_) for glob_ in name_globs): continue try: data["entries"].append({ "name": item, "info": {}, "last_activity_time": (self.get_last_activity_time(user, item)) }) except OSError: continue if order == "name_asc": data["entries"].sort(key=lambda entry: entry["name"]) elif order == "name_desc": data["entries"].sort(key=lambda entry: entry["name"], reverse=True) elif order == "time_asc": data["entries"].sort(self._sort_summary_entries, reverse=True) else: # order == "time_desc" data["entries"].sort(self._sort_summary_entries) data["of_n_entries"] = len(data["entries"]) if per_page: data["n_pages"] = data["of_n_entries"] / per_page if data["of_n_entries"] % per_page != 0: data["n_pages"] += 1 offset = (page - 1) * per_page data["entries"] = data["entries"][offset:offset + per_page] else: data["n_pages"] = 1 # Get suite info for each entry for entry in data["entries"]: user_suite_dir = os.path.join(user_suite_dir_root, entry["name"]) rose_suite_info = os.path.join(user_suite_dir, "rose-suite.info") try: info_root = rose.config.load(rose_suite_info) for key, node in info_root.value.items(): if (node.is_ignored() or not isinstance(node.value, str)): continue entry["info"][key] = node.value except (IOError, rose.config.ConfigSyntaxError): pass data["time"] = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) if form == "json": return json.dumps(data) template = self.template_env.get_template("suites.html") return template.render(**data) def get_file(self, user, suite, path, path_in_tar=None, mode=None): """Returns file information / content or a cherrypy response.""" f_name = self._get_user_suite_dir(user, suite, path) conf = ResourceLocator.default().get_conf() view_size_max = int( conf.get_value(["rose-bush", "view-size-max"], self.VIEW_SIZE_MAX)) if path_in_tar: tar_f = tarfile.open(f_name, "r:gz") try: tar_info = tar_f.getmember(path_in_tar) except KeyError: raise cherrypy.HTTPError(404) f_size = tar_info.size handle = tar_f.extractfile(path_in_tar) if handle.read(2) == "#!": mime = self.MIME_TEXT_PLAIN else: mime = mimetypes.guess_type( urllib.pathname2url(path_in_tar))[0] handle.seek(0) if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): temp_f = NamedTemporaryFile() f_bsize = os.fstatvfs(temp_f.fileno()).f_bsize while True: bytes_ = handle.read(f_bsize) if not bytes_: break temp_f.write(bytes_) cherrypy.response.headers["Content-Type"] = mime try: return cherrypy.lib.static.serve_file(temp_f.name, mime) finally: temp_f.close() text = handle.read() else: f_size = os.stat(f_name).st_size if open(f_name).read(2) == "#!": mime = self.MIME_TEXT_PLAIN else: mime = mimetypes.guess_type(urllib.pathname2url(f_name))[0] if not mime: mime = self.MIME_TEXT_PLAIN if (mode == "download" or f_size > view_size_max or mime and (not mime.startswith("text/") or mime.endswith("html"))): cherrypy.response.headers["Content-Type"] = mime return cherrypy.lib.static.serve_file(f_name, mime) text = open(f_name).read() try: if mode in [None, "text"]: text = jinja2.escape(text) lines = [unicode(line) for line in text.splitlines()] except UnicodeDecodeError: if path_in_tar: handle.seek(0) # file closed by cherrypy return cherrypy.lib.static.serve_fileobj( handle, self.MIME_TEXT_PLAIN) else: return cherrypy.lib.static.serve_file(f_name, self.MIME_TEXT_PLAIN) else: if path_in_tar: handle.close() name = path if path_in_tar: name = "log/" + path_in_tar job_entry = None if name.startswith("log/job"): names = self.bush_dao.parse_job_log_rel_path(name) if len(names) == 4: cycle, task, submit_num, _ = names entries = self.bush_dao.get_suite_job_entries( user, suite, [cycle], [task], None, None, None, None, None)[0] for entry in entries: if entry["submit_num"] == int(submit_num): job_entry = entry break if fnmatch(os.path.basename(path), "rose*.conf"): file_content = "rose-conf" else: file_content = self.bush_dao.is_conf(path) return lines, job_entry, file_content, f_name def get_last_activity_time(self, user, suite): """Returns last activity time for a suite based on database stat""" for name in [os.path.join("log", "db"), "cylc-suite.db"]: fname = os.path.join(self._get_user_suite_dir(user, suite), name) try: return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(os.stat(fname).st_mtime)) except OSError: continue @cherrypy.expose def viewsearch(self, user, suite, path=None, path_in_tar=None, mode=None, search_string=None, search_mode=None): """Search a text log file.""" # get file or serve raw data file_output = self.get_file(user, suite, path, path_in_tar=path_in_tar, mode=mode) if isinstance(file_output, tuple): lines, _, file_content, _ = self.get_file(user, suite, path, path_in_tar=path_in_tar, mode=mode) else: return file_output template = self.template_env.get_template("view-search.html") if search_string: results = [] line_numbers = [] # perform search for i, line in enumerate(lines): if search_mode is None or search_mode == self.SEARCH_MODE_TEXT: match = line.find(search_string) if match == -1: continue start = match end = start + len(search_string) elif search_mode == self.SEARCH_MODE_REGEX: match = re.search(search_string, line) if not match: continue start, end = match.span() else: # ERROR: un-reccognised search_mode break # if line matches search string include in results results.append([line[:start], line[start:end], line[end:]]) if mode in [None, "text"]: line_numbers.append(i + 1) # line numbers start from 1 lines = results else: # no search is being performed, client is requesting the whole # page if mode in [None, "text"]: line_numbers = range(1, len(lines) + 1) else: line_numbers = [] lines = [[line] for line in lines] return template.render(lines=lines, line_numbers=line_numbers, file_content=file_content) @cherrypy.expose def view(self, user, suite, path, path_in_tar=None, mode=None, no_fuzzy_time="0"): """View a text log file.""" # get file or serve raw data file_output = self.get_file(user, suite, path, path_in_tar=path_in_tar, mode=mode) if isinstance(file_output, tuple): lines, job_entry, file_content, f_name = self.get_file( user, suite, path, path_in_tar=path_in_tar, mode=mode) else: return file_output template = self.template_env.get_template("view.html") data = {} data.update(self._get_suite_logs_info(user, suite)) return template.render( rose_version=self.rose_version, script=cherrypy.request.script_name, method="view", time=strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), logo=self.logo, title=self.title, host=self.host_name, user=user, suite=suite, path=path, path_in_tar=path_in_tar, f_name=f_name, mode=mode, no_fuzzy_time=no_fuzzy_time, file_content=file_content, lines=lines, entry=job_entry, task_status_groups=self.bush_dao.TASK_STATUS_GROUPS, **data) def _get_suite_logs_info(self, user, suite): """Return a dict with suite logs and Rosie suite info.""" data = {"info": {}, "files": {}} user_suite_dir = self._get_user_suite_dir(user, suite) # rose-suite.info info_name = os.path.join(user_suite_dir, "rose-suite.info") if os.path.isfile(info_name): try: info_root = rose.config.load(info_name) for key, node in info_root.value.items(): if node.is_ignored() or not isinstance(node.value, str): continue data["info"][key] = node.value except rose.config.ConfigSyntaxError: pass # rose-suite-run.conf, rose-suite-run.log, rose-suite-run.version data["files"]["rose"] = {} for key in ["conf", "log", "version"]: f_name = os.path.join(user_suite_dir, "log/rose-suite-run." + key) if os.path.isfile(f_name): stat = os.stat(f_name) data["files"]["rose"]["log/rose-suite-run." + key] = { "path": "log/rose-suite-run." + key, "mtime": stat.st_mtime, "size": stat.st_size } # Other recognised formats for key in ["html", "txt", "version"]: for f_name in glob(os.path.join(user_suite_dir, "log/*." + key)): if os.path.basename(f_name).startswith("rose-"): continue name = os.path.join("log", os.path.basename(f_name)) stat = os.stat(f_name) data["files"]["rose"]["other:" + name] = { "path": name, "mtime": stat.st_mtime, "size": stat.st_size } k, logs_info = self.bush_dao.get_suite_logs_info(user, suite) data["files"][k] = logs_info return data @classmethod def _check_dir_access(cls, path): """Check directory is accessible. Raises: - cherrypy.HTTPError(404) if path does not exist - cherrypy.HTTPError(403) if path not accessible Return path on success. """ if not os.path.exists(path): raise cherrypy.HTTPError(404) if not os.access(path, os.R_OK): raise cherrypy.HTTPError(403) return path @staticmethod def _get_user_home(user): """Return, e.g. ~/cylc-run/ for a cylc suite. N.B. os.path.expanduser does not fail if ~user is invalid. Raises: cherrypy.HTTPError(404) """ try: return pwd.getpwnam(user).pw_dir except KeyError: raise cherrypy.HTTPError(404) def _get_user_suite_dir_root(self, user): """Return, e.g. ~user/cylc-run/ for a cylc suite.""" return self._check_dir_access( os.path.join(self._get_user_home(user), self.bush_dao.SUITE_DIR_REL_ROOT)) @staticmethod def _check_string_for_path(string): """Raise HTTP 403 error if the provided string contain path chars. Examples: >>> RoseBushService._check_string_for_path( ... os.path.join('foo', 'bar')) Traceback (most recent call last): ... HTTPError: (403, None) Raises: cherrypy.HTTPError(403) """ if os.path.split(string)[0] != '': raise cherrypy.HTTPError(403) @staticmethod def _check_path_normalised(path): """Raise HTTP 403 error if path is not normalised. Examples: >>> RoseBushService._check_path_normalised('foo//bar') Traceback (most recent call last): ... HTTPError: (403, None) >>> RoseBushService._check_path_normalised('foo/bar/') Traceback (most recent call last): ... HTTPError: (403, None) >>> RoseBushService._check_path_normalised('foo/./bar') Traceback (most recent call last): ... HTTPError: (403, None) >>> RoseBushService._check_path_normalised('foo/../bar') Traceback (most recent call last): ... HTTPError: (403, None) >>> RoseBushService._check_path_normalised('../foo') Traceback (most recent call last): ... HTTPError: (403, None) Raises: cherrypy.HTTPError(403) """ path = os.path.join('foo', path) # Enable checking of ../foo paths. if os.path.normpath(path) != path: raise cherrypy.HTTPError(403) def _get_user_suite_dir(self, user, suite, *paths): """Return, e.g. ~user/cylc-run/suite/... for a cylc suite. Raises: - cherrypy.HTTPError(404) if path does not exist - cherrypy.HTTPError(403) if path not accessible """ self._check_string_for_path(user) self._check_path_normalised(suite) for path in paths: self._check_path_normalised(path) suite_dir = os.path.join(self._get_user_home(user), self.bush_dao.SUITE_DIR_REL_ROOT, suite) if not paths: return self._check_dir_access(suite_dir) path = os.path.join(suite_dir, *paths) if not path.startswith(suite_dir): # Raise HTTP 403 if path lies outside of the suite directory. Note: # >>> os.path.join('/foo', '/bar') # '/bar' raise cherrypy.HTTPError(403) return self._check_dir_access(path) @staticmethod def _sort_summary_entries(suite1, suite2): """Sort suites by last_activity_time.""" return (cmp(suite2.get("last_activity_time"), suite1.get("last_activity_time")) or cmp(suite1["name"], suite2["name"]))
class SuiteRunner(Runner): """Invoke a Rose suite.""" SLEEP_PIPE = 0.05 NAME = "suite" OPTIONS = [ "conf_dir", "defines", "defines_suite", "gcontrol_mode", "host", "install_only_mode", "local_install_only_mode", "log_archive_mode", "log_keep", "log_name", "name", "new_mode", "no_overwrite_mode", "opt_conf_keys", "reload_mode", "remote", "restart_mode", "run_mode", "strict_mode", "validate_suite_only"] # Lists of rsync (always) exclude globs SYNC_EXCLUDES = ( "/.*", "/cylc-suite.db", "/log", "/log.*", "/state", "/share", "/work", ) def __init__(self, *args, **kwargs): Runner.__init__(self, *args, **kwargs) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_run_cleaner = SuiteRunCleaner( event_handler=self.event_handler, host_selector=self.host_selector, suite_engine_proc=self.suite_engine_proc) def run_impl(self, opts, args, uuid, work_files): # Log file, temporary if hasattr(self.event_handler, "contexts"): t_file = TemporaryFile() log_context = ReporterContext(None, self.event_handler.VV, t_file) self.event_handler.contexts[uuid] = log_context # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() # Suite name from the current working directory if opts.conf_dir: self.fs_util.chdir(opts.conf_dir) opts.conf_dir = os.getcwd() # --remote=KEY=VALUE,... if opts.remote: # opts.name always set for remote. return self._run_remote(opts, opts.name) conf_tree = self.config_load(opts) self.fs_util.chdir(conf_tree.conf_dirs[0]) suite_name = opts.name if not opts.name: suite_name = os.path.basename(os.getcwd()) # Check suite.rc #! line for template scheme templ_scheme = "jinja2" if self.suite_engine_proc.SUITE_CONF in conf_tree.files: suiterc_path = os.path.join( conf_tree.files[self.suite_engine_proc.SUITE_CONF], self.suite_engine_proc.SUITE_CONF) with open(suiterc_path) as fh: line = fh.readline() if line.startswith("#!"): templ_scheme = line[2:].strip().lower() suite_section = (templ_scheme + ':' + self.suite_engine_proc.SUITE_CONF) extra_defines = [] if opts.defines_suite: for define in opts.defines_suite: extra_defines.append("[" + suite_section + "]" + define) # Automatic Rose constants # ROSE_ORIG_HOST: originating host # ROSE_VERSION: Rose version (not retained in run_mode=="reload") # Suite engine version my_rose_version = ResourceLocator.default().get_version() suite_engine_key = self.suite_engine_proc.get_version_env_name() if opts.run_mode in ["reload", "restart"]: prev_config_path = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") prev_config = ConfigLoader()(prev_config_path) suite_engine_version = prev_config.get_value( ["env", suite_engine_key]) else: suite_engine_version = self.suite_engine_proc.get_version() resloc = ResourceLocator.default() auto_items = [ (suite_engine_key, suite_engine_version), ("ROSE_ORIG_HOST", self.host_selector.get_local_host()), ("ROSE_SITE", resloc.get_conf().get_value(['site'], '')), ("ROSE_VERSION", resloc.get_version())] for key, val in auto_items: requested_value = conf_tree.node.get_value(["env", key]) if requested_value: if key == "ROSE_VERSION" and val != requested_value: exc = VersionMismatchError(requested_value, val) raise ConfigValueError(["env", key], requested_value, exc) val = requested_value else: conf_tree.node.set(["env", key], val, state=conf_tree.node.STATE_NORMAL) extra_defines.append('[%s]%s="%s"' % (suite_section, key, val)) # Pass automatic Rose constants as suite defines self.conf_tree_loader.node_loader.load_defines(extra_defines, conf_tree.node) # See if suite is running or not if opts.run_mode == "reload": # Check suite is running self.suite_engine_proc.get_suite_contact(suite_name) else: self.suite_engine_proc.check_suite_not_running(suite_name) # Install the suite to its run location suite_dir_rel = self._suite_dir_rel(suite_name) # Unfortunately a large try/finally block to ensure a temporary folder # created in validate only mode is cleaned up. Exceptions are not # caught here try: # Process Environment Variables environ = self.config_pm(conf_tree, "env") if opts.validate_suite_only_mode: temp_dir = mkdtemp() suite_dir = os.path.join(temp_dir, suite_dir_rel) os.makedirs(suite_dir, 0o0700) else: suite_dir = os.path.join( os.path.expanduser("~"), suite_dir_rel) suite_conf_dir = os.getcwd() locs_conf = ConfigNode() if opts.new_mode: if os.getcwd() == suite_dir: raise NewModeError("PWD", os.getcwd()) elif opts.run_mode in ["reload", "restart"]: raise NewModeError("--run", opts.run_mode) self.suite_run_cleaner.clean(suite_name) if os.getcwd() != suite_dir: if opts.run_mode == "run": self._run_init_dir(opts, suite_name, conf_tree, locs_conf=locs_conf) os.chdir(suite_dir) # Housekeep log files now_str = None if not opts.install_only_mode and not opts.local_install_only_mode: now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") self._run_init_dir_log(opts, now_str) self.fs_util.makedirs("log/suite") # Rose configuration and version logs self.fs_util.makedirs("log/rose-conf") run_mode = opts.run_mode if run_mode not in ["reload", "restart", "run"]: run_mode = "run" mode = run_mode if opts.validate_suite_only_mode: mode = "validate-suite-only" elif opts.install_only_mode: mode = "install-only" elif opts.local_install_only_mode: mode = "local-install-only" prefix = "rose-conf/%s-%s" % (strftime("%Y%m%dT%H%M%S"), mode) # Dump the actual configuration as rose-suite-run.conf ConfigDumper()(conf_tree.node, "log/" + prefix + ".conf") # Install version information file write_source_vc_info( suite_conf_dir, "log/" + prefix + ".version", self.popen) # If run through rose-stem, install version information files for # each source tree if they're a working copy if hasattr(opts, 'source') and hasattr(opts, 'project'): for i, url in enumerate(opts.source): if os.path.isdir(url): write_source_vc_info( url, "log/" + opts.project[i] + "-" + str(i) + ".version", self.popen) for ext in [".conf", ".version"]: self.fs_util.symlink(prefix + ext, "log/rose-suite-run" + ext) # Move temporary log to permanent log if hasattr(self.event_handler, "contexts"): log_file_path = os.path.abspath( os.path.join("log", "rose-suite-run.log")) log_file = open(log_file_path, "ab") temp_log_file = self.event_handler.contexts[uuid].handle temp_log_file.seek(0) log_file.write(temp_log_file.read()) self.event_handler.contexts[uuid].handle = log_file temp_log_file.close() # Process Files cwd = os.getcwd() for rel_path, conf_dir in conf_tree.files.items(): if (conf_dir == cwd or any(fnmatchcase(os.sep + rel_path, exclude) for exclude in self.SYNC_EXCLUDES) or conf_tree.node.get( [templ_scheme + ":" + rel_path]) is not None): continue # No sub-directories, very slow otherwise if os.sep in rel_path: rel_path = rel_path.split(os.sep, 1)[0] target_key = self.config_pm.get_handler( "file").PREFIX + rel_path target_node = conf_tree.node.get([target_key]) if target_node is None: conf_tree.node.set([target_key]) target_node = conf_tree.node.get([target_key]) elif target_node.is_ignored(): continue source_node = target_node.get("source") if source_node is None: target_node.set( ["source"], os.path.join( conf_dir, rel_path)) elif source_node.is_ignored(): continue self.config_pm(conf_tree, "file", no_overwrite_mode=opts.no_overwrite_mode) # Process suite configuration template header # (e.g. Jinja2:suite.rc, EmPy:suite.rc) self.config_pm(conf_tree, templ_scheme, environ=environ) # Ask suite engine to parse suite configuration # and determine if it is up to date (unchanged) if opts.validate_suite_only_mode: suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf( suite_dir, None, opts.strict_mode, debug_mode=True) else: suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf( suite_name, opts.run_mode, opts.strict_mode, opts.debug_mode) finally: # Ensure the temporary directory created is cleaned up regardless # of success or failure if opts.validate_suite_only_mode and os.path.exists(temp_dir): shutil.rmtree(temp_dir) # Only validating so finish now if opts.validate_suite_only_mode: return # Install share/work directories (local) for name in ["share", "share/cycle", "work"]: self._run_init_dir_work( opts, suite_name, name, conf_tree, locs_conf=locs_conf) if opts.local_install_only_mode: return # Install suite files to each remote [user@]host for name in ["", "log/", "share/", "share/cycle/", "work/"]: uuid_file = os.path.abspath(name + uuid) open(uuid_file, "w").close() work_files.append(uuid_file) # Install items to user@host auths = self.suite_engine_proc.get_tasks_auths(suite_name) proc_queue = [] # [[proc, command, "ssh"|"rsync", auth], ...] for auth in sorted(auths): host = auth if "@" in auth: host = auth.split("@", 1)[1] # Remote shell command = self.popen.get_cmd("ssh", "-n", auth) # Provide ROSE_VERSION and CYLC_VERSION in the environment shcommand = "env ROSE_VERSION=%s %s=%s" % ( my_rose_version, suite_engine_key, suite_engine_version) # Use login shell? no_login_shell = self._run_conf( "remote-no-login-shell", host=host, conf_tree=conf_tree) if not no_login_shell or no_login_shell.lower() != "true": shcommand += r""" bash -l -c '"$0" "$@"'""" # Path to "rose" command, if applicable rose_bin = self._run_conf( "remote-rose-bin", host=host, conf_tree=conf_tree, default="rose") # Build remote "rose suite-run" command shcommand += " %s suite-run -vv -n %s" % (rose_bin, suite_name) for key in ["new", "debug", "install-only"]: attr = key.replace("-", "_") + "_mode" if getattr(opts, attr, None) is not None: shcommand += " --%s" % key if opts.log_keep: shcommand += " --log-keep=%s" % opts.log_keep if opts.log_name: shcommand += " --log-name=%s" % opts.log_name if not opts.log_archive_mode: shcommand += " --no-log-archive" shcommand += " --run=%s" % opts.run_mode # Build --remote= option shcommand += " --remote=uuid=%s" % uuid if now_str is not None: shcommand += ",now-str=%s" % now_str host_confs = [ "root-dir", "root-dir{share}", "root-dir{share/cycle}", "root-dir{work}"] locs_conf.set([auth]) for key in host_confs: value = self._run_conf(key, host=host, conf_tree=conf_tree) if value is not None: val = self.popen.list_to_shell_str([str(value)]) shcommand += ",%s=%s" % (key, pipes.quote(val)) locs_conf.set([auth, key], value) command.append(shcommand) proc = self.popen.run_bg(*command) proc_queue.append([proc, command, "ssh", auth]) while proc_queue: sleep(self.SLEEP_PIPE) proc, command, command_name, auth = proc_queue.pop(0) if proc.poll() is None: # put it back in proc_queue proc_queue.append([proc, command, command_name, auth]) continue ret_code = proc.wait() out, err = proc.communicate() if ret_code: raise RosePopenError(command, ret_code, out, err) if command_name == "rsync": self.handle_event(out, level=Event.VV) continue else: self.handle_event(out, level=Event.VV, prefix="[%s] " % auth) for line in out.split("\n"): if "/" + uuid == line.strip(): locs_conf.unset([auth]) break else: filters = {"excludes": [], "includes": []} for name in ["", "log/", "share/", "share/cycle/", "work/"]: filters["excludes"].append(name + uuid) target = auth + ":" + suite_dir_rel cmd = self._get_cmd_rsync(target, **filters) proc_queue.append( [self.popen.run_bg(*cmd), cmd, "rsync", auth]) # Install ends ConfigDumper()(locs_conf, os.path.join("log", "rose-suite-run.locs")) if opts.install_only_mode: return elif opts.run_mode == "reload" and suite_conf_unchanged: conf_name = self.suite_engine_proc.SUITE_CONF self.handle_event(SkipReloadEvent(suite_name, conf_name)) return # Start the suite self.fs_util.chdir("log") self.suite_engine_proc.run(suite_name, opts.host, opts.run_mode, args) # Disconnect log file handle, so monitoring tool command will no longer # be associated with the log file. self.event_handler.contexts[uuid].handle.close() self.event_handler.contexts.pop(uuid) # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if opts.gcontrol_mode and opts.run_mode != "reload": self.suite_engine_proc.gcontrol(suite_name) return 0 @classmethod def _run_conf( cls, key, default=None, host=None, conf_tree=None, r_opts=None): """Return the value of a setting given by a key for a given host. If r_opts is defined, we are already in a remote host, so there is no need to do a host match. Otherwise, the setting may be found in the run time configuration, or the default (i.e. site/user configuration). The value of each setting in the configuration would be in a line delimited list of PATTERN=VALUE pairs. """ if r_opts is not None: return r_opts.get(key, default) if host is None: host = "localhost" for conf, keys in [ (conf_tree.node, []), (ResourceLocator.default().get_conf(), ["rose-suite-run"])]: if conf is None: continue node_value = conf.get_value(keys + [key]) if node_value is None: continue for line in node_value.strip().splitlines(): pattern, value = line.strip().split("=", 1) if (pattern.startswith("jinja2:") or pattern.startswith("empy:")): section, name = pattern.rsplit(":", 1) p_node = conf.get([section, name], no_ignore=True) # Values in "jinja2:*" and "empy:*" sections are quoted. pattern = ast.literal_eval(p_node.value) if fnmatchcase(host, pattern): return value.strip() return default def _run_init_dir(self, opts, suite_name, conf_tree=None, r_opts=None, locs_conf=None): """Create the suite's directory.""" suite_dir_rel = self._suite_dir_rel(suite_name) home = os.path.expanduser("~") suite_dir_root = self._run_conf("root-dir", conf_tree=conf_tree, r_opts=r_opts) if suite_dir_root: if locs_conf is not None: locs_conf.set(["localhost", "root-dir"], suite_dir_root) suite_dir_root = env_var_process(suite_dir_root) suite_dir_home = os.path.join(home, suite_dir_rel) if (suite_dir_root and os.path.realpath(home) != os.path.realpath(suite_dir_root)): suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel) self.fs_util.makedirs(suite_dir_real) self.fs_util.symlink(suite_dir_real, suite_dir_home, opts.no_overwrite_mode) else: self.fs_util.makedirs(suite_dir_home) def _run_init_dir_log(self, opts, now_str=None): """Create the suite's log/ directory. Housekeep, archive old ones.""" # Do nothing in log append mode if log directory already exists if opts.run_mode in ["reload", "restart"] and os.path.isdir("log"): return # Log directory of this run if now_str is None: now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") now_log = "log." + now_str self.fs_util.makedirs(now_log) self.fs_util.symlink(now_log, "log") now_log_name = getattr(opts, "log_name", None) if now_log_name: self.fs_util.symlink(now_log, "log." + now_log_name) # Keep log for this run and named logs logs = set(glob("log.*") + ["log"]) for log in list(logs): if os.path.islink(log): logs.remove(log) log_link = os.readlink(log) if log_link in logs: logs.remove(log_link) # Housekeep old logs, if necessary log_keep = getattr(opts, "log_keep", None) if log_keep: t_threshold = time() - abs(float(log_keep)) * 86400.0 for log in list(logs): if os.path.isfile(log): if t_threshold > os.stat(log).st_mtime: self.fs_util.delete(log) logs.remove(log) else: for root, _, files in os.walk(log): keep = False for file_ in files: path = os.path.join(root, file_) if (os.path.exists(path) and os.stat(path).st_mtime >= t_threshold): keep = True break if keep: break else: self.fs_util.delete(log) logs.remove(log) # Archive old logs, if necessary if getattr(opts, "log_archive_mode", True): for log in list(logs): if os.path.isfile(log): continue log_tar_gz = log + ".tar.gz" try: self.popen.run_simple("tar", "-czf", log_tar_gz, log) except RosePopenError: try: self.fs_util.delete(log_tar_gz) except OSError: pass raise else: self.handle_event(SuiteLogArchiveEvent(log_tar_gz, log)) self.fs_util.delete(log) def _run_init_dir_work(self, opts, suite_name, name, conf_tree=None, r_opts=None, locs_conf=None): """Create a named suite's directory.""" item_path = os.path.realpath(name) item_path_source = item_path key = "root-dir{" + name + "}" item_root = self._run_conf(key, conf_tree=conf_tree, r_opts=r_opts) if item_root is None: # backward compat item_root = self._run_conf( "root-dir-" + name, conf_tree=conf_tree, r_opts=r_opts) if item_root: if locs_conf is not None: locs_conf.set(["localhost", key], item_root) item_root = env_var_process(item_root) suite_dir_rel = self._suite_dir_rel(suite_name) if os.path.isabs(item_root): item_path_source = os.path.join(item_root, suite_dir_rel, name) else: item_path_source = item_root item_path_source = os.path.realpath(item_path_source) if item_path == item_path_source: if opts.new_mode: self.fs_util.delete(name) self.fs_util.makedirs(name) else: if opts.new_mode: self.fs_util.delete(item_path_source) self.fs_util.makedirs(item_path_source) if os.sep in name: dirname_of_name = os.path.dirname(name) self.fs_util.makedirs(dirname_of_name) item_path_source_rel = os.path.relpath( item_path_source, os.path.realpath(dirname_of_name)) else: item_path_source_rel = os.path.relpath(item_path_source) if len(item_path_source_rel) < len(item_path_source): self.fs_util.symlink( item_path_source_rel, name, opts.no_overwrite_mode) else: self.fs_util.symlink( item_path_source, name, opts.no_overwrite_mode) def _run_remote(self, opts, suite_name): """rose suite-run --remote=KEY=VALUE,...""" suite_dir_rel = self._suite_dir_rel(suite_name) r_opts = {} for item in opts.remote.split(","): key, val = item.split("=", 1) r_opts[key] = val uuid_file = os.path.join(suite_dir_rel, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("/" + r_opts["uuid"] + "\n", level=0) elif opts.new_mode: self.fs_util.delete(suite_dir_rel) if opts.run_mode == "run" or not os.path.exists(suite_dir_rel): self._run_init_dir(opts, suite_name, r_opts=r_opts) os.chdir(suite_dir_rel) for name in ["share", "share/cycle", "work"]: uuid_file = os.path.join(name, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event(name + "/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_work(opts, suite_name, name, r_opts=r_opts) if not opts.install_only_mode: uuid_file = os.path.join("log", r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event("log/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_log(opts, r_opts.get("now-str")) self.fs_util.makedirs("log/suite") def _get_cmd_rsync(self, target, excludes=None, includes=None): """rsync relevant suite items to target.""" if excludes is None: excludes = [] if includes is None: includes = [] cmd = self.popen.get_cmd("rsync") for exclude in excludes + list(self.SYNC_EXCLUDES): cmd.append("--exclude=" + exclude) for include in includes: cmd.append("--include=" + include) cmd.append("./") cmd.append(target) return cmd def _suite_dir_rel(self, suite_name): """Return the relative path to the suite running directory.""" return self.suite_engine_proc.get_suite_dir_rel(suite_name)
class StemRunner(object): """Set up options for running a STEM job through Rose.""" def __init__(self, opts, reporter=None, popen=None, fs_util=None): self.opts = opts if reporter is None: self.reporter = Reporter(opts.verbosity - opts.quietness) else: self.reporter = reporter if popen is None: self.popen = RosePopener(event_handler=self.reporter) else: self.popen = popen if fs_util is None: self.fs_util = FileSystemUtil(event_handler=self.reporter) else: self.fs_util = fs_util self.host_selector = HostSelector(event_handler=self.reporter, popen=self.popen) def _add_define_option(self, var, val): """Add a define option passed to the SuiteRunner.""" if self.opts.defines: self.opts.defines.append(SUITE_RC_PREFIX + var + '=' + val) else: self.opts.defines = [SUITE_RC_PREFIX + var + '=' + val] self.reporter(ConfigVariableSetEvent(var, val)) return def _get_base_dir(self, item): """Given a source tree return the following from 'fcm loc-layout': * url * sub_tree * peg_rev * root * project """ rc, output, stderr = self.popen.run('fcm', 'loc-layout', item) if rc != 0: raise ProjectNotFoundException(item, stderr) ret = {} for line in output.splitlines(): if ":" not in line: continue key, value = line.split(":", 1) if key: if value: ret[key] = value.strip() return ret def _get_project_from_url(self, source_dict): """Run 'fcm keyword-print' to work out the project name.""" repo = source_dict['root'] if source_dict['project']: repo += '/' + source_dict['project'] rc, kpoutput, stderr = self.popen.run('fcm', 'kp', source_dict['url']) project = None for line in kpoutput.splitlines(): if line.rstrip().endswith(repo): kpresult = re.search(r'^location{primary}\[(.*)\]', line) if kpresult: project = kpresult.group(1) break return project def _deduce_mirror(self, source_dict, project): """Deduce the mirror location of this source tree.""" # Root location for project proj_root = source_dict['root'] + '/' + source_dict['project'] # Swap project to mirror project = re.sub(r'\.x$', r'.xm', project) mirror_repo = "fcm:" + project # Generate mirror location mirror = re.sub(proj_root, mirror_repo, source_dict['url']) # Remove any sub-tree mirror = re.sub(source_dict['sub_tree'], r'', mirror) mirror = re.sub(r'/@', r'@', mirror) # Add forwards slash after .xm if missing if '.xm/' not in mirror: mirror = re.sub(r'\.xm', r'.xm/', mirror) return mirror def _ascertain_project(self, item): """Set the project name and top-level from 'fcm loc-layout'. Returns: * project name * top-level location of the source tree with revision number * top-level location of the source tree without revision number * revision number """ project = None try: project, item = item.split("=", 1) except ValueError: pass if re.search(r'^\.', item): item = os.path.abspath(os.path.join(os.getcwd(), item)) if project is not None: print "[WARN] Forcing project for '{0}' to be '{1}'".format( item, project) return project, item, item, '', '' source_dict = self._get_base_dir(item) project = self._get_project_from_url(source_dict) if not project: raise ProjectNotFoundException(item) mirror = self._deduce_mirror(source_dict, project) if 'peg_rev' in source_dict and '@' in item: revision = '@' + source_dict['peg_rev'] base = re.sub(r'@.*', r'', item) else: revision = '' base = item # Remove subtree from base and item if 'sub_tree' in source_dict: item = re.sub( r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', item, count=1) base = re.sub( r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', base, count=1) # Remove trailing forwards-slash item = re.sub(r'/$', r'', item) base = re.sub(r'/$', r'', base) # Remove anything after a point project = re.sub(r'\..*', r'', project) return project, item, base, revision, mirror def _generate_name(self): """Generate a suite name from the name of the first source tree.""" try: basedir = self._ascertain_project(os.getcwd())[1] except ProjectNotFoundException: if self.opts.conf_dir: basedir = os.path.abspath(self.opts.conf_dir) else: basedir = os.getcwd() name = os.path.basename(basedir) self.reporter(NameSetEvent(name)) return name def _this_suite(self): """Find the location of the suite in the first source tree.""" # Get base of first source basedir = '' if self.opts.source: basedir = self.opts.source[0] else: basedir = self._ascertain_project(os.getcwd())[1] suitedir = os.path.join(basedir, DEFAULT_TEST_DIR) suitefile = os.path.join(suitedir, "rose-suite.conf") if not os.path.isfile(suitefile): raise RoseSuiteConfNotFoundException(suitedir) self._check_suite_version(suitefile) return suitedir def _read_site_config_and_return_options(self): """Read the site rose.conf file.""" return ResourceLocator.default().get_conf().get_value( ["rose-stem", "automatic-options"]) def _check_suite_version(self, fname): """Check the suite is compatible with this version of rose-stem.""" if not os.path.isfile(fname): raise RoseSuiteConfNotFoundException(os.path.dirname(fname)) config = rose.config.load(fname) suite_rose_stem_version = config.get(['ROSE_STEM_VERSION']) if suite_rose_stem_version: suite_rose_stem_version = int(suite_rose_stem_version.value) else: suite_rose_stem_version = None if not suite_rose_stem_version == ROSE_STEM_VERSION: raise RoseStemVersionException(suite_rose_stem_version) def _prepend_localhost(self, url): """Prepend the local hostname to urls which do not point to repository locations.""" if ':' not in url or url.split(':', 1)[0] not in ['svn', 'fcm', 'http', 'https', 'svn+ssh']: url = self.host_selector.get_local_host() + ':' + url return url def process(self): """Process STEM options into 'rose suite-run' options.""" # Generate options for source trees repos = {} repos_with_hosts = {} if not self.opts.source: self.opts.source = ['.'] self.opts.project = list() for i, url in enumerate(self.opts.source): project, url, base, rev, mirror = self._ascertain_project(url) self.opts.source[i] = url self.opts.project.append(project) # Versions of variables with hostname prepended for working copies url_host = self._prepend_localhost(url) base_host = self._prepend_localhost(base) if project in repos: repos[project].append(url) repos_with_hosts[project].append(url_host) else: repos[project] = [url] repos_with_hosts[project] = [url_host] self._add_define_option('SOURCE_' + project.upper() + '_REV', '"' + rev + '"') self._add_define_option('SOURCE_' + project.upper() + '_BASE', '"' + base + '"') self._add_define_option('HOST_SOURCE_' + project.upper() + '_BASE', '"' + base_host + '"') self._add_define_option('SOURCE_' + project.upper() + '_MIRROR', '"' + mirror + '"') self.reporter(SourceTreeAddedAsBranchEvent(url)) for project, branches in repos.iteritems(): var = 'SOURCE_' + project.upper() branchstring = RosePopener.list_to_shell_str(branches) self._add_define_option(var, '"' + branchstring + '"') for project, branches in repos_with_hosts.iteritems(): var_host = 'HOST_SOURCE_' + project.upper() branchstring = RosePopener.list_to_shell_str(branches) self._add_define_option(var_host, '"' + branchstring + '"') # Generate the variable containing tasks to run if self.opts.group: if not self.opts.defines: self.opts.defines = [] expanded_groups = [] for i in self.opts.group: expanded_groups.extend(i.split(',')) self.opts.defines.append(SUITE_RC_PREFIX + 'RUN_NAMES=' + str(expanded_groups)) # Load the config file and return any automatic-options auto_opts = self._read_site_config_and_return_options() if auto_opts: automatic_options = auto_opts.split() for option in automatic_options: elements = option.split("=") if len(elements) == 2: self._add_define_option( elements[0], '"' + elements[1] + '"') # Change into the suite directory if self.opts.conf_dir: self.reporter(SuiteSelectionEvent(self.opts.conf_dir)) self._check_suite_version( os.path.join(self.opts.conf_dir, 'rose-suite.conf')) else: thissuite = self._this_suite() self.fs_util.chdir(thissuite) self.reporter(SuiteSelectionEvent(thissuite)) # Create a default name for the suite; allow override by user if not self.opts.name: self.opts.name = self._generate_name() return self.opts
class SuiteRestarter(object): """Wrap "cylc restart".""" def __init__(self, event_handler=None): self.event_handler = event_handler self.popen = RosePopener(self.event_handler) self.fs_util = FileSystemUtil(self.event_handler) self.config_pm = ConfigProcessorsManager(self.event_handler, self.popen, self.fs_util) self.host_selector = HostSelector(self.event_handler, self.popen) self.suite_engine_proc = SuiteEngineProcessor.get_processor( event_handler=self.event_handler, popen=self.popen, fs_util=self.fs_util) def handle_event(self, *args, **kwargs): """Handle event.""" if callable(self.event_handler): self.event_handler(*args, **kwargs) def restart(self, suite_name=None, host=None, gcontrol_mode=None, args=None): """Restart a "cylc" suite.""" # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() if not suite_name: suite_name = get_suite_name(self.event_handler) suite_dir = self.suite_engine_proc.get_suite_dir(suite_name) if not os.path.exists(suite_dir): raise SuiteNotFoundError(suite_dir) # Ensure suite is not running hosts = [] if host: hosts.append(host) self.suite_engine_proc.check_suite_not_running(suite_name, hosts) # Determine suite host to restart suite if host: hosts = [host] else: hosts = [] val = ResourceLocator.default().get_conf().get_value( ["rose-suite-run", "hosts"], "localhost") known_hosts = self.host_selector.expand(val.split())[0] for known_host in known_hosts: if known_host not in hosts: hosts.append(known_host) if hosts == ["localhost"]: host = hosts[0] else: host = self.host_selector(hosts)[0][0] self.handle_event(SuiteHostSelectEvent(suite_name, "restart", host)) # Suite host environment run_conf_file_name = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") try: run_conf = ConfigLoader().load(run_conf_file_name) except (ConfigSyntaxError, IOError): environ = None else: run_conf_tree = ConfigTree() run_conf_tree.node = run_conf environ = self.config_pm(run_conf_tree, "env") # Restart the suite self.suite_engine_proc.run(suite_name, host, environ, "restart", args) # Write suite host name to host file host_file_name = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.host") open(host_file_name, "w").write(host + "\n") # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return