def check_flow_file( path: Union[Path, str], symlink_suiterc: bool = False, logger: 'Logger' = LOG ) -> Path: """Raises WorkflowFilesError if no flow file in path sent. Args: path: Path to check for a flow.cylc and/or suite.rc file. symlink_suiterc: If True and suite.rc exists but not flow.cylc, create flow.cylc as a symlink to suite.rc. logger: A custom logger to use to log warnings. Returns the path of the flow file if present. """ flow_file_path = Path(expand_path(path), SuiteFiles.FLOW_FILE) if flow_file_path.is_file(): # Note: this includes if flow.cylc is a symlink return flow_file_path suite_rc_path = Path(path, SuiteFiles.SUITE_RC) if suite_rc_path.is_file(): if symlink_suiterc: flow_file_path.symlink_to(suite_rc_path) logger.warning( f'The filename "{SuiteFiles.SUITE_RC}" is deprecated in ' f'favour of "{SuiteFiles.FLOW_FILE}". Symlink created.') return suite_rc_path raise WorkflowFilesError( f"no {SuiteFiles.FLOW_FILE} or {SuiteFiles.SUITE_RC} in {path}")
def format_directives(cls, job_conf): """Format the job directives for a job file.""" job_file_path = expand_path(job_conf['job_file_path']) directives = job_conf['directives'].__class__() directives['--job-name'] = (job_conf['task_id'] + '.' + job_conf['workflow_name']) directives['--output'] = job_file_path.replace('%', '%%') + ".out" directives['--error'] = job_file_path.replace('%', '%%') + ".err" if (job_conf["execution_time_limit"] and directives.get("--time") is None): directives["--time"] = "%d:%02d" % ( job_conf["execution_time_limit"] / 60, job_conf["execution_time_limit"] % 60) for key, value in list(job_conf['directives'].items()): directives[key] = value lines = [] seen = set() for key, value in directives.items(): m = cls.REC_HETJOB.match(key) if m: n = m.groups()[0] if n != "0" and n not in seen: lines.append(cls.SEP_HETJOB) seen.add(n) newkey = cls.REC_HETJOB.sub('', key) else: newkey = key if value: lines.append("%s%s=%s" % (cls.DIRECTIVE_PREFIX, newkey, value)) else: lines.append("%s%s" % (cls.DIRECTIVE_PREFIX, newkey)) return lines
def test_expand_path( path: str, expected: str, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv('FOO', 'foo/bar') monkeypatch.delenv('NON_EXIST', raising=False) assert expand_path(path) == expected
def check_flow_file( path: Union[Path, str], symlink_suiterc: bool = False, logger: Optional['Logger'] = LOG ) -> Path: """Raises WorkflowFilesError if no flow file in path sent. Args: path: Path to check for a flow.cylc and/or suite.rc file. symlink_suiterc: If True and suite.rc exists, create flow.cylc as a symlink to suite.rc. If a flow.cylc symlink already exists but points elsewhere, it will be replaced. logger: A custom logger to use to log warnings. Returns the path of the flow file if present. """ flow_file_path = Path(expand_path(path), WorkflowFiles.FLOW_FILE) suite_rc_path = Path(expand_path(path), WorkflowFiles.SUITE_RC) depr_msg = ( f'The filename "{WorkflowFiles.SUITE_RC}" is deprecated ' f'in favour of "{WorkflowFiles.FLOW_FILE}"') if flow_file_path.is_file(): if not flow_file_path.is_symlink(): return flow_file_path if flow_file_path.resolve() == suite_rc_path.resolve(): # A symlink that points to *existing* suite.rc if logger: logger.warning(depr_msg) return flow_file_path if suite_rc_path.is_file(): if not symlink_suiterc: if logger: logger.warning(depr_msg) return suite_rc_path if flow_file_path.is_symlink(): # Symlink broken or points elsewhere - replace flow_file_path.unlink() flow_file_path.symlink_to(WorkflowFiles.SUITE_RC) if logger: logger.warning(f'{depr_msg}. Symlink created.') return flow_file_path raise WorkflowFilesError( f"no {WorkflowFiles.FLOW_FILE} or {WorkflowFiles.SUITE_RC} in {path}")
def unlink_runN(path: Union[Path, str]) -> bool: """Remove symlink runN if it exists. Args: path: Absolute path to workflow dir containing runN. """ try: Path(expand_path(path, WorkflowFiles.RUN_N)).unlink() except OSError: return False return True
def workflow_state(workflow, task, point, offset=None, status='succeeded', message=None, cylc_run_dir=None): """Connect to a workflow DB and query the requested task state. * Reports satisfied only if the remote workflow state has been achieved. * Returns all workflow state args to pass on to triggering tasks. Arguments: workflow (str): The workflow to interrogate. task (str): The name of the task to query. point (str): The cycle point. offset (str): The offset between the cycle this xtrigger is used in and the one it is querying for as an ISO8601 time duration. e.g. PT1H (one hour). status (str): The task status required for this xtrigger to be satisfied. message (str): The custom task output required for this xtrigger to be satisfied. .. note:: This cannot be specified in conjunction with ``status``. cylc_run_dir (str): The directory in which the workflow to interrogate. .. note:: This only needs to be supplied if the workflow is running in a different location to what is specified in the global configuration (usually ``~/cylc-run``). Returns: tuple: (satisfied, results) satisfied (bool): True if ``satisfied`` else ``False``. results (dict): Dictionary containing the args / kwargs which were provided to this xtrigger. """ if cylc_run_dir: cylc_run_dir = expand_path(cylc_run_dir) else: cylc_run_dir = get_workflow_run_dir('') if offset is not None: point = str(add_offset(point, offset)) try: checker = CylcWorkflowDBChecker(cylc_run_dir, workflow) except (OSError, sqlite3.Error): # Failed to connect to DB; target workflow may not be started. return (False, None) fmt = checker.get_remote_point_format() if fmt: my_parser = TimePointParser() point = str(my_parser.parse(point, dump_format=fmt)) if message is not None: satisfied = checker.task_state_met(task, point, message=message) else: satisfied = checker.task_state_met(task, point, status=status) results = { 'workflow': workflow, 'task': task, 'point': point, 'offset': offset, 'status': status, 'message': message, 'cylc_run_dir': cylc_run_dir } return satisfied, results
def __init__(self, rund, workflow): db_path = expand_path(rund, workflow, "log", CylcWorkflowDAO.DB_FILE_BASE_NAME) if not os.path.exists(db_path): raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), db_path) self.conn = sqlite3.connect(db_path, timeout=10.0)
def main(parser: COP, options: 'Values', reg: str, task_id: Optional[str] = None, color: bool = False) -> None: """Implement cylc cat-log CLI. Determine log path, user@host, batchview_cmd, and action (print, dir-list, cat, edit, or tail), and then if the log path is: a) local: perform action on log path, or b) remote: re-invoke cylc cat-log as a) on the remote account """ if options.remote_args: # Invoked on job hosts for job logs only, as a wrapper to view_log(). # Tail and batchview commands from global config on workflow host). logpath, mode, tail_tmpl = options.remote_args[0:3] logpath = expand_path(logpath) tail_tmpl = expand_path(tail_tmpl) try: batchview_cmd = options.remote_args[3] except IndexError: batchview_cmd = None res = view_log(logpath, mode, tail_tmpl, batchview_cmd, remote=True, color=color) if res == 1: sys.exit(res) return workflow_name, _ = parse_reg(reg) # Get long-format mode. try: mode = MODES[options.mode] except KeyError: mode = options.mode if not task_id: # Cat workflow logs, local only. if options.filename is not None: raise UserInputError("The '-f' option is for job logs only.") logpath = get_workflow_run_log_name(workflow_name) if options.rotation_num: logs = glob('%s.*' % logpath) logs.sort(key=os.path.getmtime, reverse=True) try: logpath = logs[int(options.rotation_num)] except IndexError: raise UserInputError("max rotation %d" % (len(logs) - 1)) tail_tmpl = os.path.expandvars(get_platform()["tail command template"]) out = view_log(logpath, mode, tail_tmpl, color=color) if out == 1: sys.exit(1) if mode == 'edit': tmpfile_edit(out, options.geditor) return if task_id: # Cat task job logs, may be on workflow or job host. if options.rotation_num is not None: raise UserInputError("only workflow (not job) logs get rotated") try: task, point = TaskID.split(task_id) except ValueError: parser.error("Illegal task ID: %s" % task_id) if options.submit_num != NN: try: options.submit_num = "%02d" % int(options.submit_num) except ValueError: parser.error("Illegal submit number: %s" % options.submit_num) if options.filename is None: options.filename = JOB_LOG_OUT else: # Convert short filename args to long (e.g. 'o' to 'job.out'). with suppress(KeyError): options.filename = JOB_LOG_OPTS[options.filename] # KeyError: Is already long form (standard log, or custom). platform_name, job_runner_name, live_job_id = get_task_job_attrs( workflow_name, point, task, options.submit_num) platform = get_platform(platform_name) batchview_cmd = None if live_job_id is not None: # Job is currently running. Get special job runner log view # command (e.g. qcat) if one exists, and the log is out or err. conf_key = None if options.filename == JOB_LOG_OUT: if mode == 'cat': conf_key = "out viewer" elif mode == 'tail': conf_key = "out tailer" elif options.filename == JOB_LOG_ERR: if mode == 'cat': conf_key = "err viewer" elif mode == 'tail': conf_key = "err tailer" if conf_key is not None: batchview_cmd_tmpl = None with suppress(KeyError): batchview_cmd_tmpl = platform[conf_key] if batchview_cmd_tmpl is not None: batchview_cmd = batchview_cmd_tmpl % { "job_id": str(live_job_id) } log_is_remote = (is_remote_platform(platform) and (options.filename != JOB_LOG_ACTIVITY)) log_is_retrieved = (platform['retrieve job logs'] and live_job_id is None) if log_is_remote and (not log_is_retrieved or options.force_remote): logpath = os.path.normpath( get_remote_workflow_run_job_dir(workflow_name, point, task, options.submit_num, options.filename)) tail_tmpl = platform["tail command template"] # Reinvoke the cat-log command on the remote account. cmd = ['cat-log', *verbosity_to_opts(cylc.flow.flags.verbosity)] for item in [logpath, mode, tail_tmpl]: cmd.append('--remote-arg=%s' % shlex.quote(item)) if batchview_cmd: cmd.append('--remote-arg=%s' % shlex.quote(batchview_cmd)) cmd.append(workflow_name) is_edit_mode = (mode == 'edit') # TODO: Add Intelligent Host selection to this try: proc = remote_cylc_cmd(cmd, platform, capture_process=is_edit_mode, manage=(mode == 'tail')) except KeyboardInterrupt: # Ctrl-C while tailing. pass else: if is_edit_mode: # Write remote stdout to a temp file for viewing in editor. # Only BUFSIZE bytes at a time in case huge stdout volume. out = NamedTemporaryFile() data = proc.stdout.read(BUFSIZE) while data: out.write(data) data = proc.stdout.read(BUFSIZE) os.chmod(out.name, S_IRUSR) out.seek(0, 0) else: # Local task job or local job log. logpath = os.path.normpath( get_workflow_run_job_dir(workflow_name, point, task, options.submit_num, options.filename)) tail_tmpl = os.path.expandvars(platform["tail command template"]) out = view_log(logpath, mode, tail_tmpl, batchview_cmd, color=color) if mode != 'edit': sys.exit(out) if mode == 'edit': tmpfile_edit(out, options.geditor)
def main(parser: COP, options: 'Values', workflow: str) -> None: workflow, _ = parse_reg(workflow) if options.use_task_point and options.cycle: raise UserInputError( "cannot specify a cycle point and use environment variable") if options.use_task_point: if "CYLC_TASK_CYCLE_POINT" not in os.environ: raise UserInputError("CYLC_TASK_CYCLE_POINT is not defined") options.cycle = os.environ["CYLC_TASK_CYCLE_POINT"] if options.offset and not options.cycle: raise UserInputError( "You must target a cycle point to use an offset") # Attempt to apply specified offset to the targeted cycle if options.offset: options.cycle = str(add_offset(options.cycle, options.offset)) # Exit if both task state and message are to being polled if options.status and options.msg: raise UserInputError("cannot poll both status and custom output") if options.msg and not options.task and not options.cycle: raise UserInputError("need a taskname and cyclepoint") # Exit if an invalid status is requested if (options.status and options.status not in TASK_STATUSES_ORDERED and options.status not in CylcWorkflowDBChecker.STATE_ALIASES): raise UserInputError(f"invalid status '{options.status}'") # this only runs locally if options.run_dir: run_dir = expand_path(options.run_dir) else: run_dir = get_cylc_run_dir() pollargs = { 'workflow': workflow, 'run_dir': run_dir, 'task': options.task, 'cycle': options.cycle, 'status': options.status, 'message': options.msg, } spoller = WorkflowPoller("requested state", options.interval, options.max_polls, args=pollargs) connected, formatted_pt = spoller.connect() if not connected: raise CylcError("cannot connect to the workflow DB") if options.status and options.task and options.cycle: # check a task status spoller.condition = options.status if not spoller.poll(): sys.exit(1) elif options.msg: # Check for a custom task output spoller.condition = "output: %s" % options.msg if not spoller.poll(): sys.exit(1) else: # just display query results spoller.checker.display_maps( spoller.checker.workflow_state_query( task=options.task, cycle=formatted_pt, status=options.status))
def main(parser, options, suite): if options.use_task_point and options.cycle: raise UserInputError( "cannot specify a cycle point and use environment variable") if options.use_task_point: if "CYLC_TASK_CYCLE_POINT" in os.environ: options.cycle = os.environ["CYLC_TASK_CYCLE_POINT"] else: raise UserInputError("CYLC_TASK_CYCLE_POINT is not defined") if options.offset and not options.cycle: raise UserInputError("You must target a cycle point to use an offset") if options.template: print("WARNING: ignoring --template (no longer needed)", file=sys.stderr) # Attempt to apply specified offset to the targeted cycle if options.offset: options.cycle = str(add_offset(options.cycle, options.offset)) # Exit if both task state and message are to being polled if options.status and options.msg: raise UserInputError("cannot poll both status and custom output") if options.msg and not options.task and not options.cycle: raise UserInputError("need a taskname and cyclepoint") # Exit if an invalid status is requested if (options.status and options.status not in TASK_STATUSES_ORDERED and options.status not in CylcSuiteDBChecker.STATE_ALIASES): raise UserInputError("invalid status '" + options.status + "'") # this only runs locally run_dir = expand_path(options.run_dir or get_platform()['run directory']) pollargs = { 'suite': suite, 'run_dir': run_dir, 'task': options.task, 'cycle': options.cycle, 'status': options.status, 'message': options.msg, } spoller = SuitePoller("requested state", options.interval, options.max_polls, args=pollargs) connected, formatted_pt = spoller.connect() if not connected: raise CylcError("cannot connect to the suite DB") if options.status and options.task and options.cycle: # check a task status spoller.condition = options.status if not spoller.poll(): sys.exit(1) elif options.msg: # Check for a custom task output spoller.condition = "output: %s" % options.msg if not spoller.poll(): sys.exit(1) else: # just display query results spoller.checker.display_maps( spoller.checker.suite_state_query(task=options.task, cycle=formatted_pt, status=options.status))
def install_workflow( flow_name: Optional[str] = None, source: Optional[Union[Path, str]] = None, run_name: Optional[str] = None, no_run_name: bool = False, no_symlinks: bool = False ) -> Tuple[Path, Path, str]: """Install a workflow, or renew its installation. Install workflow into new run directory. Create symlink to suite source location, creating any symlinks for run, work, log, share, share/cycle directories. Args: flow_name: workflow name, default basename($PWD). source: directory location of flow.cylc file, default $PWD. run_name: name of the run, overrides run1, run2, run 3 etc... If specified, cylc install will not create runN symlink. rundir: for overriding the default cylc-run directory. no_run_name: Flag as True to install workflow into ~/cylc-run/<flow_name> no_symlinks: Flag as True to skip making localhost symlink dirs Return: source: The source directory. rundir: The directory the workflow has been installed into. flow_name: The installed suite name (which may be computed here). Raise: WorkflowFilesError: No flow.cylc file found in source location. Illegal name (can look like a relative path, but not absolute). Another suite already has this name (unless --redirect). Trying to install a workflow that is nested inside of another. """ if not source: source = Path.cwd() elif Path(source).name == SuiteFiles.FLOW_FILE: source = Path(source).parent source = Path(expand_path(source)) if not flow_name: flow_name = Path.cwd().stem validate_flow_name(flow_name) if run_name in SuiteFiles.RESERVED_NAMES: raise WorkflowFilesError( f'Run name cannot be "{run_name}".') validate_source_dir(source, flow_name) run_path_base = Path(get_workflow_run_dir(flow_name)) relink, run_num, rundir = get_run_dir(run_path_base, run_name, no_run_name) if Path(rundir).exists(): raise WorkflowFilesError( f"\"{rundir}\" exists." " Try using cylc reinstall. Alternatively, install with another" " name, using the --run-name option.") check_nested_run_dirs(rundir, flow_name) symlinks_created = {} if not no_symlinks: sub_dir = flow_name if run_num: sub_dir += '/' + f'run{run_num}' symlinks_created = make_localhost_symlinks(rundir, sub_dir) INSTALL_LOG = _get_logger(rundir, 'cylc-install') if not no_symlinks and bool(symlinks_created) is True: for src, dst in symlinks_created.items(): INSTALL_LOG.info(f"Symlink created from {src} to {dst}") try: rundir.mkdir(exist_ok=True) except OSError as e: if e.strerror == "File exists": raise WorkflowFilesError(f"Run directory already exists : {e}") if relink: link_runN(rundir) create_workflow_srv_dir(rundir) rsync_cmd = get_rsync_rund_cmd(source, rundir) proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True) stdout, stderr = proc.communicate() INSTALL_LOG.info(f"Copying files from {source} to {rundir}") INSTALL_LOG.info(f"{stdout}") if not proc.returncode == 0: INSTALL_LOG.warning( f"An error occurred when copying files from {source} to {rundir}") INSTALL_LOG.warning(f" Error: {stderr}") cylc_install = Path(rundir.parent, SuiteFiles.Install.DIRNAME) check_flow_file(rundir, symlink_suiterc=True, logger=INSTALL_LOG) if no_run_name: cylc_install = Path(rundir, SuiteFiles.Install.DIRNAME) source_link = cylc_install.joinpath(SuiteFiles.Install.SOURCE) cylc_install.mkdir(parents=True, exist_ok=True) if not source_link.exists(): INSTALL_LOG.info(f"Creating symlink from {source_link}") source_link.symlink_to(source) elif source_link.exists() and (os.readlink(source_link) == str(source)): INSTALL_LOG.info( f"Symlink from \"{source_link}\" to \"{source}\" in place.") else: raise WorkflowFilesError( "Source directory between runs are not consistent.") # check source link matches the source symlink from workflow dir. INSTALL_LOG.info(f'INSTALLED {flow_name} from {source} -> {rundir}') print(f'INSTALLED {flow_name} from {source} -> {rundir}') _close_install_log(INSTALL_LOG) return source, rundir, flow_name