def construct_remote_tidy_ssh_cmd( platform: Dict[str, Any]) -> Tuple[List[str], str]: cmd = ['remote-tidy'] cmd.extend(verbosity_to_opts(cylc.flow.flags.verbosity)) cmd.append(get_install_target_from_platform(platform)) cmd.append(get_remote_workflow_run_dir(self.workflow)) host = get_host_from_platform(platform, bad_hosts=self.bad_hosts) cmd = construct_ssh_cmd(cmd, platform, host, timeout='10s') return cmd, host
def _construct_ssh_cmd(raw_cmd, host=None, forward_x11=False, stdin=False, ssh_cmd=None, ssh_login_shell=None, remote_cylc_path=None, set_UTC=False, set_verbosity=False, timeout=None): """Build an SSH command for execution on a remote platform hosts. Arguments: raw_cmd (list): primitive command to run remotely. host (string): remote host name. Use 'localhost' if not specified. forward_x11 (boolean): If True, use 'ssh -Y' to enable X11 forwarding, else just 'ssh'. stdin: If None, the `-n` option will be added to the SSH command line. ssh_cmd (string): ssh command to use: If unset defaults to localhost ssh cmd. ssh_login_shell (boolean): If True, launch remote command with `bash -l -c 'exec "$0" "$@"'`. remote_cylc_path (string): Path containing the `cylc` executable. This is required if the remote executable is not in $PATH. set_UTC (boolean): If True, check UTC mode and specify if set to True (non-default). set_verbosity (boolean): If True apply -q, -v opts to match cylc.flow.flags.verbosity. timeout (str): String for bash timeout command. Return: list - A list containing a chosen command including all arguments and options necessary to directly execute the bare command on a given host via ssh. """ # If ssh cmd isn't given use the default from localhost settings. if ssh_cmd is None: command = shlex.split(get_platform()['ssh command']) else: command = shlex.split(ssh_cmd) if forward_x11: command.append('-Y') if stdin is None: command.append('-n') user_at_host = '' if host: user_at_host += host else: user_at_host += 'localhost' command.append(user_at_host) # Pass CYLC_VERSION and optionally, CYLC_CONF_PATH & CYLC_UTC through. command += ['env', quote(r'CYLC_VERSION=%s' % CYLC_VERSION)] for envvar in [ 'CYLC_CONF_PATH', 'CYLC_COVERAGE', 'CLIENT_COMMS_METH', 'CYLC_ENV_NAME' ]: if envvar in os.environ: command.append(quote(f'{envvar}={os.environ[envvar]}')) if set_UTC and os.getenv('CYLC_UTC') in ["True", "true"]: command.append(quote(r'CYLC_UTC=True')) command.append(quote(r'TZ=UTC')) # Use bash -l? if ssh_login_shell is None: ssh_login_shell = get_platform()['use login shell'] if ssh_login_shell: # A login shell will always source /etc/profile and the user's bash # profile file. To avoid having to quote the entire remote command # it is passed as arguments to the bash script. command += ['bash', '--login', '-c', quote(r'exec "$0" "$@"')] if timeout: command += ['timeout', timeout] # 'cylc' on the remote host if not remote_cylc_path: remote_cylc_path = get_platform()['cylc path'] if remote_cylc_path: cylc_cmd = str(Path(remote_cylc_path) / 'cylc') else: cylc_cmd = 'cylc' command.append(cylc_cmd) # Insert core raw command after ssh, but before its own, command options. command += raw_cmd if set_verbosity: command.extend(verbosity_to_opts(cylc.flow.flags.verbosity)) return command
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 remote_init(self, platform: Dict[str, Any], curve_auth: 'ThreadAuthenticator', client_pub_key_dir: str) -> None: """Initialise a remote host if necessary. Call "cylc remote-init" to install workflow items to remote: ".service/contact": For TCP task communication "python/": if source exists Args: platform: A dict containing settings relating to platform used in this remote installation. curve_auth: The ZMQ authenticator. client_pub_key_dir: Client public key directory, used by the ZMQ authenticator. """ install_target = platform['install target'] if install_target == get_localhost_install_target(): self.remote_init_map[install_target] = REMOTE_FILE_INSTALL_DONE return # Set status of install target to in progress while waiting for remote # initialisation to finish self.remote_init_map[install_target] = REMOTE_INIT_IN_PROGRESS # Determine what items to install comms_meth: CommsMeth = CommsMeth(platform['communication method']) items = self._remote_init_items(comms_meth) # Create a TAR archive with the service files, # so they can be sent later via SSH's STDIN to the task remote. tmphandle = self.proc_pool.get_temporary_file() tarhandle = tarfile.open(fileobj=tmphandle, mode='w') for path, arcname in items: tarhandle.add(path, arcname=arcname) tarhandle.close() tmphandle.seek(0) # Build the remote-init command to be run over ssh cmd = ['remote-init'] cmd.extend(verbosity_to_opts(cylc.flow.flags.verbosity)) cmd.append(str(install_target)) cmd.append(get_remote_workflow_run_dir(self.workflow)) dirs_to_symlink = get_dirs_to_symlink(install_target, self.workflow) for key, value in dirs_to_symlink.items(): if value is not None: cmd.append(f"{key}={quote(value)} ") # Create the ssh command try: host = get_host_from_platform(platform, bad_hosts=self.bad_hosts) except NoHostsError as exc: LOG.error( PlatformError( f'{PlatformError.MSG_INIT}\n{exc}', platform['name'], )) self.remote_init_map[ platform['install target']] = REMOTE_INIT_FAILED self.bad_hosts -= set(platform['hosts']) self.ready = True else: log_platform_event('remote init', platform, host) cmd = construct_ssh_cmd(cmd, platform, host) self.proc_pool.put_command( SubProcContext('remote-init', cmd, stdin_files=[tmphandle], host=host), bad_hosts=self.bad_hosts, callback=self._remote_init_callback, callback_args=[ platform, tmphandle, curve_auth, client_pub_key_dir ], callback_255=self._remote_init_callback_255, callback_255_args=[platform])