Skip to content

Commit

Permalink
remote: ensure all remote commands use a platform config
Browse files Browse the repository at this point in the history
* Remote interfaces (SSH & rsync) now all require a platform
  object for configuration purposes (e.g. "ssh command").
* Convenience interfaces added for remote calls to Cylc servers
  which use the localhost platform configuration. These
  are now used for:
  * `cylc play` command re-invocation on a Cylc server.
  * Evaluation of host selection rankings (via `cylc psutil`).
  * The detect old contact file check, which tests whether
    a workflow is still running on the server recored in the
    contact file.
  • Loading branch information
oliver-sanders committed Sep 21, 2022
1 parent 686c1a9 commit 554ea02
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 91 deletions.
4 changes: 2 additions & 2 deletions cylc/flow/host_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from cylc.flow.cfgspec.glbl_cfg import glbl_cfg
from cylc.flow.exceptions import CylcError, HostSelectException
from cylc.flow.hostuserutil import get_fqdn_by_host, is_remote_host
from cylc.flow.remote import _remote_cylc_cmd, run_cmd
from cylc.flow.remote import run_cmd, cylc_server_cmd
from cylc.flow.terminal import parse_dirty_json


Expand Down Expand Up @@ -533,7 +533,7 @@ def _get_metrics(hosts, metrics, data=None):
}
for host in hosts:
if is_remote_host(host):
proc_map[host] = _remote_cylc_cmd(cmd, host=host, **kwargs)
proc_map[host] = cylc_server_cmd(cmd, host=host, **kwargs)
else:
proc_map[host] = run_cmd(['cylc'] + cmd, **kwargs)

Expand Down
19 changes: 12 additions & 7 deletions cylc/flow/network/ssh_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from cylc.flow.exceptions import ClientError, ClientTimeout
from cylc.flow.network.client_factory import CommsMeth
from cylc.flow.network import get_location
from cylc.flow.remote import _remote_cylc_cmd
from cylc.flow.remote import remote_cylc_cmd
from cylc.flow.workflow_files import load_contact_file, ContactFileFields


Expand Down Expand Up @@ -60,15 +60,20 @@ async def async_request(self, command, args=None, timeout=None):
try:
async with ascyncto(timeout):
cmd, ssh_cmd, login_sh, cylc_path, msg = self.prepare_command(
command, args, timeout)
proc = _remote_cylc_cmd(
command, args, timeout
)
platform = {
'ssh command': ssh_cmd,
'cylc path': cylc_path,
'use login shell': login_sh,
}
proc = remote_cylc_cmd(
cmd,
platform,
host=self.host,
stdin_str=msg,
ssh_cmd=ssh_cmd,
remote_cylc_path=cylc_path,
ssh_login_shell=login_sh,
capture_process=True)
capture_process=True
)
while True:
if proc.poll() is not None:
break
Expand Down
173 changes: 95 additions & 78 deletions cylc/flow/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,83 +226,51 @@ def construct_rsync_over_ssh_cmd(


def construct_ssh_cmd(
raw_cmd, platform, host, **kwargs
):
"""Build an SSH command for execution on a remote platform.
Constructs the SSH command according to the platform configuration.
See _construct_ssh_cmd for argument documentation.
"""
return _construct_ssh_cmd(
raw_cmd,
host=host,
ssh_cmd=platform['ssh command'],
remote_cylc_path=platform['cylc path'],
ssh_login_shell=platform['use login shell'],
**kwargs
)


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
raw_cmd,
platform,
host,
forward_x11=False,
stdin=False,
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.
platform (dict):
The Cylc job "platform" to run the command on. This is used
to determine the settings used e.g. "ssh command".
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:
Returns:
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)
command = shlex.split(platform['ssh command'])

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)
command.append(host)

