Example #1
0
 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)
Example #2
0
 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)
Example #3
0
 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))
Example #4
0
File: stem.py Project: kinow/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)
Example #5
0
 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)
Example #6
0
File: bush.py Project: kaday/rose
 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
Example #7
0
 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
Example #8
0
 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)
Example #9
0
 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
Example #10
0
 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()
Example #11
0
    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
Example #12
0
 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
Example #13
0
 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)
Example #14
0
 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
Example #15
0
 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
Example #16
0
File: bush.py Project: lexual/rose
    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
Example #17
0
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"]))
Example #18
0
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)
Example #19
0
File: stem.py Project: kinow/rose
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
Example #20
0
    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
Example #21
0
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)
Example #22
0
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"]))
Example #23
0
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
Example #24
0
    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
Example #25
0
File: bush.py Project: kaday/rose
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"]))
Example #26
0
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"]))
Example #27
0
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)
Example #28
0
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
Example #29
0
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