# Pass CYLC_VERSION and optionally, CYLC_CONF_PATH & CYLC_UTC through.
command += ['env', quote(r'CYLC_VERSION=%s' % CYLC_VERSION)]
Expand All @@ -323,8 +291,7 @@ def _construct_ssh_cmd(
command.append(quote(r'TZ=UTC'))

# Use bash -l?
if ssh_login_shell is None:
ssh_login_shell = get_platform()['use login shell']
ssh_login_shell = 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
Expand All @@ -335,14 +302,11 @@ def _construct_ssh_cmd(
command += ['timeout', timeout]

# 'cylc' on the remote host
if not remote_cylc_path:
remote_cylc_path = get_platform()['cylc path']

remote_cylc_path = 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.
Expand All @@ -354,56 +318,109 @@ def _construct_ssh_cmd(
return command


def remote_cylc_cmd(cmd, platform, bad_hosts=None, **kwargs):
"""Execute a Cylc command on a remote platform.
def construct_cylc_server_ssh_cmd(
cmd,
host,
**kwargs,
):
"""Concenience command for building SSH commands for remote Cylc servers.
Uses the platform configuration to construct the command.
Build an SSH command that connects to the specified host using the
localhost platform config.
* To run commands on job platforms use construct_ssh_cmd.
* To run commands on Cylc servers (i.e. `[scheduler][run hosts]available`)
use this interface.
Runs a command via SSH using the configuration for the localhost platform.
This assumes the host you are connecting to shares the $HOME filesystem
with the localhost platform.
For arguments see construct_ssh_cmd.
Returns:
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.
See _construct_ssh_cmd for argument documentation.
"""
return _remote_cylc_cmd(
return construct_ssh_cmd(
cmd,
host=get_host_from_platform(platform, bad_hosts=bad_hosts),
ssh_cmd=platform['ssh command'],
remote_cylc_path=platform['cylc path'],
ssh_login_shell=platform['use login shell'],
**kwargs
get_platform(), # use localhost settings
host,
**kwargs,
)


def _remote_cylc_cmd(
cmd,
host=None,
stdin=None,
stdin_str=None,
ssh_login_shell=None,
ssh_cmd=None,
remote_cylc_path=None,
capture_process=False,
manage=False
def remote_cylc_cmd(
cmd,
platform,
bad_hosts=None,
host=None,
stdin=None,
stdin_str=None,
ssh_login_shell=None,
ssh_cmd=None,
remote_cylc_path=None,
capture_process=False,
manage=False
):
"""Execute a Cylc command on a remote platform.
See run_cmd and _construct_ssh_cmd for argument documentation.
Uses the platform configuration to construct the command.
See run_cmd and construct_ssh_cmd for argument documentation.
Returns:
subprocess.Popen or int - If capture_process=True, return the Popen
object if created successfully. Otherwise, return the exit code of the
remote command.
"""
if not host:
# no host selected => perform host selection from platform config
host = get_host_from_platform(platform, bad_hosts=bad_hosts)

return run_cmd(
_construct_ssh_cmd(
construct_ssh_cmd(
cmd,
platform,
host=host,
stdin=True if stdin_str else stdin,
ssh_login_shell=ssh_login_shell,
ssh_cmd=ssh_cmd,
remote_cylc_path=remote_cylc_path
),
stdin=stdin,
stdin_str=stdin_str,
capture_process=capture_process,
capture_status=True,
manage=manage
)


def cylc_server_cmd(cmd, host=None, **kwargs):
"""Convenience function for running commands on remote Cylc servers.
Executes a Cylc command on the specified host using localhost platform
config.
* To run commands on job platforms use remote_cylc_cmd.
* To run commands on Cylc servers (i.e. `[scheduler][run hosts]available`)
use this interface.
Runs a command via SSH using the configuration for the localhost platform.
This assumes the host you are connecting to shares the $HOME filesystem
with the localhost platform.
See run_cmd and construct_ssh_cmd for argument documentation.
Returns:
subprocess.Popen or int - If capture_process=True, return the Popen
object if created successfully. Otherwise, return the exit code of the
remote command.
"""
return remote_cylc_cmd(
cmd,
get_platform(), # use localhost settings
host=host,
**kwargs,
)
4 changes: 2 additions & 2 deletions cylc/flow/scheduler_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
icp_option,
)
from cylc.flow.pathutil import get_workflow_run_scheduler_log_path
from cylc.flow.remote import _remote_cylc_cmd
from cylc.flow.remote import cylc_server_cmd
from cylc.flow.scheduler import Scheduler, SchedulerError
from cylc.flow.scripts.common import cylc_header
from cylc.flow.workflow_files import (
Expand Down Expand Up @@ -393,7 +393,7 @@ def _distribute(host, workflow_id_raw, workflow_id):
cmd.append("--host=localhost")

# Re-invoke the command
_remote_cylc_cmd(cmd, host=host)
cylc_server_cmd(cmd, host=host)
sys.exit(0)


Expand Down
4 changes: 2 additions & 2 deletions cylc/flow/workflow_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
)
from cylc.flow.remote import (
DEFAULT_RSYNC_OPTS,
_construct_ssh_cmd,
construct_cylc_server_ssh_cmd,
construct_ssh_cmd,
)
from cylc.flow.terminal import parse_dirty_json
Expand Down Expand Up @@ -406,7 +406,7 @@ def _is_process_running(
metric = f'[["Process", {pid}]]'
if is_remote_host(host):
cmd = ['psutil']
cmd = _construct_ssh_cmd(cmd, host)
cmd = construct_cylc_server_ssh_cmd(cmd, host)
else:
cmd = ['cylc', 'psutil']
proc = Popen( # nosec
Expand Down

0 comments on commit 554ea02

Please sign in to comment.