From 2623eb44663ce65233a9560fe8763e77b4d1a30f Mon Sep 17 00:00:00 2001 From: Ben Fitzpatrick Date: Thu, 21 Jul 2016 17:01:47 +0100 Subject: [PATCH] #1872: comms layer to https --- README.md | 4 - bin/cylc | 2 +- bin/cylc-broadcast | 22 +- bin/cylc-dump | 6 +- bin/cylc-ext-trigger | 6 +- bin/cylc-get-suite-version | 6 +- bin/cylc-gui | 12 +- bin/cylc-hold | 14 +- bin/cylc-insert | 17 +- bin/cylc-kill | 10 +- bin/cylc-monitor | 6 +- bin/cylc-nudge | 6 +- bin/cylc-ping | 15 +- bin/cylc-poll | 12 +- bin/cylc-release | 10 +- bin/cylc-reload | 6 +- bin/cylc-remove | 13 +- bin/cylc-reset | 11 +- bin/cylc-scan | 15 +- bin/cylc-set-runahead | 8 +- bin/cylc-set-verbosity | 8 +- bin/cylc-show | 16 +- bin/cylc-spawn | 10 +- bin/cylc-stop | 27 +- bin/cylc-trigger | 28 +- dev/CommsTest/CommsTestClient.py | 37 -- dev/CommsTest/CommsTestServer.py | 21 -- dev/CommsTest/README | 35 -- doc/cug.tex | 92 ++--- doc/siterc.tex | 138 +++---- doc/suiterc.tex | 9 +- examples/remote/minimal/suite.rc | 8 +- lib/cylc/broadcast_report.py | 2 +- lib/cylc/cfgspec/globalcfg.py | 54 ++- lib/cylc/config.py | 6 +- lib/cylc/dump.py | 2 +- lib/cylc/gui/app_gcylc.py | 97 +++-- lib/cylc/gui/dbchooser.py | 20 +- lib/cylc/gui/gpanel.py | 2 +- lib/cylc/gui/gscan.py | 3 +- lib/cylc/gui/updater.py | 120 ++---- lib/cylc/gui/updater_dot.py | 2 +- lib/cylc/gui/updater_graph.py | 49 ++- lib/cylc/gui/updater_tree.py | 2 +- lib/cylc/network/__init__.py | 97 ++++- lib/cylc/network/connection_validator.py | 205 ----------- lib/cylc/network/daemon.py | 23 ++ lib/cylc/network/ext_trigger_client.py | 24 ++ lib/cylc/network/ext_trigger_server.py | 24 ++ lib/cylc/network/https/__init__.py | 0 lib/cylc/network/https/base_client.py | 346 ++++++++++++++++++ lib/cylc/network/https/base_server.py | 35 ++ .../network/{ => https}/client_reporter.py | 37 +- lib/cylc/network/https/daemon.py | 170 +++++++++ lib/cylc/network/https/ext_trigger_client.py | 78 ++++ .../ext_trigger_server.py} | 80 +--- lib/cylc/network/https/port_scan.py | 135 +++++++ .../network/https/suite_broadcast_client.py | 33 ++ .../suite_broadcast_server.py} | 63 +++- .../network/https/suite_command_client.py | 29 ++ .../network/https/suite_command_server.py | 197 ++++++++++ .../network/https/suite_identifier_client.py | 41 +++ .../suite_identifier_server.py} | 24 +- lib/cylc/network/https/suite_info_client.py | 39 ++ lib/cylc/network/https/suite_info_server.py | 134 +++++++ lib/cylc/network/https/suite_log_client.py | 30 ++ .../suite_log_server.py} | 27 +- lib/cylc/network/https/suite_state_client.py | 175 +++++++++ .../suite_state_server.py} | 193 ++-------- lib/cylc/network/https/task_msg_client.py | 38 ++ .../task_msg_server.py} | 33 +- lib/cylc/network/https/util.py | 77 ++++ lib/cylc/network/method.py | 18 + lib/cylc/network/port_scan.py | 168 +-------- lib/cylc/network/pyro_base.py | 221 ----------- lib/cylc/network/pyro_daemon.py | 89 ----- lib/cylc/network/suite_broadcast_client.py | 24 ++ lib/cylc/network/suite_broadcast_server.py | 24 ++ lib/cylc/network/suite_command.py | 90 ----- lib/cylc/network/suite_command_client.py | 24 ++ lib/cylc/network/suite_command_server.py | 24 ++ lib/cylc/network/suite_identifier_client.py | 24 ++ lib/cylc/network/suite_identifier_server.py | 24 ++ lib/cylc/network/suite_info.py | 80 ---- lib/cylc/network/suite_info_client.py | 25 ++ lib/cylc/network/suite_info_server.py | 24 ++ lib/cylc/network/suite_log_client.py | 24 ++ lib/cylc/network/suite_log_server.py | 24 ++ lib/cylc/network/suite_state_client.py | 30 ++ lib/cylc/network/suite_state_server.py | 23 ++ lib/cylc/network/task_msg_client.py | 24 ++ lib/cylc/network/task_msg_server.py | 24 ++ lib/cylc/option_parsers.py | 15 +- lib/cylc/registration.py | 312 +++++++++++----- lib/cylc/scheduler.py | 233 ++++++------ lib/cylc/task_message.py | 26 +- lib/cylc/task_pool.py | 93 ++--- lib/cylc/wallclock.py | 2 +- licences/LICENSE-MIT-PYRO | 28 -- tests/authentication/00-identity.t | 4 +- tests/authentication/05-full-control.t | 8 +- tests/authentication/07-back-compat.t | 172 --------- .../09-remote-suite-same-name.t | 6 +- tests/broadcast/09-remote/suite.rc | 2 +- tests/cylc-scan/01-hosts.t | 2 + tests/lib/bash/test_header | 2 +- tests/restart/broadcast/suite.rc | 2 +- tests/startup/02-state-summary.t | 2 +- .../validate/50-fail-authentication-hashes.t | 37 -- 109 files changed, 3064 insertions(+), 2266 deletions(-) delete mode 100755 dev/CommsTest/CommsTestClient.py delete mode 100755 dev/CommsTest/CommsTestServer.py delete mode 100644 dev/CommsTest/README delete mode 100644 lib/cylc/network/connection_validator.py create mode 100644 lib/cylc/network/daemon.py create mode 100644 lib/cylc/network/ext_trigger_client.py create mode 100644 lib/cylc/network/ext_trigger_server.py create mode 100644 lib/cylc/network/https/__init__.py create mode 100644 lib/cylc/network/https/base_client.py create mode 100644 lib/cylc/network/https/base_server.py rename lib/cylc/network/{ => https}/client_reporter.py (78%) create mode 100644 lib/cylc/network/https/daemon.py create mode 100644 lib/cylc/network/https/ext_trigger_client.py rename lib/cylc/network/{ext_trigger.py => https/ext_trigger_server.py} (56%) create mode 100644 lib/cylc/network/https/port_scan.py create mode 100644 lib/cylc/network/https/suite_broadcast_client.py rename lib/cylc/network/{suite_broadcast.py => https/suite_broadcast_server.py} (89%) create mode 100644 lib/cylc/network/https/suite_command_client.py create mode 100644 lib/cylc/network/https/suite_command_server.py create mode 100644 lib/cylc/network/https/suite_identifier_client.py rename lib/cylc/network/{suite_identifier.py => https/suite_identifier_server.py} (75%) create mode 100644 lib/cylc/network/https/suite_info_client.py create mode 100644 lib/cylc/network/https/suite_info_server.py create mode 100644 lib/cylc/network/https/suite_log_client.py rename lib/cylc/network/{suite_log.py => https/suite_log_server.py} (79%) create mode 100644 lib/cylc/network/https/suite_state_client.py rename lib/cylc/network/{suite_state.py => https/suite_state_server.py} (58%) create mode 100644 lib/cylc/network/https/task_msg_client.py rename lib/cylc/network/{task_msgqueue.py => https/task_msg_server.py} (58%) create mode 100644 lib/cylc/network/https/util.py create mode 100644 lib/cylc/network/method.py delete mode 100644 lib/cylc/network/pyro_base.py delete mode 100644 lib/cylc/network/pyro_daemon.py create mode 100644 lib/cylc/network/suite_broadcast_client.py create mode 100644 lib/cylc/network/suite_broadcast_server.py delete mode 100644 lib/cylc/network/suite_command.py create mode 100644 lib/cylc/network/suite_command_client.py create mode 100644 lib/cylc/network/suite_command_server.py create mode 100644 lib/cylc/network/suite_identifier_client.py create mode 100644 lib/cylc/network/suite_identifier_server.py delete mode 100644 lib/cylc/network/suite_info.py create mode 100644 lib/cylc/network/suite_info_client.py create mode 100644 lib/cylc/network/suite_info_server.py create mode 100644 lib/cylc/network/suite_log_client.py create mode 100644 lib/cylc/network/suite_log_server.py create mode 100644 lib/cylc/network/suite_state_client.py create mode 100644 lib/cylc/network/suite_state_server.py create mode 100644 lib/cylc/network/task_msg_client.py create mode 100644 lib/cylc/network/task_msg_server.py delete mode 100644 licences/LICENSE-MIT-PYRO delete mode 100644 tests/authentication/07-back-compat.t delete mode 100755 tests/validate/50-fail-authentication-hashes.t diff --git a/README.md b/README.md index febed8daf52..26abc729655 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,6 @@ Licences for non-cylc work included in this distribution can be found in the * `lib/isodatetime/`: Unmodified external software library released under the LGPL license. See [metomi/isodatetime](https://github.com/metomi/isodatetime). - * `lib/Pyro/`: - External software library released under the MIT license. - Minor modification based on version 3.16. - See [Pyro 3.16](https://pypi.python.org/pypi/Pyro). * `lib/xdot.py`: External software released under the LGPL license. Modifications based on version 0.6. See diff --git a/bin/cylc b/bin/cylc index aa6ffb63ed4..247babd8cda 100755 --- a/bin/cylc +++ b/bin/cylc @@ -36,7 +36,7 @@ try: os.getcwd() except OSError as exc: # The current working directory has been deleted (or filesystem - # problems of some kind...). This results in Pyro not being found, + # problems of some kind...). This results in cherrypy not being found, # immediately below. We cannot just chdir to $HOME as gcylc does # because that would break relative directory path command arguments # (cylc reg SUITE PATH). diff --git a/bin/cylc-broadcast b/bin/cylc-broadcast index c11c5633675..b4a2ba5fff6 100755 --- a/bin/cylc-broadcast +++ b/bin/cylc-broadcast @@ -71,7 +71,7 @@ import cylc.flags from cylc.broadcast_report import ( get_broadcast_change_report, get_broadcast_bad_options_report) from cylc.option_parsers import CylcOptionParser as COP -from cylc.network.suite_broadcast import BroadcastClient +from cylc.network.suite_broadcast_client import BroadcastClient from cylc.print_tree import print_tree from cylc.task_id import TaskID from cylc.cfgspec.suite import SPEC, upg @@ -107,7 +107,7 @@ def get_rdict(left, right=None): def main(): - parser = COP(__doc__, pyro=True) + parser = COP(__doc__, comms=True) parser.add_option( "-t", "--tag", metavar="CYCLE_POINT", @@ -192,7 +192,7 @@ def main(): pass pclient = BroadcastClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) @@ -202,7 +202,7 @@ def main(): name, point_string = TaskID.split(options.showtask) except ValueError: parser.error("TASKID must be " + TaskID.SYNTAX) - settings = pclient.broadcast('get', options.showtask) + settings = pclient.broadcast('get', task_id=options.showtask) padding = get_padding(settings) * ' ' if options.raw: print str(settings) @@ -212,7 +212,9 @@ def main(): if options.clear: modified_settings, bad_options = pclient.broadcast( - 'clear', options.point_strings, options.namespaces) + 'clear', point_strings=options.point_strings, + namespaces=options.namespaces + ) if modified_settings: print get_broadcast_change_report( modified_settings, is_cancel=True) @@ -220,7 +222,7 @@ def main(): if options.expire: modified_settings, bad_options = pclient.broadcast( - 'expire', options.expire) + 'expire', cutoff=options.expire) if modified_settings: print get_broadcast_change_report( modified_settings, is_cancel=True) @@ -249,7 +251,9 @@ def main(): validate(setting, SPEC['runtime']['__MANY__']) settings.append(setting) modified_settings, bad_options = pclient.broadcast( - 'clear', point_strings, namespaces, settings) + 'clear', point_strings=point_strings, + namespaces=namespaces, cancel_settings=settings + ) if modified_settings: print get_broadcast_change_report( modified_settings, is_cancel=True) @@ -270,7 +274,9 @@ def main(): validate(setting, SPEC['runtime']['__MANY__']) settings.append(setting) modified_settings, bad_options = pclient.broadcast( - 'put', point_strings, namespaces, settings) + 'put', point_strings=point_strings, + namespaces=namespaces, settings=settings + ) print get_broadcast_change_report(modified_settings) sys.exit(get_broadcast_bad_options_report(bad_options, is_set=True)) diff --git a/bin/cylc-dump b/bin/cylc-dump index 4bfcf8c5b6e..c91380d76bc 100755 --- a/bin/cylc-dump +++ b/bin/cylc-dump @@ -40,13 +40,13 @@ if '--use-ssh' in sys.argv[1:]: sys.exit(0) import cylc.flags -from cylc.network.suite_state import StateSummaryClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_state_client import StateSummaryClient from cylc.dump import dump_to_stdout def main(): - parser = COP(__doc__, pyro=True, noforce=True) + parser = COP(__doc__, comms=True, noforce=True) parser.add_option( "-g", "--global", help="Global information only.", @@ -78,7 +78,7 @@ def main(): try: pclient = StateSummaryClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) # get state summary, task names, cycle points diff --git a/bin/cylc-ext-trigger b/bin/cylc-ext-trigger index 6a050405052..5df62acce49 100755 --- a/bin/cylc-ext-trigger +++ b/bin/cylc-ext-trigger @@ -40,12 +40,12 @@ import sys import cylc.flags from cylc.option_parsers import CylcOptionParser as COP -from cylc.network.ext_trigger import ExtTriggerClient +from cylc.network.ext_trigger_client import ExtTriggerClient def main(): parser = COP( - __doc__, pyro=True, + __doc__, comms=True, argdoc=[("REG", "Suite name"), ("MSG", "External trigger message"), ("ID", "Unique trigger ID")]) @@ -70,7 +70,7 @@ def main(): print 'Send to suite %s: "%s" (%s)' % (suite, event_msg, event_id) pclient = ExtTriggerClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) diff --git a/bin/cylc-get-suite-version b/bin/cylc-get-suite-version index add68dd7805..0531bd06848 100755 --- a/bin/cylc-get-suite-version +++ b/bin/cylc-get-suite-version @@ -32,18 +32,18 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.option_parsers import CylcOptionParser as COP from cylc.task_id import TaskID -from cylc.network.suite_info import SuiteInfoClient +from cylc.network.suite_info_client import SuiteInfoClient from cylc.cfgspec.globalcfg import GLOBAL_CFG def main(): - parser = COP(__doc__, pyro=True, argdoc=[('REG', 'Suite name')]) + parser = COP(__doc__, comms=True, argdoc=[('REG', 'Suite name')]) (options, args) = parser.parse_args() suite = args[0] pclient = SuiteInfoClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) print pclient.get_info('get_cylc_version') diff --git a/bin/cylc-gui b/bin/cylc-gui index 73647df07dc..1a7a9b6fdfc 100755 --- a/bin/cylc-gui +++ b/bin/cylc-gui @@ -55,7 +55,7 @@ def main(): sys.path.append( os.path.dirname(os.path.realpath(os.path.abspath(__file__))) + '/../') - parser = COP(__doc__, pyro=True, noforce=True, jset=True, + parser = COP(__doc__, comms=True, noforce=True, jset=True, argdoc=[('[REG]', 'Suite name')]) parser.add_option( @@ -78,11 +78,9 @@ def main(): warnings.filterwarnings('ignore', 'use the new', Warning) from cylc.gui.app_gcylc import ControlApp - # Make current working directory be $HOME. Otherwise (1) if the user - # attempts to start gcylc from a CWD that has been removed, Pyro will - # not be importable below; and (2) if the CWD gets removed later while - # gcylc is running, subprocesses spawned by gcylc will fail when they - # attempt to determine their CWD. + # Make current working directory be $HOME. Otherwise if the CWD gets + # removed later while gcylc is running, subprocesses spawned by gcylc will + # fail when they attempt to determine their CWD. os.chdir(os.environ['HOME']) gtk.settings_get_default().set_long_property( @@ -98,7 +96,7 @@ def main(): suite = None app = ControlApp( suite, options.db, options.owner, options.host, - options.port, options.pyro_timeout, + options.port, options.comms_timeout, load_template_vars(options.templatevars, options.templatevars_file), options.restricted) gtk.main() diff --git a/bin/cylc-hold b/bin/cylc-hold index 22d89130ba7..17a7a86fa3c 100755 --- a/bin/cylc-hold +++ b/bin/cylc-hold @@ -35,13 +35,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ("REG", "Suite name"), ('[TASKID ...]', 'Task identifiers')]) @@ -62,15 +62,17 @@ def main(): else: prompt('Hold suite %s' % suite, options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) if args: - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('hold_task', items, compat) + items = parser.parse_multitask_compat(options, args) + pclient.put_command('hold_tasks', items=items) elif options.hold_point_string: pclient.put_command( - 'hold_after_point_string', options.hold_point_string) + 'hold_after_point_string', + point_string=options.hold_point_string + ) else: pclient.put_command('hold_suite') diff --git a/bin/cylc-insert b/bin/cylc-insert index 2bfe180a95c..082d8c5ad66 100755 --- a/bin/cylc-insert +++ b/bin/cylc-insert @@ -42,14 +42,14 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient from cylc.task_id import TaskID def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ("REG", "Suite name"), ('TASKID [...]', 'Task identifier')]) @@ -68,14 +68,13 @@ def main(): if (options.multitask_compat and len(args) in [2, 3] and all(["/" not in arg for arg in args]) and all(["." not in arg for arg in args[1:]])): - items, compat = (args[0], args[1]) + items = [(args[0] + "." + args[1])] if len(args) == 3: options.stop_point_string = args[2] prompt( - 'Insert %s at %s in %s' % (items, compat, suite), - options.force) + 'Insert %s in %s' % (items, suite), options.force) else: - items, compat = (args, None) + items = args for i, item in enumerate(items): if not TaskID.is_valid_id_for_insert(item): sys.exit('ERROR: "%s": invalid task ID (argument %d)' % ( @@ -83,12 +82,14 @@ def main(): prompt('Insert %s in %s' % (items, suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) pclient.put_command( - 'insert_task', items, compat, None, options.stop_point_string) + 'insert_tasks', items=items, + stop_point_string=options.stop_point_string + ) if __name__ == "__main__": diff --git a/bin/cylc-kill b/bin/cylc-kill index d1273e51b96..497e1a8d5b0 100755 --- a/bin/cylc-kill +++ b/bin/cylc-kill @@ -33,13 +33,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ('REG', 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -52,11 +52,11 @@ def main(): else: prompt('Kill ALL tasks in %s' % (suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('kill_tasks', items, compat) + items = parser.parse_multitask_compat(options, args) + pclient.put_command('kill_tasks', items=items) if __name__ == "__main__": diff --git a/bin/cylc-monitor b/bin/cylc-monitor index 2597fc74065..bd5f9dd4a49 100755 --- a/bin/cylc-monitor +++ b/bin/cylc-monitor @@ -33,7 +33,7 @@ from time import sleep from parsec.OrderedDict import OrderedDict from cylc.option_parsers import CylcOptionParser as COP -from cylc.network.suite_state import ( +from cylc.network.suite_state_client import ( SUITE_STATUS_SPLIT_REC, get_suite_status_string, StateSummaryClient, SuiteStillInitialisingError) from cylc.wallclock import get_time_string_from_unix_time @@ -50,7 +50,7 @@ class SuiteMonitor(object): """cylc [info] monitor [OPTIONS] ARGS A terminal-based live suite monitor. Exit with 'Ctrl-C'.""", - pyro=True, noforce=True) + comms=True, noforce=True) self.parser.add_option( "-a", "--align", @@ -107,7 +107,7 @@ A terminal-based live suite monitor. Exit with 'Ctrl-C'.""", len_header = sum(len(s) for s in TASK_STATUSES_ORDERED) self.pclient = StateSummaryClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db) while True: diff --git a/bin/cylc-nudge b/bin/cylc-nudge index 0d885ef6955..232f2411fb0 100755 --- a/bin/cylc-nudge +++ b/bin/cylc-nudge @@ -37,18 +37,18 @@ if '--use-ssh' in sys.argv[1:]: sys.exit(0) import cylc.flags -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): - parser = COP(__doc__, pyro=True) + parser = COP(__doc__, comms=True) (options, args) = parser.parse_args() suite = args[0] pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) diff --git a/bin/cylc-ping b/bin/cylc-ping index 70a55f8d89f..9758fca85ef 100755 --- a/bin/cylc-ping +++ b/bin/cylc-ping @@ -31,13 +31,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.option_parsers import CylcOptionParser as COP from cylc.task_id import TaskID -from cylc.network.suite_info import SuiteInfoClient +from cylc.network.suite_info_client import SuiteInfoClientAnon from cylc.cfgspec.globalcfg import GLOBAL_CFG def main(): parser = COP( - __doc__, pyro=True, + __doc__, comms=True, argdoc=[('REG', 'Suite name'), ('[TASK]', 'Task ' + TaskID.SYNTAX)]) parser.add_option( @@ -48,18 +48,17 @@ def main(): (options, args) = parser.parse_args() if options.print_ports: - base = GLOBAL_CFG.get(['pyro', 'base port']) - range = GLOBAL_CFG.get(['pyro', 'maximum number of ports']) + base = GLOBAL_CFG.get(['comms', 'base port']) + range = GLOBAL_CFG.get(['comms', 'maximum number of ports']) print base, '<= port <=', base + range sys.exit(0) suite = args[0] - pclient = SuiteInfoClient( - suite, options.owner, options.host, options.pyro_timeout, + pclient = SuiteInfoClientAnon( + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - pclient.set_use_scan_hash() # cylc ping SUITE pclient.get_info('ping_suite') # (no need to check the result) @@ -72,7 +71,7 @@ def main(): task_id = args[1] if not TaskID.is_valid_id(task_id): sys.exit("Invalid task ID: " + task_id) - success, msg = pclient.get_info('ping_task', task_id) + success, msg = pclient.get_info('ping_task', task_id=task_id) if not success: sys.exit('ERROR: ' + msg) diff --git a/bin/cylc-poll b/bin/cylc-poll index dda37374430..22a9ca1c2d3 100755 --- a/bin/cylc-poll +++ b/bin/cylc-poll @@ -24,7 +24,7 @@ To poll one or more tasks, "cylc poll REG TASKID"; to poll all active tasks: "cylc poll REG". Note that automatic job polling can used to track task status on task hosts -that do not allow any communication by RPC (pyro) or ssh back to the suite host +that do not allow any communication by HTTPS or ssh back to the suite host - see site/user config file documentation. Polling is also done automatically on restarting a suite, for any tasks that @@ -40,13 +40,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ('REG', 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -59,11 +59,11 @@ def main(): else: prompt('Poll ALL tasks in %s' % (suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('poll_tasks', items, compat) + items = parser.parse_multitask_compat(options, args) + pclient.put_command('poll_tasks', items=items) if __name__ == "__main__": diff --git a/bin/cylc-release b/bin/cylc-release index 15b84693a14..f89e39741ef 100755 --- a/bin/cylc-release +++ b/bin/cylc-release @@ -34,13 +34,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ("REG", 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -53,12 +53,12 @@ def main(): else: prompt('Release suite %s' % suite, options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) if args: - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('release_task', items, compat) + items = parser.parse_multitask_compat(options, args) + pclient.put_command('release_tasks', items=items) else: pclient.put_command('release_suite') diff --git a/bin/cylc-reload b/bin/cylc-reload index 5b67cd8cac9..39c592f4e67 100755 --- a/bin/cylc-reload +++ b/bin/cylc-reload @@ -47,19 +47,19 @@ if '--use-ssh' in sys.argv[1:]: sys.exit(0) import cylc.flags -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient from cylc.prompt import prompt def main(): - parser = COP(__doc__, pyro=True) + parser = COP(__doc__, comms=True) (options, args) = parser.parse_args() suite = args[0] prompt('Reload %s' % suite, options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) pclient.put_command('reload_suite') diff --git a/bin/cylc-remove b/bin/cylc-remove index c7ec588cdca..2e68c0391ad 100755 --- a/bin/cylc-remove +++ b/bin/cylc-remove @@ -33,13 +33,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ("REG", "Suite name"), ('TASKID [...]', 'Task identifiers')]) @@ -54,13 +54,12 @@ def main(): suite = args.pop(0) prompt('remove task(s) %s in %s' % (args, suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('remove_task', items, compat, - None, not options.no_spawn) - + items = parser.parse_multitask_compat(options, args) + pclient.put_command('remove_tasks', items=items, + spawn=(not options.no_spawn)) if __name__ == "__main__": try: diff --git a/bin/cylc-reset b/bin/cylc-reset index f07f250174e..ee331f692e3 100755 --- a/bin/cylc-reset +++ b/bin/cylc-reset @@ -38,14 +38,14 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient from cylc.task_state import TASK_STATUSES_CAN_RESET_TO def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ('REG', 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -77,11 +77,12 @@ def main(): prompt('Reset task(s) %s in %s' % (args, suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('reset_task_state', items, compat, options.state) + items = parser.parse_multitask_compat(options, args) + pclient.put_command( + 'reset_task_states', items=items, state=options.state) if __name__ == "__main__": diff --git a/bin/cylc-scan b/bin/cylc-scan index 6d7c9e4f34d..2a177d7cbd4 100755 --- a/bin/cylc-scan +++ b/bin/cylc-scan @@ -37,7 +37,7 @@ Passphrases are still required to get identity information from older suites (cylc <= 6.5.0), otherwise you'll see "connection denied (security reasons)". WARNING: a suite suspended with Ctrl-Z will cause port scans to hang until the -connection times out (see --pyro-timeout).""" +connection times out (see --comms-timeout).""" import sys if "--use-ssh" in sys.argv[1:]: @@ -71,7 +71,7 @@ def main(): """Implement "cylc scan".""" parser = COP( __doc__, - pyro=True, + comms=True, noforce=True, argdoc=[( "[HOSTS ...]", "Hosts to scan instead of the configured hosts.")] @@ -130,10 +130,10 @@ def main(): action="store_true", default=False, dest="print_ports") parser.add_option( - "--pyro-timeout", metavar="SEC", + "--comms-timeout", "--pyro-timeout", metavar="SEC", help="Set a timeout for network connections " "to running suites. The default is 60 seconds.", - action="store", default=60, dest="pyro_timeout") + action="store", default=60, dest="comms_timeout") parser.add_option( "--old", "--old-format", @@ -149,8 +149,9 @@ def main(): options, args = parser.parse_args() if options.print_ports: - base = GLOBAL_CFG.get(["pyro", "base port"]) - max_num_ports = GLOBAL_CFG.get(["pyro", "maximum number of ports"]) + base = GLOBAL_CFG.get(["communication", "base port"]) + max_num_ports = GLOBAL_CFG.get( + ["communication", "maximum number of ports"]) print base, "<= port <=", base + max_num_ports sys.exit(0) @@ -206,7 +207,7 @@ def main(): state_legend = state_legend.rstrip() skip_one = True - for result in scan_all(args, options.db, options.pyro_timeout): + for result in scan_all(args, options.db, options.comms_timeout): host, scan_result = result try: port, suite_identity = scan_result diff --git a/bin/cylc-set-runahead b/bin/cylc-set-runahead index 603a40bf3d2..40439a4ad18 100755 --- a/bin/cylc-set-runahead +++ b/bin/cylc-set-runahead @@ -34,13 +34,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, + __doc__, comms=True, argdoc=[('REG', 'Suite name'), ('[HOURS]', 'Runahead limit (default: no limit)')]) @@ -52,14 +52,14 @@ def main(): runahead = args[1] pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) if runahead: prompt('Change runahead limit in %s to %s' % (suite, runahead), options.force) - pclient.put_command('set_runahead', runahead) + pclient.put_command('set_runahead', interval=runahead) else: # no limit! prompt('Change runahead limit in %s to NO LIMIT' % suite, diff --git a/bin/cylc-set-verbosity b/bin/cylc-set-verbosity index ea5b1408430..007a80db4b9 100755 --- a/bin/cylc-set-verbosity +++ b/bin/cylc-set-verbosity @@ -33,8 +33,8 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient LOGGING_LVL_OF = { "INFO": INFO, @@ -48,7 +48,7 @@ LOGGING_LVL_OF = { def main(): parser = COP( - __doc__, pyro=True, + __doc__, comms=True, argdoc=[ ('REG', 'Suite name'), ('LEVEL', ', '.join(LOGGING_LVL_OF.keys())) @@ -66,11 +66,11 @@ def main(): prompt("Set logging level to %s in %s" % (priority_str, suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - pclient.put_command('set_verbosity', priority) + pclient.put_command('set_verbosity', level=priority) if __name__ == "__main__": diff --git a/bin/cylc-show b/bin/cylc-show index 1f084e3ec7c..d1facfd61f2 100755 --- a/bin/cylc-show +++ b/bin/cylc-show @@ -30,14 +30,15 @@ if '--use-ssh' in sys.argv[1:]: sys.exit(0) import cylc.flags -from cylc.network.suite_info import SuiteInfoClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_info_client import ( + SuiteInfoClient, SuiteInfoClientAnon) from cylc.task_id import TaskID def main(): parser = COP( - __doc__, pyro=True, noforce=True, + __doc__, comms=True, noforce=True, argdoc=[('REG', 'Suite name'), ('[' + TaskID.SYNTAX_OPT_POINT + ']', 'Task name or ID')]) @@ -45,9 +46,10 @@ def main(): suite = args[0] pclient = SuiteInfoClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, - print_uuid=options.print_uuid) + print_uuid=options.print_uuid + ) if len(args) == 1: # Print suite info. @@ -67,14 +69,16 @@ def main(): # Print task instance info. task_id = arg - info = pclient.get_info('get_task_info', name) + info = pclient.get_info('get_task_info', name=name) if not info: sys.exit("ERROR: task not found: %s" % name) for key, value in sorted(info.items(), reverse=True): print "%s: %s" % (key, value or "(not given)") if point_string is not None: - result = pclient.get_info('get_task_requisites', name, point_string) + result = pclient.get_info('get_task_requisites', + name=name, + point_string=point_string) if not result: sys.exit("ERROR: task instance not found: %s" % task_id) diff --git a/bin/cylc-spawn b/bin/cylc-spawn index 988def82e9b..800928f22ae 100755 --- a/bin/cylc-spawn +++ b/bin/cylc-spawn @@ -32,13 +32,13 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient def main(): parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ('REG', 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -49,11 +49,11 @@ def main(): prompt('Spawn task(s) %s in %s' % (args, suite), options.force) pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) - items, compat = parser.parse_multitask_compat(options, args) - pclient.put_command('spawn_tasks', items, compat) + items = parser.parse_multitask_compat(options, args) + pclient.put_command('spawn_tasks', items=items) if __name__ == "__main__": diff --git a/bin/cylc-stop b/bin/cylc-stop index c815dbb97b3..ed5b2602434 100755 --- a/bin/cylc-stop +++ b/bin/cylc-stop @@ -48,9 +48,9 @@ if '--use-ssh' in sys.argv[1:]: import cylc.flags from cylc.prompt import prompt from cylc.task_id import TaskID -from cylc.network.suite_command import SuiteCommandClient -from cylc.network.suite_info import SuiteInfoClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient +from cylc.network.suite_info_client import SuiteInfoClient from cylc.command_polling import poller @@ -59,8 +59,9 @@ class stop_poller(poller): def load(self): self.pclient = SuiteInfoClient( - self.args['suite'], self.args['owner'], self.args['host'], - self.args['pyro_timeout'], self.args['port'], self.args['db']) + self.args['suite'], owner=self.args['owner'], + host=self.args['host'], timeout=self.args['comms_timeout'], + port=self.args['port'], db=self.args['db']) def check(self): # return True if suite has stopped (success) else False @@ -76,7 +77,7 @@ class stop_poller(poller): def main(): parser = COP( - __doc__, pyro=True, + __doc__, comms=True, argdoc=[("REG", "Suite name"), ("[STOP]", """a/ task POINT (cycle point), or b/ ISO 8601 date-time (clock time), or @@ -117,7 +118,7 @@ def main(): parser.error("ERROR: --kill is not compatible with --now") pclient = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) @@ -129,7 +130,7 @@ def main(): 'suite': suite, 'owner': options.owner, 'host': options.host, - 'pyro_timeout': options.pyro_timeout, + 'comms_timeout': options.comms_timeout, 'port': options.port, 'db': options.db } @@ -140,28 +141,30 @@ def main(): 'Set shutdown at wall clock %s for %s' % ( options.wall_clock, suite), options.force) - pclient.put_command('set_stop_after_clock_time', options.wall_clock) + pclient.put_command('set_stop_after_clock_time', + datetime_string=options.wall_clock) elif shutdown_at and TaskID.is_valid_id(shutdown_arg): # STOP argument detected prompt( 'Set shutdown after task %s for %s' % (shutdown_arg, suite), options.force) - pclient.put_command('set_stop_after_task', shutdown_arg) + pclient.put_command('set_stop_after_task', task_id=shutdown_arg) elif shutdown_at: # not a task ID, may be a cycle point prompt( 'Set shutdown at cycle point %s for %s' % (shutdown_arg, suite), options.force) - pclient.put_command('set_stop_after_point', shutdown_arg) + pclient.put_command('set_stop_after_point', point_string=shutdown_arg) elif options.now > 1: prompt('Shut down and terminate %s now' % suite, options.force) - pclient.put_command('stop_now', True) + pclient.put_command('stop_now', kill_active_tasks=True) elif options.now: prompt('Shut down %s now' % suite, options.force) pclient.put_command('stop_now') else: prompt('Shut down %s' % suite, options.force) - pclient.put_command('set_stop_cleanly', options.kill) + pclient.put_command('set_stop_cleanly', + kill_active_tasks=options.kill) if int(options.max_polls) > 0: # (test to avoid the "nothing to do" warning for # --max-polls=0) diff --git a/bin/cylc-trigger b/bin/cylc-trigger index dfc72f4bee8..0eb9073cb76 100755 --- a/bin/cylc-trigger +++ b/bin/cylc-trigger @@ -50,9 +50,9 @@ import subprocess import cylc.flags from cylc.prompt import prompt -from cylc.network.suite_command import SuiteCommandClient -from cylc.network.suite_info import SuiteInfoClient from cylc.option_parsers import CylcOptionParser as COP +from cylc.network.suite_command_client import SuiteCommandClient +from cylc.network.suite_info_client import SuiteInfoClient from cylc.cfgspec.globalcfg import GLOBAL_CFG from cylc.task_id import TaskID @@ -60,7 +60,7 @@ from cylc.task_id import TaskID def main(): """CLI for "cylc trigger".""" parser = COP( - __doc__, pyro=True, multitask=True, + __doc__, comms=True, multitask=True, argdoc=[ ('REG', 'Suite name'), ('[TASKID ...]', 'Task identifiers')]) @@ -84,28 +84,26 @@ def main(): prompt(msg, options.force) cmd_client = SuiteCommandClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=options.set_uuid, print_uuid=options.print_uuid) if options.edit_run: - items, compat = parser.parse_multitask_compat(options, args) - if compat: - task_id = TaskID.get(items, compat) - else: - task_id = args[0] + items = parser.parse_multitask_compat(options, args) + task_id = items[0] # Check that TASK is a unique task. info_client = SuiteInfoClient( - suite, options.owner, options.host, options.pyro_timeout, + suite, options.owner, options.host, options.comms_timeout, options.port, options.db, my_uuid=cmd_client.my_uuid) - success, msg = info_client.get_info('ping_task', task_id, True) + success, msg = info_client.get_info( + 'ping_task', task_id=task_id, exists_only=True) if not success: sys.exit('ERROR: %s' % msg) # Get the job filename from the suite daemon - the task cycle point may # need standardising to the suite cycle point format. jobfile_path, compat = info_client.get_info( - 'get_task_jobfile_path', task_id) + 'get_task_jobfile_path', task_id=task_id) if not jobfile_path: sys.exit('ERROR: task not found') @@ -124,7 +122,7 @@ def main(): old_mtime = None # Tell the suite daemon to generate the job file. - cmd_client.put_command('dry_run_task', [task_id]) + cmd_client.put_command('dry_run_tasks', items=[task_id]) # Wait for the new job file to be written. Use mtime because the same # file could potentially exist already, left from a previous run. @@ -194,8 +192,8 @@ def main(): sys.exit(0) # Trigger the task proxy(s). - items, compat = parser.parse_multitask_compat(options, args) - cmd_client.put_command('trigger_task', items, compat) + items = parser.parse_multitask_compat(options, args) + cmd_client.put_command('trigger_tasks', items=items) if __name__ == "__main__": diff --git a/dev/CommsTest/CommsTestClient.py b/dev/CommsTest/CommsTestClient.py deleted file mode 100755 index 11ffaf24780..00000000000 --- a/dev/CommsTest/CommsTestClient.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python - -# See CommsTest/README - -import os, sys -import Pyro.core - -if len(sys.argv) != 3: - print "USAGE: CommsTestClient.py HOST PORT" - sys.exit(1) - -host = sys.argv[1] -port = sys.argv[2] - -try: - proxy = Pyro.core.getProxyForURI("PYROLOC://" + host + ":" + port + "/report") -except Pyro.errors.URIError, x: - raise SystemExit( 'getProxyForURI URI ERROR, ' + host + ': ' + str(x) ) -except Exception, x: - raise SystemExit( 'ERROR: ' + str(x) ) - -try: - print proxy.get_report( os.environ["USER"] ) -except Pyro.errors.ProtocolError, x: - # Examples: - # 1/ "connection failed" => no pyro server at this port - # 2/ "incompatible protocol version => non-pyro server found (e.g. - # ssh at port 22) - raise SystemExit( 'Call via Pyro Proxy: Protocol ERROR, ' + str(x) ) -except Pyro.errors.NamingError, x: - # Example: - # ("no object found by this name", "report"): - # A pyro server (the intended one OR another one) but the requested - # object is not registered by it - raise SystemExit( 'Call via Pyro Proxy: Naming ERROR, ' + str(x) ) -except Exception, x: - raise SystemExit( 'Call via Pyro Proxy: ERROR, ' + str(x) ) diff --git a/dev/CommsTest/CommsTestServer.py b/dev/CommsTest/CommsTestServer.py deleted file mode 100755 index f1e23a34428..00000000000 --- a/dev/CommsTest/CommsTestServer.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python - -import sys -import socket -import Pyro.core - -if len(sys.argv) != 1: - print "USAGE: CommsTestServer.py" - print "(no options or arguments)" - sys.exit(1) - -my_host = socket.getfqdn() - -class Report(Pyro.core.ObjBase): - def get_report(self, name): - return "***Hello " + name + ", this is the CommsTest server on " + my_host + "***" - -daemon=Pyro.core.Daemon() -uri=daemon.connect( Report(),"report") -print "[CommsTest Server listening on", my_host + ":" + str(daemon.port) + "]" -daemon.requestLoop() diff --git a/dev/CommsTest/README b/dev/CommsTest/README deleted file mode 100644 index ac32fb93ddb..00000000000 --- a/dev/CommsTest/README +++ /dev/null @@ -1,35 +0,0 @@ -Hilary Oliver, NIWA, 2012 -cylc metascheduler. - -CommsTestServer.py and CommsTestClient.py are minimal Pyro server and -client programs that communicate over the network in the same way that -cylc clients (tasks, commands, and GUIs) communicate with cylc servers -(running suites). EXCEPT THAT UNLIKE CYLC THEY DON'T USE CONNECTION -AUTHENTICATION. This code was adapted from the simple "Joke Generator" -Pyro3 example by Irmen de Jong (adapted to avoid the Pyro Nameserver, -which is not used by cylc, in particular). - -Pyro listens and connects to network sockets on ports starting at 7766. - -If you cannot get cylc tasks to communicate with their parent suite you -may be able to use this example to help elucidate the problem, which is -likely to do with your local network or firewall configuration (e.g. -the default Pyro ports, 7766 through 7866, may be blocked - but note -that cylc may be configured to use a non-default port range). - -________________________________________________________________________ -ON THE CommsTest SERVER HOST - -bob> hostname - foo.bar.baz -bob> CommsTestServer.py - [CommsTestServer listening on foo.bar.baz:7766] - -________________________________________________________________________ -ON THE CommsTest CLIENT HOST -(first try running the client on the server host, then elsewhere) - -bob> hostname - waz.bar.baz -bob> CommsTestClient.py foo.bar.baz 7766 - ***Hello bob, this is the CommsTest server on foo.bar.baz*** diff --git a/doc/cug.tex b/doc/cug.tex index f31703d6ce7..111f24c5147 100644 --- a/doc/cug.tex +++ b/doc/cug.tex @@ -441,11 +441,14 @@ \section{Required Software} \item ImageMagick (for image scaling) \end{myitemize} -Note: Older versions of cylc require Pyro-3 (Python Remote Objects). Current -version of cylc includes a modified version of Pyro 3.16 in its distribution. -Pyro is used for network communication between server processes (cylc suites) -and client programs (running tasks, the gcylc GUI, user commands). \newline -\url{https://pypi.python.org/pypi/Pyro/} +Cylc includes a slightly modified version of cherrypy 6.0.2, a pure Python +HTTP framework that we use as a web server for communication +between server processes (cylc suites) and client programs (running tasks, +the gcylc GUI, user commands). Client communication is done via the +Python requests library if available (recommended) or via pure Python via +urllib2. +\newline \url{http://www.cherrypy.org/} +\newline \url{http://docs.python-requests.org/} Finally, cylc makes heavy use of Python {\em ordered dictionary} data structures. Significant speedup in parsing large suites can be had @@ -525,7 +528,6 @@ \subsection{Install The External Dependencies} shell$ yum install ImageMagick # Python packages: -shell$ easy_install pyro # (3.16) shell$ easy_install Jinja2 # (2.6) shell$ easy_install pygraphviz @@ -1380,7 +1382,7 @@ \subsection{Remote Tasks} scp. See \url{http://www.openssh.com/faq.html#2.9} for more information. \item Networking settings must allow communication {\em back} from -the task host to the suite host, either by network ports (Pyro) or ssh, +the task host to the suite host, either by network ports or ssh, unless the last-resort one way {\em task polling} communication method is used. @@ -4437,8 +4439,8 @@ \subsubsection{Remote Task Hosting} requirement. \item If SSH task communication is configured, non-interactive ssh is required from the task host to the suite host. - \item If Pyro (default) task communication is configured, the task host - should have access to the Pyro port on the suite host. + \item If (default) task communication is configured, the task host + should have access to the port on the suite host. \end{myitemize} \item the suite definition directory, or some fraction of its content, can be installed on the task host, if needed. @@ -6028,19 +6030,19 @@ \subsection{How Tasks Interact With Running Suites} report progress back to the suite. The messaging commands can be configured to work in two different ways: \begin{myenumerate} - \item {\bf Pyro:} direct messaging via network sockets using - Pyro (Python Remote Objects). + \item {\bf default:} direct messaging via network sockets using + HTTPS. \item {\bf ssh:} for tasks hosts that block access to the - network ports required by Pyro, cylc can use non-interactive ssh to + network ports required, cylc can use non-interactive ssh to re-invoke task messaging commands on the suite host (where - ultimately Pyro is still used to connect to the server process). + ultimately HTTPS is still used to connect to the server process). \end{myenumerate} \item {\bf polling:} for task hosts that do not allow return routing to -the suite host for Pyro or ssh, cylc can poll tasks at configurable +the suite host or ssh, cylc can poll tasks at configurable intervals, using non-interactive ssh. \end{myenumerate} -The Pyro communication method is the default because it is the most +The remote HTTPS communication method is the default because it is the most direct and efficient; the ssh method inserts an extra step in the process (command re-invocation on the suite host); and task polling is the least efficient because results are checked at predetermined @@ -6084,8 +6086,8 @@ \subsubsection{Task Polling} \subsection{Alternatives To Polling When Routing Is Blocked} -If Pyro and ssh ports are blocked but you don't want to use polling from -the suite host: +If remote ports are blocked and non-interactive ssh doesn't work, but you +don't want to use polling from the suite host: \begin{myitemize} \item it has been suggested that network {\em port forwarding} may provide a solution; @@ -6112,20 +6114,25 @@ \subsection{Task Host Communications Configuration} # If send fails after this many tries, give up trying: maximum number of tries = 7 - # This timeout is the same as --pyro-timeout for user commands. If + # This timeout is the same as --comms-timeout for user commands. If # set to None (no timeout) message send to non-responsive suite # (e.g. suspended with Ctrl-Z) could hang indefinitely. connection timeout in seconds = 30 -# Pyro is required for communications between cylc clients and servers -# (i.e. between suite-connecting commands and guis, and running suite -# server processes). -[pyro] +# Setup the communication method details. This is required for +# communications between cylc clients and servers (i.e. between +# suite-connecting commands and guis, and running suite server processes). +[communications] + + # Configure the choice of communication method. Only https is supported + # at the moment. +# SITE ONLY + method = https # Each suite listens on a dedicated network port. # Servers bind on the first port available from the base port up: # SITE ONLY - base port = 7766 + base port = 43001 # This sets the maximum number of suites that can run at once. # SITE ONLY @@ -6139,28 +6146,28 @@ \subsection{Task Host Communications Configuration} # Add task host sections if local defaults are not sufficient. [[HOST]] # Method of communication of task progress back to the suite: - # 1) pyro - direct client-server RPC via network ports - # 2) ssh - re-invoke pyro messaging commands on suite server + # 1) default - direct client-server RPC via network ports + # 2) ssh - re-invoke messaging commands on suite server # 3) poll - the suite polls for status of passive tasks - # Pyro RPC is still required in all cases *on the suite host* + # HTTPS comms are still required in all cases *on the suite host* # for cylc clients (commands etc.) to communicate with suites. - task communication method = "pyro" # or "ssh" or "poll" + task communication method = "default" # or "ssh" or "poll" # The "poll" method sets a default interval here to ensure no # tasks are accidentally left unpolled. You should override this # with run-length appropriate intervals under task [runtime] - # which will also result in routine polling to check task health - # under the pyro or ssh communications methods. + # under the default or ssh communications methods. default polling interval in minutes = 1.0 \end{lstlisting} \subsection{How Commands Interact With Running Suites} User-invoked commands that connect to running suites can also choose -between direct communication across network sockets (Pyro) and +between direct communication across network sockets (HTTPS) and re-invocation of commands on the suite host using non-interactive ssh (there is a \lstinline=--use-ssh= command option for this purpose). -The gcylc GUI requires direct Pyro connections to its target suite. If +The gcylc GUI requires direct HTTPS connections to its target suite. If that is not possible, run gcylc on the suite host. @@ -6372,7 +6379,7 @@ \subsection{Network Connection Timeouts} cannot hang indefinitely if the suite is not responding (this can be caused by suspending a suite with Ctrl-Z) thereby preventing the task from completing. The same can be done on the command line for other -suite-connecting user commands, with the \lstinline=--pyro-timeout= option. +suite-connecting user commands, with the \lstinline=--comms-timeout= option. \subsection{Runahead Limiting} \label{RunaheadLimit} @@ -7664,20 +7671,19 @@ \section{Cylc Development History - Major Changes} \end{myitemize} -\section{Pyro} -\label{Pyro} +\section{Communication Method} +\label{Communication} + +Cylc suite daemons and clients (commands, cylc gui, task messaging) +communicate via particular ports using the HTTPS protocol, secured +by HTTP Digest Authentication using the suite's 20-random-character +private passphrase and private SSL certificate. -Pyro (Python Remote Objects) is a widely used open source objected -oriented Remote Procedure Call technology developed by Irmen de Jong. +This is enabled via the included-in-cylc cherrypy library (for the +server) and either the Python requests library (if available) or +the built-in Python libraries for the clients. -Earlier versions of cylc used the Pyro Nameserver to marshal -communication between client programs (tasks, commands, viewers, etc.) -and their target suites. This worked well, but in principle it -provided a route for one suite or user on the network to bring down -all running suites by killing the nameserver. Consequently cylc now -uses Pyro simply as a lightweight object oriented wrapper for -direct network socket communication between client programs and their -target suites - all suites are thus entirely isolated from one another. +All suites are entirely isolated from one another. \section{Cylc 6 Migration Reference} \label{cylc-6-migration} diff --git a/doc/siterc.tex b/doc/siterc.tex index c125b8bb023..5440cbef0d7 100644 --- a/doc/siterc.tex +++ b/doc/siterc.tex @@ -160,8 +160,8 @@ \subsection{[task messaging]} \subsubsection[connection timeout]{[task messaging] \textrightarrow connection timeout} -This is the same as the \lstinline=--pyro-timeout= option in cylc -commands. Without a timeout Pyro connections to unresponsive +This is the same as the \lstinline=--comms-timeout= option in cylc +commands. Without a timeout remote connections to unresponsive suites can hang indefinitely (suites suspended with Ctrl-Z for instance). \begin{myitemize} @@ -324,23 +324,41 @@ \subsection{[editors]} \end{myitemize} -\subsection{[pyro]} +\subsection{[communication]} -Pyro is the RPC layer used for network communication between cylc +This section covers options for network communication between cylc clients (suite-connecting commands and guis) servers (running suites). Each suite listens on a dedicated network port, binding on the first available starting at the configured base port. -\subsubsection[base port]{[pyro] \textrightarrow base port } +By default, the communication method is HTTPS secured with HTTP Digest +Authentication. If the system does not support SSL, it will fall back +to HTTP. + +\subsubsection[method]{[communication] \textrightarrow method } + +The choice of client-server communication method - currently only HTTPS +is supported, although others could be developed and plugged in. + +\begin{myitemize} +\item {\em type:} string +\item {\em options:} + \begin{myitemize} + \item {\bf https} + \end{myitemize} +\item {\em default:} https +\end{myitemize} + +\subsubsection[base port]{[communication] \textrightarrow base port } The first port that cylc is allowed to use. \begin{myitemize} \item {\em type:} integer -\item {\em default:} 7766 +\item {\em default:} 43001 \end{myitemize} -\subsubsection[maximum number of ports]{[pyro] \textrightarrow maximum number of ports} +\subsubsection[maximum number of ports]{[communication] \textrightarrow maximum number of ports} This determines the maximum number of suites that can run at once on the suite host. @@ -350,7 +368,7 @@ \subsection{[pyro]} \item {\em default:} 100 \end{myitemize} -\subsubsection[ports directory]{[pyro] \textrightarrow ports directory} +\subsubsection[ports directory]{[communication] \textrightarrow ports directory} Each suite stores its port number, by suite name, under this directory. @@ -359,6 +377,33 @@ \subsection{[pyro]} \item {\em default:} \lstinline@$HOME/.cylc/ports/@ \end{myitemize} +\subsubsection[proxies on]{[communication] \textrightarrow proxies on} + +Enable or disable proxy servers for HTTPS - disabled by default. + +\begin{myitemize} +\item {\em type:} boolean +\item {\em localhost default:} False +\end{myitemize} + +\subsubsection[options]{[communication] \textrightarrow options} + +Option flags for the communication method. Currently only 'SHA1' is +supported for HTTPS, which alters HTTP Digest Auth to use the SHA1 hash +algorithm rather than the standard MD5. This is more secure but is also +less well supported by third party web clients including web browsers. +You may need to add the 'SHA1' option if you are running on platforms +where MD5 is discouraged (e.g. under FIPS). + +\begin{myitemize} +\item {\em type:} string\_list +\item {\em default:} \lstinline@[]@ +\item {\em options:} + \begin{myitemize} + \item {\bf SHA1} + \end{myitemize} +\end{myitemize} + \subsection{[monitor]} Configurable settings for the command line \lstinline=cylc monitor= tool. @@ -453,11 +498,11 @@ \subsection{[hosts]} \item {\em type:} string (must be one of the following three options) \item {\em options:} \begin{myitemize} - \item {\bf pyro} - direct client-server RPC via network ports - \item {\bf ssh} - use ssh to re-invoke the Pyro messaging commands on the suite server + \item {\bf default} - direct client-server communication via network ports + \item {\bf ssh} - use ssh to re-invoke the messaging commands on the suite server \item {\bf poll} - the suite polls for the status of tasks (no task messaging) \end{myitemize} -\item {\em localhost default:} pyro +\item {\em localhost default:} default \end{myitemize} \paragraph[remote copy template]{[hosts] \textrightarrow [[HOST]] \textrightarrow remote copy template } @@ -911,74 +956,3 @@ \subsection{[authentication]} \end{myitemize} \item {\em default:} state-totals \end{myitemize} - -\subsubsection{[authentication] \textrightarrow hashes} - -This sets the hash algorithms used for Pyro HMAC, which are used to -authenticate a client (e.g. gcylc) to the suite daemon. The first item is used -as the default hash. Subsequent items (if any) are used as fallback hashes -(e.g. for backwards compatibility with older suite daemons that are still -running with an alternative hash). - -The default hashes are SHA-256 followed by MD5. You may want to exclude MD5 for -security reasons - e.g. for FIPS -(\url{https://en.wikipedia.org/wiki/FIPS_140-2}) compliance. Older cylc -versions use MD5. - -SHA-256 and SHA-512 are the most secure. MD5 is popularly used for HMAC, but is -the least secure. SHA-1 is slightly better. Note that HMAC use is not as -susceptible to hash collision weakness, so both algorithms are more secure for -HMAC use than in the general case. - -Clients will attempt to establish a connection using the algorithms from first -to last. The exceptions are scanning and pinging, which will use the algorithm -configured in the [authentication] \textrightarrow scan hash setting below. If -you have lots of suites running with e.g. MD5 hashes, you will want to include -'md5' in your list of hashes and set it as the default scan hash. - -See \url{https://docs.python.org/2/library/hmac.html} and -\url{https://docs.python.org/2/library/hashlib.html} for discussion of hash -algorithms. - -\begin{myitemize} -\item {\em type:} comma-separated list of hash names (must each be one of the - following options) -\item {\em options:} - \begin{myitemize} - \item {\em sha256} - SHA-256 (recommended, default) - \item {\em sha512} - SHA-512 (most secure, but slowest) - \item {\em sha1} - SHA-1 (may be the best option on legacy systems) - \item {\em md5} - MD5 (old default, usually used for HMAC, but the - least secure) - \end{myitemize} -\item {\em default:} sha256,md5 -\end{myitemize} - - -\subsubsection{[authentication] \textrightarrow scan hash} - -See [authentication] \textrightarrow hashes. - -Configure which of the specified hash algorithms in [authentication] -\textrightarrow hashes is used for scanning and pinging (cylc scan, cylc ping). - -The default hash algorithm for scans is MD5, for backwards compatibility -reasons. You can override it by specifying a different value for this setting -(e.g. for FIPS compliance). - -The default will become SHA-256 sometime in 2016, when it is unlikely that any -old MD5 suites are still around. - -\begin{myitemize} -\item {\em type:} hash name (must be one of the following options) -\item {\em options:} - \begin{myitemize} - \item {\em sha256} - SHA-256 (recommended, future default) - \item {\em sha512} - SHA-512 (most secure, but slowest) - \item {\em sha1} - SHA-1 (may be the best option on legacy systems) - \item {\em md5} - MD5 (default, good for backwards compatibility, but - the least secure) - \end{myitemize} -\item {\em default:} md5 -\end{myitemize} - diff --git a/doc/suiterc.tex b/doc/suiterc.tex index 1815cc9a1c3..ee0bc91f9be 100644 --- a/doc/suiterc.tex +++ b/doc/suiterc.tex @@ -1325,7 +1325,7 @@ \subsection{[runtime]} For the polling task communication method this overrides the default submission polling interval in the site/user config files -(\ref{SiteAndUserConfiguration}). For pyro and ssh task communications, +(\ref{SiteAndUserConfiguration}). For default and ssh task communications, polling is not done by default but it can still be configured here as a regular check on the health of submitted tasks. @@ -1351,7 +1351,7 @@ \subsection{[runtime]} For the polling task communication method this overrides the default execution polling interval in the site/user config files -(\ref{SiteAndUserConfiguration}). For pyro and ssh task communications, +(\ref{SiteAndUserConfiguration}). For default and ssh task communications, polling is not done by default but it can still be configured here as a regular check on the health of submitted tasks. @@ -1372,9 +1372,8 @@ \subsection{[runtime]} Configure host and username, for tasks that do not run on the suite host account. Non-interactive ssh is used to submit the task by the configured batch system, so you must distribute your ssh key to allow -this. Cylc must be installed on remote task hosts, but of the external -software dependencies only Pyro is required there (not even that if {\em -ssh messaging} is used; see below). +this. Cylc must be installed on remote task hosts, but no external +software dependencies are required there. \subparagraph[host]{[runtime] \textrightarrow [[\_\_NAME\_\_]] \textrightarrow [[[remote]]] \textrightarrow host} \label{DynamicHostSelection} diff --git a/examples/remote/minimal/suite.rc b/examples/remote/minimal/suite.rc index 2b8fa357c30..4c7bc3bdf2a 100644 --- a/examples/remote/minimal/suite.rc +++ b/examples/remote/minimal/suite.rc @@ -1,10 +1,10 @@ title = a minimal self-contained remote task suite description = """ -This runs one task on a remote host. To use it, install cylc (and Pyro -OR ssh key for ssh-based remote messaging) on the remote host specified -under [[[remote]]] below. Remote cylc location, and ssh messaging, can -be set in host-specific sections of the cylc site/user config file.""" +This runs one task on a remote host. To use it, install cylc on the +remote host specified under [[[remote]]] below. Remote cylc +location, and ssh messaging, can be set in host-specific sections of +the cylc site/user config file.""" [scheduling] [[dependencies]] diff --git a/lib/cylc/broadcast_report.py b/lib/cylc/broadcast_report.py index 80760b32ddd..1872bb413cc 100644 --- a/lib/cylc/broadcast_report.py +++ b/lib/cylc/broadcast_report.py @@ -39,7 +39,7 @@ def get_broadcast_bad_options_report(bad_options, is_set=False): msg = BAD_OPTIONS_TITLE for key, values in sorted(bad_options.items()): for value in values: - if isinstance(value, tuple): + if isinstance(value, tuple) or isinstance(value, list): value_str = "" values = list(value) while values: diff --git a/lib/cylc/cfgspec/globalcfg.py b/lib/cylc/cfgspec/globalcfg.py index 50f8bcbef0a..e43eeaeb13b 100644 --- a/lib/cylc/cfgspec/globalcfg.py +++ b/lib/cylc/cfgspec/globalcfg.py @@ -127,10 +127,14 @@ 'gui': vdr(vtype='string', default="gvim -f"), }, - 'pyro': { - 'base port': vdr(vtype='integer', default=7766), + 'communication': { + 'method': vdr(vtype='string', default="https", + options=["https"]), + 'base port': vdr(vtype='integer', default=43001), 'maximum number of ports': vdr(vtype='integer', default=100), 'ports directory': vdr(vtype='string', default="$HOME/.cylc/ports/"), + 'proxies on': vdr(vtype='boolean', default=False), + 'options': vdr(vtype='string_list', default=[]), }, 'monitor': { @@ -145,7 +149,7 @@ 'work directory': vdr(vtype='string', default="$HOME/cylc-run"), 'task communication method': vdr( vtype='string', - options=["pyro", "ssh", "poll"], default="pyro"), + options=["default", "ssh", "poll"], default="default"), 'remote copy template': vdr( vtype='string', default='scp -oBatchMode=yes -oConnectTimeout=10'), @@ -197,7 +201,7 @@ 'run directory': vdr(vtype='string'), 'work directory': vdr(vtype='string'), 'task communication method': vdr( - vtype='string', options=["pyro", "ssh", "poll"]), + vtype='string', options=["default", "ssh", "poll"]), 'remote copy template': vdr(vtype='string'), 'remote shell template': vdr(vtype='string'), 'use login shell': vdr(vtype='boolean', default=None), @@ -275,14 +279,6 @@ vtype='string', options=PRIVILEGE_LEVELS[:PRIVILEGE_LEVELS.index('shutdown') + 1], default="state-totals"), - 'hashes': vdr( - vtype='string_list', - options=['md5', 'sha1', 'sha256', 'sha512'], - default=['sha256', 'md5']), - 'scan hash': vdr( - vtype='string', - options=['md5', 'sha1', 'sha256', 'sha512'], - default='md5'), }, } @@ -339,6 +335,32 @@ def upg(cfg, descr): for key in SPEC['cylc']['events']: u.deprecate( '6.11.0', ['cylc', 'event hooks', key], ['cylc', 'events', key]) + u.obsolete( + '7.0.0', + ['pyro', 'base port'] + ) + u.obsolete( + '7.0.0', + ['pyro', 'maximum number of ports'], + ['communication', 'maximum number of ports'] + ) + u.obsolete( + '7.0.0', + ['pyro', 'ports directory'], + ['communication', 'ports directory'] + ) + u.obsolete( + '7.0.0', + ['pyro'] + ) + u.obsolete( + '7.0.0', + ['authentication', 'hashes'] + ) + u.obsolete( + '7.0.0', + ['authentication', 'scan hash'] + ) u.upgrade() @@ -543,8 +565,8 @@ def create_cylc_run_tree(self, suite): if value: self.create_directory(value, item) - item = '[pyro]ports directory' - value = cfg['pyro']['ports directory'] + item = '[communication]ports directory' + value = cfg['communication']['ports directory'] self.create_directory(value, item) def get_tmpdir(self): @@ -593,8 +615,8 @@ def transform(self): for key, val in cfg['documentation']['files'].items(): cfg['documentation']['files'][key] = expandvars(val) - cfg['pyro']['ports directory'] = expandvars( - cfg['pyro']['ports directory']) + cfg['communication']['ports directory'] = expandvars( + cfg['communication']['ports directory']) for key, val in cfg['hosts']['localhost'].items(): if val and 'directory' in key: diff --git a/lib/cylc/config.py b/lib/cylc/config.py index 2099eca545e..9e2f5bfa23b 100644 --- a/lib/cylc/config.py +++ b/lib/cylc/config.py @@ -1571,11 +1571,15 @@ def get_conditional_label(self, expression): return label def get_graph_raw(self, start_point_string, stop_point_string, - group_nodes=[], ungroup_nodes=[], + group_nodes=None, ungroup_nodes=None, ungroup_recursive=False, group_all=False, ungroup_all=False): """Convert the abstract graph edges held in self.edges (etc.) to actual edges for a concrete range of cycle points.""" + if group_nodes is None: + group_nodes = [] + if ungroup_nodes is None: + ungroup_nodes = [] members = self.runtime['first-parent descendants'] hierarchy = self.runtime['first-parent ancestors'] diff --git a/lib/cylc/dump.py b/lib/cylc/dump.py index e4580c352d9..b1eba522910 100644 --- a/lib/cylc/dump.py +++ b/lib/cylc/dump.py @@ -19,7 +19,7 @@ import time from cylc.task_id import TaskID -from cylc.network.suite_state import SUITE_STATUS_STOPPED +from cylc.network.suite_state_client import SUITE_STATUS_STOPPED from cylc.task_state import TASK_STATUS_READY diff --git a/lib/cylc/gui/app_gcylc.py b/lib/cylc/gui/app_gcylc.py index 93dc5721d06..09aa3b85bbb 100644 --- a/lib/cylc/gui/app_gcylc.py +++ b/lib/cylc/gui/app_gcylc.py @@ -53,7 +53,7 @@ from cylc.gui.util import ( get_icon, get_image_dir, get_logo, EntryTempText, EntryDialog, setup_icons, set_exception_hook_dialog) -from cylc.network.suite_state import ( +from cylc.network.suite_state_client import ( extract_group_state, SUITE_STATUS_STOPPED_WITH) from cylc.task_id import TaskID from cylc.version import CYLC_VERSION @@ -136,16 +136,16 @@ class InitData(object): Class to hold initialisation data. """ def __init__(self, suite, owner, host, port, db, - pyro_timeout, template_vars, ungrouped_views, use_defn_order): + comms_timeout, template_vars, ungrouped_views, use_defn_order): self.suite = suite self.owner = owner self.host = host self.port = port self.db = db - if pyro_timeout: - self.pyro_timeout = float(pyro_timeout) + if comms_timeout: + self.comms_timeout = float(comms_timeout) else: - self.pyro_timeout = None + self.comms_timeout = None self.template_vars_opts = "" for item in template_vars.items(): @@ -523,8 +523,13 @@ class ControlApp(object): VIEWS["graph"] = ControlGraph VIEWS_ORDERED.append("graph") +<<<<<<< HEAD def __init__(self, suite, db, owner, host, port, pyro_timeout, template_vars, restricted_display): +======= + def __init__(self, suite, db, owner, host, port, comms_timeout, + template_vars, template_vars_file, restricted_display): +>>>>>>> 30b4022... #1872: comms layer to https gobject.threads_init() @@ -536,10 +541,17 @@ def __init__(self, suite, db, owner, host, port, pyro_timeout, if "graph" in self.__class__.VIEWS_ORDERED: self.__class__.VIEWS_ORDERED.remove('graph') +<<<<<<< HEAD self.cfg = InitData( suite, owner, host, port, db, pyro_timeout, template_vars, gcfg.get(["ungrouped views"]), gcfg.get(["sort by definition order"])) +======= + self.cfg = InitData(suite, owner, host, port, db, comms_timeout, + template_vars, template_vars_file, + gcfg.get(["ungrouped views"]), + gcfg.get(["sort by definition order"])) +>>>>>>> 30b4022... #1872: comms layer to https self.theme_name = gcfg.get(['use theme']) self.theme = gcfg.get(['themes', self.theme_name]) @@ -977,7 +989,7 @@ def click_exit(self, foo): def click_open(self, foo=None): app = dbchooser(self.window, self.cfg.db, self.cfg.owner, - self.cfg.cylc_tmpdir, self.cfg.pyro_timeout) + self.cfg.cylc_tmpdir, self.cfg.comms_timeout) reg, auth = None, None while True: response = app.window.run() @@ -996,16 +1008,16 @@ def click_open(self, foo=None): self.reset(reg, auth) def pause_suite(self, bt): - self.put_pyro_command('hold_suite') + self.put_comms_command('hold_suite') def resume_suite(self, bt): - self.put_pyro_command('release_suite') + self.put_comms_command('release_suite') def stopsuite_default(self, *args): """Try to stop the suite (after currently running tasks...).""" if not self.get_confirmation("Stop suite %s?" % self.cfg.suite): return - self.put_pyro_command('set_stop_cleanly') + self.put_comms_command('set_stop_cleanly') def stopsuite(self, bt, window, kill_rb, stop_rb, stopat_rb, stopct_rb, stoptt_rb, stopnow_rb, stopnownow_rb, stoppoint_entry, @@ -1072,19 +1084,28 @@ def stopsuite(self, bt, window, kill_rb, stop_rb, stopat_rb, stopct_rb, window.destroy() if stop: - self.put_pyro_command('set_stop_cleanly', False) + self.put_comms_command('set_stop_cleanly', + kill_active_tasks=False) elif stopkill: - self.put_pyro_command('set_stop_cleanly', True) + self.put_comms_command('set_stop_cleanly', + kill_active_tasks=True) elif stopat: - self.put_pyro_command('set_stop_after_point', stop_point_string) + self.put_comms_command('set_stop_after_point', + point_string=stop_point_string) elif stopnow: +<<<<<<< HEAD self.put_pyro_command('stop_now') elif stopnownow: self.put_pyro_command('stop_now', True) +======= + self.put_comms_command('stop_now') +>>>>>>> 30b4022... #1872: comms layer to https elif stopclock: - self.put_pyro_command('set_stop_after_clock_time', stopclock_time) + self.put_comms_command('set_stop_after_clock_time', + datetime_string=stopclock_time) elif stoptask: - self.put_pyro_command('set_stop_after_task', stoptask_id) + self.put_comms_command('set_stop_after_task', + task_id=stoptask_id) def load_point_strings(self, bt, startentry, stopentry): item1 = " -i '[scheduling]initial cycle point'" @@ -1533,7 +1554,7 @@ def change_runahead(self, w, entry, window): else: limit = ent window.destroy() - self.put_pyro_command('set_runahead', limit) + self.put_comms_command('set_runahead', interval=limit) def update_tb(self, tb, line, tags=None): if tags: @@ -1543,7 +1564,8 @@ def update_tb(self, tb, line, tags=None): def popup_requisites(self, w, e, task_id): name, point_string = TaskID.split(task_id) - result = self.get_pyro_info('get_task_requisites', name, point_string) + result = self.get_comms_info( + 'get_task_requisites', name=name, point_string=point_string) if result: # (else no tasks were found at all -suite shutting down) if task_id not in result: @@ -1653,12 +1675,12 @@ def hold_task(self, b, task_ids, stop=True, is_family=False): for task_id in task_ids: if not self.get_confirmation("Hold %s?" % (task_id)): return - self.put_pyro_command('hold_task', task_ids) + self.put_comms_command('hold_tasks', items=task_ids) else: for task_id in task_ids: if not self.get_confirmation("Release %s?" % (task_id)): return - self.put_pyro_command('release_task', task_ids) + self.put_comms_command('release_tasks', items=task_ids) def trigger_task_now(self, b, task_ids, is_family=False): """Trigger task via the suite daemon's command interface.""" @@ -1668,7 +1690,7 @@ def trigger_task_now(self, b, task_ids, is_family=False): for task_id in task_ids: if not self.get_confirmation("Trigger %s?" % task_id): return - self.put_pyro_command('trigger_task', task_ids) + self.put_comms_command('trigger_tasks', items=task_ids) def trigger_task_edit_run(self, b, task_id): """ @@ -1690,7 +1712,7 @@ def poll_task(self, b, task_ids, is_family=False): for task_id in task_ids: if not self.get_confirmation("Poll %s?" % task_id): return - self.put_pyro_command('poll_tasks', task_ids) + self.put_comms_command('poll_tasks', items=task_ids) def kill_task(self, b, task_ids, is_family=False): """Kill a task/family.""" @@ -1701,7 +1723,7 @@ def kill_task(self, b, task_ids, is_family=False): if not self.get_confirmation("Kill %s?" % task_id, force_prompt=True): return - self.put_pyro_command('kill_tasks', task_ids) + self.put_comms_command('kill_tasks', items=task_ids) def spawn_task(self, b, e, task_ids, is_family=False): """For tasks to spawn their successors.""" @@ -1713,7 +1735,7 @@ def spawn_task(self, b, e, task_ids, is_family=False): return False if not self.get_confirmation("Force spawn %s?" % task_id): return - self.put_pyro_command('spawn_tasks', task_ids, None) + self.put_comms_command('spawn_tasks', items=task_ids) def reset_task_state(self, b, e, task_ids, state, is_family=False): """Reset the state of a task/family.""" @@ -1725,7 +1747,8 @@ def reset_task_state(self, b, e, task_ids, state, is_family=False): return False if not self.get_confirmation("reset %s to %s?" % (task_id, state)): return - self.put_pyro_command('reset_task_state', task_ids, None, state) + self.put_comms_command('reset_task_state', items=task_ids, + state=state) def remove_task(self, b, task_ids, is_family): """Remove a task.""" @@ -1737,7 +1760,7 @@ def remove_task(self, b, task_ids, is_family): "Remove %s after spawning?" % task_id): return name, point_string = TaskID.split(task_id) - self.put_pyro_command('remove_task', task_ids, None, True) + self.put_comms_command('remove_task', task_ids, spawn=True) def remove_task_nospawn(self, b, task_ids, is_family=False): """Remove a task, without spawn.""" @@ -1748,7 +1771,7 @@ def remove_task_nospawn(self, b, task_ids, is_family=False): if not self.get_confirmation( "Remove %s without spawning?" % task_id): return - self.put_pyro_command('remove_task', task_ids, None, False) + self.put_comms_command('remove_tasks', task_ids, spawn=False) def stopsuite_popup(self, b): window = gtk.Window() @@ -2184,24 +2207,26 @@ def insert_task(self, w, window, entry_task_ids, entry_stop_point): window.destroy() if not stop_point_str.strip(): stop_point_str = None - self.put_pyro_command( - 'insert_task', task_ids, None, None, stop_point_str) + self.put_comms_command( + 'insert_tasks', items=task_ids, + stop_point_string=stop_point_str + ) def poll_all(self, w): """Poll all active tasks.""" if not self.get_confirmation("Poll all submitted/running task jobs?"): return - self.put_pyro_command('poll_tasks', None, None, None) + self.put_comms_command('poll_tasks') def reload_suite(self, w): if not self.get_confirmation("Reload suite definition?"): return - self.put_pyro_command('reload_suite') + self.put_comms_command('reload_suite') def nudge_suite(self, w): if not self.get_confirmation("Nudge suite?"): return - self.put_pyro_command('nudge') + self.put_comms_command('nudge') def _popup_logview(self, task_id, task_state_summary, choice=None): """Display task job log files in a combo log viewer.""" @@ -2226,7 +2251,7 @@ def _popup_logview(self, task_id, task_state_summary, choice=None): ) for submit_num, job_user_at_host in sorted( job_hosts.items(), reverse=True): - submit_num_str = "%02d" % submit_num + submit_num_str = "%02d" % int(submit_num) local_job_log_dir = os.path.join(itask_log_dir, submit_num_str) for filename in ["job", "job-activity.log"]: filenames.append(os.path.join(local_job_log_dir, filename)) @@ -2811,7 +2836,7 @@ def describe_suite(self, w): # Show suite title and description. if self.updater.connected: # Interrogate the suite daemon. - info = self.get_pyro_info('get_suite_info') + info = self.get_comms_info('get_suite_info') descr = '\n'.join( "%s: %s" % (key, val) for key, val in info.items()) info_dialog(descr, self.window).inform() @@ -3331,19 +3356,19 @@ def destroy_theme_legend(self, widget): """Handle a destroy of the theme legend window.""" self.theme_legend_window = None - def put_pyro_command(self, command, *args): + def put_comms_command(self, command, **kwargs): try: success, msg = self.updater.suite_command_client.put_command( - command, *args) + command, **kwargs) except Exception, x: warning_dialog(x.__str__(), self.window).warn() else: if not success: warning_dialog(msg, self.window).warn() - def get_pyro_info(self, command, *args): + def get_comms_info(self, command, **kwargs): try: - return self.updater.suite_info_client.get_info(command, *args) + return self.updater.suite_info_client.get_info(command, **kwargs) except Exception as exc: warning_dialog(str(exc), self.window).warn() diff --git a/lib/cylc/gui/dbchooser.py b/lib/cylc/gui/dbchooser.py index 1199a36f097..b8756268853 100644 --- a/lib/cylc/gui/dbchooser.py +++ b/lib/cylc/gui/dbchooser.py @@ -38,13 +38,13 @@ class db_updater(threading.Thread): SCAN_INTERVAL = 60.0 - def __init__(self, regd_treestore, db, filtr=None, pyro_timeout=None): + def __init__(self, regd_treestore, db, filtr=None, timeout=None): self.db = db self.quit = False - if pyro_timeout: - self.pyro_timeout = float(pyro_timeout) + if timeout: + self.timeout = float(timeout) else: - self.pyro_timeout = None + self.timeout = None self.regd_treestore = regd_treestore super(db_updater, self).__init__() @@ -112,7 +112,7 @@ def update(self): # Scan for running suites choices = [] - for host, scan_result in scan_all(pyro_timeout=self.pyro_timeout): + for host, scan_result in scan_all(timeout=self.timeout): try: port, suite_identity = scan_result except ValueError: @@ -279,14 +279,14 @@ def match_func(self, model, iter_, data): class dbchooser(object): - def __init__(self, parent, db, db_owner, tmpdir, pyro_timeout): + def __init__(self, parent, db, db_owner, tmpdir, timeout): self.db = db self.db_owner = db_owner - if pyro_timeout: - self.pyro_timeout = float(pyro_timeout) + if timeout: + self.timeout = float(timeout) else: - self.pyro_timeout = None + self.timeout = None self.chosen = None @@ -423,7 +423,7 @@ def start_updater(self, filtr=None): if self.updater: self.updater.quit = True # does this take effect? self.updater = db_updater( - self.regd_treestore, db, filtr, self.pyro_timeout) + self.regd_treestore, db, filtr, self.timeout) self.updater.start() # TODO: a button to do this? diff --git a/lib/cylc/gui/gpanel.py b/lib/cylc/gui/gpanel.py index ca643598a09..08d58ec72db 100755 --- a/lib/cylc/gui/gpanel.py +++ b/lib/cylc/gui/gpanel.py @@ -39,7 +39,7 @@ from cylc.gui.dot_maker import DotMaker from cylc.gui.util import get_icon, setup_icons from cylc.owner import USER -from cylc.network.suite_state import extract_group_state +from cylc.network.suite_state_client import extract_group_state from cylc.cfgspec.gscan import gsfg diff --git a/lib/cylc/gui/gscan.py b/lib/cylc/gui/gscan.py index df5c0f6f653..10081357cf1 100644 --- a/lib/cylc/gui/gscan.py +++ b/lib/cylc/gui/gscan.py @@ -43,7 +43,6 @@ TASK_STATUS_FAILED, TASK_STATUS_SUBMIT_FAILED) from cylc.cfgspec.gscan import gsfg -PYRO_TIMEOUT = 2 KEY_NAME = "name" KEY_OWNER = "owner" KEY_STATES = "states" @@ -54,7 +53,7 @@ def get_hosts_suites_info(hosts, timeout=None, owner=None): """Return a dictionary of hosts, suites, and their properties.""" host_suites_map = {} for host, (port, result) in scan_all( - hosts=hosts, pyro_timeout=timeout): + hosts=hosts, timeout=timeout): if owner and owner != result.get(KEY_OWNER): continue if host not in host_suites_map: diff --git a/lib/cylc/gui/updater.py b/lib/cylc/gui/updater.py index 67616c466cc..b63dee48ad4 100644 --- a/lib/cylc/gui/updater.py +++ b/lib/cylc/gui/updater.py @@ -19,7 +19,6 @@ import re import sys -import Pyro import atexit import gobject import threading @@ -30,17 +29,20 @@ import cylc.flags from cylc.dump import get_stop_state_summary from cylc.gui.cat_state import cat_state -from cylc.network.suite_state import ( +from cylc.network import ConnectionDeniedError +from cylc.network.suite_state_client import ( StateSummaryClient, SuiteStillInitialisingError, get_suite_status_string, SUITE_STATUS_NOT_CONNECTED, SUITE_STATUS_CONNECTED, - SUITE_STATUS_INITIALISING, SUITE_STATUS_STOPPED, SUITE_STATUS_STOPPING) -from cylc.network.suite_info import SuiteInfoClient -from cylc.network.suite_log import SuiteLogClient -from cylc.network.suite_command import SuiteCommandClient + SUITE_STATUS_INITIALISING, SUITE_STATUS_STOPPED, SUITE_STATUS_STOPPING +) +from cylc.network.suite_info_client import SuiteInfoClient +from cylc.network.suite_log_client import SuiteLogClient +from cylc.network.suite_command_client import SuiteCommandClient from cylc.wallclock import ( get_current_time_string, get_seconds_as_interval_string, - get_time_string_from_unix_time) + get_time_string_from_unix_time +) from cylc.task_id import TaskID from cylc.version import CYLC_VERSION from cylc.gui.warning_dialog import warning_dialog @@ -174,7 +176,7 @@ def __init__(self, app): self.version_mismatch_warned = False client_args = (self.cfg.suite, self.cfg.owner, self.cfg.host, - self.cfg.pyro_timeout, self.cfg.port, self.cfg.db, + self.cfg.comms_timeout, self.cfg.port, self.cfg.db, self.cfg.my_uuid) self.state_summary_client = StateSummaryClient(*client_args) self.suite_info_client = SuiteInfoClient(*client_args) @@ -187,7 +189,7 @@ def reconnect(self): """Try to reconnect to the suite daemon.""" if cylc.flags.debug: print >> sys.stderr, " reconnection...", - # Reset Pyro clients. + # Reset comms clients. self.suite_log_client.reset() self.state_summary_client.reset() self.suite_info_client.reset() @@ -195,10 +197,6 @@ def reconnect(self): try: self.daemon_version = self.suite_info_client.get_info( 'get_cylc_version') - except KeyError: - self.daemon_version = "??? (pre 6.1.2?)" - if cylc.flags.debug: - print >> sys.stderr, "succeeded (old daemon)" except PortFileError as exc: if cylc.flags.debug: traceback.print_exc() @@ -218,16 +216,12 @@ def reconnect(self): self.info_bar.set_update_time( None, self.info_bar.DISCONNECTED_TEXT) return - except Pyro.errors.NamingError as exc: - if cylc.flags.debug: - traceback.print_exc() - return except Exception as exc: if cylc.flags.debug: traceback.print_exc() if not self.connect_fail_warned: self.connect_fail_warned = True - if isinstance(exc, Pyro.errors.ConnectionDeniedError): + if isinstance(exc, ConnectionDeniedError): gobject.idle_add( self.warn, "ERROR: %s\n\nIncorrect suite passphrase?" % exc) @@ -276,15 +270,10 @@ def set_update(self, should_update): def retrieve_err_log(self): """Retrieve suite err log; return True if it has changed.""" - try: - new_err_content, new_err_size = ( - self.suite_log_client.get_err_content( - self.err_log_size, self._err_num_log_lines)) - except AttributeError: - # TODO: post-backwards compatibility concerns, remove this handling - new_err_content = "" - new_err_size = self.err_log_size - + new_err_content, new_err_size = ( + self.suite_log_client.get_err_content( + self.err_log_size, self._err_num_log_lines) + ) err_log_changed = (new_err_size != self.err_log_size) if err_log_changed: self.err_log_lines += new_err_content.splitlines() @@ -294,48 +283,39 @@ def retrieve_err_log(self): def retrieve_summary_update_time(self): """Retrieve suite summary update time; return True if changed.""" - do_update = False - try: - summary_update_time = ( - self.state_summary_client.get_suite_state_summary_update_time() - ) - if (summary_update_time is None or - self._summary_update_time is None or - summary_update_time != self._summary_update_time): - self._summary_update_time = summary_update_time - do_update = True - except AttributeError: - # TODO: post-backwards compatibility concerns, remove this handling - # Force an update for daemons using the old API - do_update = True - return do_update + summary_update_time = float( + self.state_summary_client.get_suite_state_summary_update_time() + ) + if (summary_update_time is None or + self._summary_update_time is None or + summary_update_time != self._summary_update_time): + self._summary_update_time = summary_update_time + return True + return False def retrieve_state_summaries(self): """Retrieve suite summary.""" + ret = self.state_summary_client.get_suite_state_summary() glbl, states, fam_states = ( self.state_summary_client.get_suite_state_summary()) self.ancestors = self.suite_info_client.get_info( 'get_first_parent_ancestors') self.ancestors_pruned = self.suite_info_client.get_info( - 'get_first_parent_ancestors', True) + 'get_first_parent_ancestors', pruned=True) self.descendants = self.suite_info_client.get_info( 'get_first_parent_descendants') self.all_families = self.suite_info_client.get_info('get_all_families') self.mode = glbl['run_mode'] - if self.cfg.use_defn_order and 'namespace definition order' in glbl: - # (protect for compat with old suite daemons) + if self.cfg.use_defn_order: nsdo = glbl['namespace definition order'] if self.ns_defn_order != nsdo: self.ns_defn_order = nsdo self.dict_ns_defn_order = dict(zip(nsdo, range(0, len(nsdo)))) - try: - self.update_time_str = get_time_string_from_unix_time( - glbl['last_updated']) - except (TypeError, ValueError): - # Older suite... - self.update_time_str = glbl['last_updated'].isoformat() + + self.update_time_str = get_time_string_from_unix_time( + glbl['last_updated']) self.global_summary = glbl if self.restricted_display: @@ -345,19 +325,8 @@ def retrieve_state_summaries(self): self.full_fam_state_summary = fam_states self.refilter() - try: - self.status = glbl['status_string'] - except KeyError: - # Back compat for suite daemons <= 6.9.1. - self.status = get_suite_status_string( - glbl['paused'], glbl['stopping'], glbl['will_pause_at'], - glbl['will_stop_at']) - - try: - self.is_reloading = glbl['reloading'] - except KeyError: - # Back compat. - pass + self.status = glbl['status_string'] + self.is_reloading = glbl['reloading'] def set_stopped(self): """Reset data and clients when suite is stopped.""" @@ -434,29 +403,6 @@ def update(self): self.info_bar.prog_bar_start, SUITE_STATUS_INITIALISING) self.info_bar.set_state([]) return False - except Pyro.errors.NamingError as exc: - if self.daemon_version is not None: - # Back compat <= 6.4.0 the state summary object was not - # connected to Pyro until initialisation was completed. - if cylc.flags.debug: - print >> sys.stderr, ( - " daemon <= 6.4.0, suite initializing ...") - self.set_status(SUITE_STATUS_INITIALISING) - if self.info_bar.prog_bar_can_start(): - gobject.idle_add(self.info_bar.prog_bar_start, - SUITE_STATUS_INITIALISING) - self.info_bar.set_state([]) - # Reconnect till we get the suite state object. - self.reconnect() - return False - else: - if cylc.flags.debug: - print >> sys.stderr, " CONNECTION LOST", str(exc) - self.set_stopped() - if self.info_bar.prog_bar_active(): - gobject.idle_add(self.info_bar.prog_bar_stop) - self.reconnect() - return False except Exception as exc: if self.status == SUITE_STATUS_STOPPING: # Expected stop: prevent the reconnection warning dialog. diff --git a/lib/cylc/gui/updater_dot.py b/lib/cylc/gui/updater_dot.py index cd43a2b20fd..2b60bf8d6ec 100644 --- a/lib/cylc/gui/updater_dot.py +++ b/lib/cylc/gui/updater_dot.py @@ -23,7 +23,7 @@ from cylc.task_id import TaskID from cylc.gui.dot_maker import DotMaker -from cylc.network.suite_state import get_id_summary +from cylc.network.suite_state_client import get_id_summary from copy import deepcopy import warnings diff --git a/lib/cylc/gui/updater_graph.py b/lib/cylc/gui/updater_graph.py index 2ba5b8ab0d9..456cc64431f 100644 --- a/lib/cylc/gui/updater_graph.py +++ b/lib/cylc/gui/updater_graph.py @@ -18,7 +18,7 @@ from cylc.graphing import CGraphPlain from cylc.mkdir_p import mkdir_p -from cylc.network.suite_state import get_id_summary +from cylc.network.suite_state_client import get_id_summary from cylc.task_id import TaskID from cylc.cfgspec.globalcfg import GLOBAL_CFG from cylc.gui.warning_dialog import warning_dialog @@ -300,38 +300,31 @@ def update_graph(self): oldest = self.oldest_point_string newest = self.newest_point_string + group_for_server = self.group + if self.group == []: + group_for_server = None + + ungroup_for_server = self.ungroup + if self.ungroup == []: + ungroup_for_server = None + try: res = self.updater.suite_info_client.get_info( - 'get_graph_raw', oldest, newest, self.group, self.ungroup, - self.ungroup_recursive, self.group_all, self.ungroup_all) - except TypeError: - # Back compat with pre cylc-6 suite daemons. - res = self.updater.suite_info_client.get( - 'get_graph_raw', oldest, newest, False, self.group, - self.ungroup, self.ungroup_recursive, self.group_all, - self.ungroup_all) - except Exception as exc: # PyroError? + 'get_graph_raw', start_point_string=oldest, + stop_point_string=newest, + group_nodes=group_for_server, + ungroup_nodes=ungroup_for_server, + ungroup_recursive=self.ungroup_recursive, + group_all=self.group_all, + ungroup_all=self.ungroup_all + ) + except Exception as exc: print >> sys.stderr, str(exc) return False - # backward compatibility for old suite daemons still running - self.have_leaves_and_feet = False - if isinstance(res, list): - # prior to suite-polling tasks in 5.4.0 - gr_edges = res - suite_polling_tasks = [] - self.leaves = [] - self.feet = [] - else: - if len(res) == 2: - # prior to graph view grouping fix in 5.4.2 - gr_edges, suite_polling_tasks = res - self.leaves = [] - self.feet = [] - elif len(res) == 4: - # 5.4.2 and later - self.have_leaves_and_feet = True - gr_edges, suite_polling_tasks, self.leaves, self.feet = res + self.have_leaves_and_feet = True + gr_edges, suite_polling_tasks, self.leaves, self.feet = res + gr_edges = [tuple(edge) for edge in gr_edges] current_id = self.get_graph_id(gr_edges) needs_redraw = current_id != self.prev_graph_id diff --git a/lib/cylc/gui/updater_tree.py b/lib/cylc/gui/updater_tree.py index 68ec86fce9d..23ca8c41e2b 100644 --- a/lib/cylc/gui/updater_tree.py +++ b/lib/cylc/gui/updater_tree.py @@ -25,7 +25,7 @@ from cylc.task_id import TaskID from cylc.gui.dot_maker import DotMaker -from cylc.network.suite_state import get_id_summary +from cylc.network.suite_state_client import get_id_summary from cylc.wallclock import get_time_string_from_unix_time from cylc.task_state import TASK_STATUSES_AUTO_EXPAND diff --git a/lib/cylc/network/__init__.py b/lib/cylc/network/__init__.py index 002dea2869f..d94aa9a168f 100644 --- a/lib/cylc/network/__init__.py +++ b/lib/cylc/network/__init__.py @@ -24,13 +24,14 @@ # Names for network-connected objects. # WARNING: these names are don't have consistent formatting, but changing them # will break backward compatibility with older cylc clients! -PYRO_SUITEID_OBJ_NAME = 'cylcid' -PYRO_EXT_TRIG_OBJ_NAME = 'ext-trigger-interface' -PYRO_BCAST_OBJ_NAME = 'broadcast_receiver' -PYRO_CMD_OBJ_NAME = 'command-interface' -PYRO_INFO_OBJ_NAME = 'suite-info' -PYRO_LOG_OBJ_NAME = 'log' -PYRO_STATE_OBJ_NAME = 'state_summary' +COMMS_SUITEID_OBJ_NAME = 'id' +COMMS_EXT_TRIG_OBJ_NAME = 'ext-trigger' +COMMS_BCAST_OBJ_NAME = 'broadcast' +COMMS_CMD_OBJ_NAME = 'command' +COMMS_INFO_OBJ_NAME = 'info' +COMMS_LOG_OBJ_NAME = 'log' +COMMS_STATE_OBJ_NAME = 'state' +COMMS_TASK_MESSAGE_OBJ_NAME = 'message' # Ordered privilege levels for authenticated users. PRIVILEGE_LEVELS = [ @@ -49,6 +50,16 @@ NO_PASSPHRASE = 'the quick brown fox' +class ConnectionDeniedError(Exception): + + """An error raised when the client is not permitted to connect.""" + + MESSAGE = "Not authorized: %s: %s: access type '%s'" + + def __str__(self): + return self.MESSAGE % (self.args[0], self.args[1], self.args[2]) + + def access_priv_ok(server_obj, required_privilege_level): """Return True if a client is allowed access to info from server_obj. @@ -59,9 +70,10 @@ def access_priv_ok(server_obj, required_privilege_level): if threading.current_thread().__class__.__name__ == '_MainThread': # Server methods may be called internally as well as by clients. return True - caller = server_obj.getLocalStorage().caller - client_privilege_level = caller.privilege_level - return (PRIVILEGE_LEVELS.index(client_privilege_level) >= + import cherrypy + user = cherrypy.request.login + priv_level = get_priv_level(user) + return (PRIVILEGE_LEVELS.index(priv_level) >= PRIVILEGE_LEVELS.index(required_privilege_level)) @@ -74,13 +86,68 @@ def check_access_priv(server_obj, required_privilege_level): if threading.current_thread().__class__.__name__ == '_MainThread': # Server methods may be called internally as well as by clients. return - caller = server_obj.getLocalStorage().caller - client_privilege_level = caller.privilege_level - if not (PRIVILEGE_LEVELS.index(client_privilege_level) >= + auth_user, prog_name, user, host, uuid, priv_level = get_client_info() + if not (PRIVILEGE_LEVELS.index(priv_level) >= PRIVILEGE_LEVELS.index(required_privilege_level)): err = CONNECT_DENIED_PRIV_TMPL % ( - client_privilege_level, required_privilege_level, - caller.user, caller.host, caller.prog_name, caller.uuid) + priv_level, required_privilege_level, + user, host, prog_name, uuid + ) getLogger("log").warn(err) # Raise an exception to be sent back to the client. raise Exception(err) + + +def get_client_info(): + """Return information about the most recent cherrypy request, if any.""" + import cherrypy + import uuid + auth_user = cherrypy.request.login + info = cherrypy.request.headers + origin_string = info.get("User-Agent", "") + origin_props = {} + if origin_string: + try: + origin_props = dict( + [_.split("/", 1) for _ in origin_string.split()] + ) + except ValueError: + pass + prog_name = origin_props.get("prog_name", "Unknown") + uuid = origin_props.get("uuid", uuid.uuid4()) + if info.get("From") and "@" in info["From"]: + user, host = info["From"].split("@") + else: + user, host = ("Unknown", "Unknown") + priv_level = get_priv_level(auth_user) + return auth_user, prog_name, user, host, uuid, priv_level + + +def get_client_connection_denied(): + """Return whether a connection was denied.""" + import cherrypy + if "Authorization" not in cherrypy.request.headers: + # Probably just the initial HTTPS handshake. + return False + status = cherrypy.response.status + if isinstance(status, basestring): + return cherrypy.response.status.split()[0] in ["401", "403"] + return cherrypy.response.status in [401, 403] + + +def get_priv_level(user): + """Get the privilege level for this authenticated user.""" + if user == "cylc": + return PRIVILEGE_LEVELS[-1] + from cylc.config import SuiteConfig + config = SuiteConfig.get_inst() + return config.cfg['cylc']['authentication']['public'] + + +def handle_proxies(): + """Unset proxies if the configuration matches this.""" + from cylc.cfgspec.globalcfg import GLOBAL_CFG + if not GLOBAL_CFG.get(['communication', 'proxies on']): + import os + os.environ.pop("http_proxy", None) + os.environ.pop("https_proxy", None) diff --git a/lib/cylc/network/connection_validator.py b/lib/cylc/network/connection_validator.py deleted file mode 100644 index 72b7b4d9a53..00000000000 --- a/lib/cylc/network/connection_validator.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import Pyro.core -import hashlib -import os -import sys - -from Pyro.protocol import DefaultConnValidator -import Pyro.constants -import Pyro.errors -import hmac - -from cylc.cfgspec.globalcfg import GLOBAL_CFG -from cylc.network import NO_PASSPHRASE, PRIVILEGE_LEVELS -from cylc.config import SuiteConfig -from cylc.suite_host import get_hostname, is_remote_host -from cylc.suite_logging import LOG -from cylc.owner import USER - - -# Access for users without the suite passphrase: encrypting the "no passphrase" -# passphrase is unnecessary, but doing so allows common passphrase handling. - -OK_HASHES = GLOBAL_CFG.get()['authentication']['hashes'] -SCAN_HASH = GLOBAL_CFG.get()['authentication']['scan hash'] -if SCAN_HASH not in OK_HASHES: - OK_HASHES.append(SCAN_HASH) - - -CONNECT_DENIED_TMPL = "[client-connect] DENIED %s@%s:%s %s" -CONNECT_ALLOWED_TMPL = "[client-connect] %s@%s:%s privilege='%s' %s" - - -class ConnValidator(DefaultConnValidator): - """Custom Pyro connection validator for user authentication.""" - - HASHES = {} - LENGTH_HASH_DIGESTS = {} - NO_PASSPHRASE_HASHES = {} - - def set_pphrase(self, pphrase): - """Store encrypted suite passphrase (called by the server).""" - self.pphrase_hashes = {} - for hash_name in OK_HASHES: - hash_ = self._get_hash(hash_name) - self.pphrase_hashes[hash_name] = hash_(pphrase).hexdigest() - - def set_default_hash(self, hash_name): - """Configure a hash to use as the default.""" - self._default_hash_name = hash_name - if None in self.HASHES: - self.HASHES.pop(None) # Pop default setting. - - def acceptIdentification(self, daemon, connection, token, challenge): - """Authorize client login.""" - - is_old_client = False - # Processes the token returned by createAuthToken. - try: - user, host, uuid, prog_name, proc_passwd = token.split(':', 4) - except ValueError: - # Back compat for old suite client (passphrase only) - # (Allows old scan to see new suites.) - proc_passwd = token - is_old_client = True - user = "(user)" - host = "(host)" - uuid = "(uuid)" - prog_name = "(OLD_CLIENT)" - - hash_name = self._get_hash_name_from_digest_length(proc_passwd) - - if hash_name not in OK_HASHES: - return (0, Pyro.constants.DENIED_SECURITY) - - hash_ = self._get_hash(hash_name) - - # Access for users without the suite passphrase: encrypting the - # no-passphrase is unnecessary, but doing so allows common handling. - no_passphrase_hash = self._get_no_passphrase_hash(hash_name) - - # Check username and password, and set privilege level accordingly. - # The auth token has a binary hash that needs conversion to ASCII. - if self._compare_hmacs( - hmac.new(challenge, - self.pphrase_hashes[hash_name].decode("hex"), - hash_).digest(), - proc_passwd): - # The client has the suite passphrase. - # Access granted at highest privilege level. - priv_level = PRIVILEGE_LEVELS[-1] - elif not is_old_client and self._compare_hmacs( - hmac.new(challenge, - no_passphrase_hash.decode("hex"), - hash_).digest(), - proc_passwd): - # The client does not have the suite passphrase. - # Public access granted at level determined by global/suite config. - config = SuiteConfig.get_inst() - priv_level = config.cfg['cylc']['authentication']['public'] - else: - # Access denied. - if not is_old_client: - # Avoid logging large numbers of denials from old scan clients - # that try all passphrases available to them. - LOG.warn(CONNECT_DENIED_TMPL % (user, host, prog_name, uuid)) - return (0, Pyro.constants.DENIED_SECURITY) - - # Store client details for use in the connection thread. - connection.user = user - connection.host = host - connection.prog_name = prog_name - connection.uuid = uuid - connection.privilege_level = priv_level - LOG.debug(CONNECT_ALLOWED_TMPL % ( - user, host, prog_name, priv_level, uuid)) - return (1, 0) - - def createAuthToken(self, authid, challenge, peeraddr, URI, daemon): - """Return a secure auth token based on the server challenge string. - - Argument authid is what's returned by mungeIdent(). - - """ - hash_ = self._get_hash() - return ":".join( - list(authid[:4]) + - [hmac.new(challenge, authid[4], hash_).digest()] - ) - - def mungeIdent(self, ident): - """Receive (uuid, passphrase) from client. Encrypt the passphrase. - - Also pass client identification info to server for logging: - (user, host, prog name). - - """ - hash_ = self._get_hash() - uuid, passphrase = ident - prog_name = os.path.basename(sys.argv[0]) - if passphrase is None: - passphrase = NO_PASSPHRASE - return (USER, get_hostname(), str(uuid), prog_name, - hash_(passphrase).digest()) - - def _compare_hmacs(self, hmac1, hmac2): - """Compare hmacs as securely as possible.""" - try: - return hmac.compare_hmacs(hmac1, hmac2) - except AttributeError: - # < Python 2.7.7. - return (hmac1 == hmac2) - - def _get_default_hash_name(self): - if hasattr(self, "_default_hash_name"): - return self._default_hash_name - return GLOBAL_CFG.get()['authentication']['hashes'][0] - - def _get_hash(self, hash_name=None): - try: - return self.HASHES[hash_name] - except KeyError: - pass - - hash_name_dest = hash_name - if hash_name is None: - hash_name = self._get_default_hash_name() - - self.HASHES[hash_name_dest] = getattr(hashlib, hash_name) - return self.HASHES[hash_name_dest] - - def _get_hash_name_from_digest_length(self, digest): - if len(digest) in self.LENGTH_HASH_DIGESTS: - return self.LENGTH_HASH_DIGESTS[len(digest)] - for hash_name in OK_HASHES: - hash_ = self._get_hash(hash_name) - len_hash = len(hash_("foo").digest()) - self.LENGTH_HASH_DIGESTS[len_hash] = hash_name - if len_hash == len(digest): - return hash_name - - def _get_no_passphrase_hash(self, hash_name=None): - try: - return self.NO_PASSPHRASE_HASHES[hash_name] - except KeyError: - hash_ = self._get_hash(hash_name) - self.NO_PASSPHRASE_HASHES[hash_name] = ( - hash_(NO_PASSPHRASE).hexdigest()) - return self.NO_PASSPHRASE_HASHES[hash_name] diff --git a/lib/cylc/network/daemon.py b/lib/cylc/network/daemon.py new file mode 100644 index 00000000000..8e9f1845d24 --- /dev/null +++ b/lib/cylc/network/daemon.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + +if METHOD == "https": + from cylc.network.https.daemon import CommsDaemon diff --git a/lib/cylc/network/ext_trigger_client.py b/lib/cylc/network/ext_trigger_client.py new file mode 100644 index 00000000000..8dff6ed8871 --- /dev/null +++ b/lib/cylc/network/ext_trigger_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.ext_trigger_client import ExtTriggerClient diff --git a/lib/cylc/network/ext_trigger_server.py b/lib/cylc/network/ext_trigger_server.py new file mode 100644 index 00000000000..6636d9b32a8 --- /dev/null +++ b/lib/cylc/network/ext_trigger_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.ext_trigger_server import ExtTriggerServer diff --git a/lib/cylc/network/https/__init__.py b/lib/cylc/network/https/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/cylc/network/https/base_client.py b/lib/cylc/network/https/base_client.py new file mode 100644 index 00000000000..b1c37379f93 --- /dev/null +++ b/lib/cylc/network/https/base_client.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Base classes for web clients.""" + +import os +import sys +from uuid import uuid4 +import warnings + +# Ignore incorrect SSL certificate warning from urllib3 via requests. +warnings.filterwarnings("ignore", "Certificate has no `subjectAltName`") + +from cylc.exceptions import PortFileError +import cylc.flags +from cylc.network import ConnectionDeniedError, NO_PASSPHRASE, handle_proxies +from cylc.owner import is_remote_user, USER +from cylc.registration import RegistrationDB, PassphraseError +from cylc.suite_host import get_hostname, is_remote_host +from cylc.suite_env import CylcSuiteEnv, CylcSuiteEnvLoadError +from cylc.version import CYLC_VERSION + + +WARNING_NO_HTTPS_SUPPORT = ( + "WARNING: server has no HTTPS support," + + " falling back to HTTP: {0}\n" +) + + +class BaseCommsClient(object): + """Base class for client-side suite object interfaces.""" + + ACCESS_DESCRIPTION = 'private' + METHOD = 'POST' + METHOD_POST = 'POST' + METHOD_GET = 'GET' + + def __init__(self, suite, owner=USER, host=None, timeout=None, + port=None, db=None, my_uuid=None, print_uuid=False): + self.suite = suite + self.host = host + self.owner = owner + if timeout is not None: + timeout = float(timeout) + self.timeout = timeout + self.port = port + self.my_uuid = my_uuid or uuid4() + if print_uuid: + print >> sys.stderr, '%s' % self.my_uuid + self.reg_db = RegistrationDB(db) + self.prog_name = os.path.basename(sys.argv[0]) + + def call_server_func(self, category, fname, **fargs): + """Call server_object.fname(*fargs, **fargs).""" + if self.host is None or self.port is None: + self._load_contact_info() + handle_proxies() + payload = fargs.pop("payload", None) + method = fargs.pop("method", self.METHOD) + host = self.host + if not self.host.split(".")[0].isdigit(): + host = self.host.split(".")[0] + if host == "localhost": + host = get_hostname().split(".")[0] + url = 'https://%s:%s/%s/%s' % ( + host, self.port, category, fname + ) + if fargs: + import urllib + params = urllib.urlencode(fargs, doseq=True) + url += "?" + params + return self.get_data_from_url(url, payload, method=method) + + def get_data_from_url(self, url, json_data, method=None): + requests_ok = True + try: + import requests + except ImportError: + requests_ok = False + else: + version = [int(_) for _ in requests.__version__.split(".")] + if version < [2, 4, 2]: + requests_ok = False + if requests_ok: + return self.get_data_from_url_with_requests( + url, json_data, method=method) + return self.get_data_from_url_with_urllib2( + url, json_data, method=method) + + def get_data_from_url_with_requests(self, url, json_data, method=None): + import requests + username, password = self._get_auth() + auth = requests.auth.HTTPDigestAuth(username, password) + if not hasattr(self, "session"): + self.session = requests.Session() + if method is None: + method = self.METHOD + if method == self.METHOD_POST: + session_method = self.session.post + else: + session_method = self.session.get + try: + ret = session_method( + url, + json=json_data, + verify=self._get_verify(), + proxies={}, + headers=self._get_headers(), + auth=auth, + timeout=self.timeout + ) + except requests.exceptions.SSLError as exc: + if "unknown protocol" in str(exc) and url.startswith("https:"): + # Server is using http rather than https, for some reason. + sys.stderr.write(WARNING_NO_HTTPS_SUPPORT.format(exc)) + return self.get_data_from_url_with_requests( + url.replace("https:", "http:", 1), json_data) + raise ConnectionDeniedError(url, self.prog_name, + self.ACCESS_DESCRIPTION) + except requests.exceptions.ConnectionError: + raise ConnectionDeniedError(url, self.prog_name, + self.ACCESS_DESCRIPTION) + if ret.status_code == 401: + raise ConnectionDeniedError(url, self.prog_name, + self.ACCESS_DESCRIPTION) + if ret.status_code >= 400: + from cylc.network.https.util import get_exception_from_html + exception_text = get_exception_from_html(ret.text) + if exception_text: + sys.stderr.write(exception_text) + else: + sys.stderr.write(ret.text) + ret.raise_for_status() + try: + return ret.json() + except ValueError: + return ret.text + + def get_data_from_url_with_urllib2(self, url, json_data, method=None): + import json + import urllib2 + import ssl + if hasattr(ssl, '_create_unverified_context'): + ssl._create_default_https_context = ssl._create_unverified_context + if method is None: + method = self.METHOD + orig_json_data = json_data + username, password = self._get_auth() + auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + auth_manager.add_password(None, url, username, password) + auth = urllib2.HTTPDigestAuthHandler(auth_manager) + opener = urllib2.build_opener(auth, urllib2.HTTPSHandler()) + headers_list = self._get_headers().items() + if json_data: + json_data = json.dumps(json_data) + headers_list.append(('Accept', 'application/json')) + json_headers = {'Content-Type': 'application/json', + 'Content-Length': len(json_data)} + else: + json_data = None + json_headers = {'Content-Length': 0} + opener.addheaders = headers_list + req = urllib2.Request(url, json_data, json_headers) + + # This is an unpleasant monkey patch, but there isn't an alternative. + # urllib2 uses POST iff there is a data payload, but that is not the + # correct criterion. The difference is basically that POST changes + # server state and GET doesn't. + req.get_method = lambda: method + try: + response = opener.open(req, timeout=self.timeout) + except urllib2.URLError as exc: + if "unknown protocol" in str(exc) and url.startswith("https:"): + # Server is using http rather than https, for some reason. + sys.stderr.write(WARNING_NO_HTTPS_SUPPORT.format(exc)) + return self.get_data_from_url_with_urllib2( + url.replace("https:", "http:", 1), orig_json_data) + raise ConnectionDeniedError(url, self.prog_name, + self.ACCESS_DESCRIPTION) + + if response.getcode() == 401: + raise ConnectionDeniedError(url, self.prog_name, + self.ACCESS_DESCRIPTION) + response_text = response.read() + if response.getcode() >= 400: + from cylc.network.https.util import get_exception_from_html + exception_text = get_exception_from_html(response_text) + if exception_text: + sys.stderr.write(exception_text) + else: + sys.stderr.write(response_text) + raise Exception("%s HTTP return code" % response.getcode()) + try: + return json.loads(response_text) + except ValueError: + return response_text + + def _get_auth(self): + """Return a user/password Digest Auth.""" + self.pphrase = self.reg_db.load_passphrase( + self.suite, self.owner, self.host) + if self.pphrase: + self.reg_db.cache_passphrase( + self.suite, self.owner, self.host, self.pphrase) + if self.pphrase is None: + return 'anon', NO_PASSPHRASE + return 'cylc', self.pphrase + + def _get_headers(self): + """Return HTTP headers identifying the client.""" + user_agent_string = ( + "cylc/%s prog_name/%s uuid/%s" % ( + CYLC_VERSION, self.prog_name, self.my_uuid + ) + ) + auth_info = "%s@%s" % (USER, get_hostname()) + return {"User-Agent": user_agent_string, + "From": auth_info} + + def _get_verify(self): + """Return the server certificate if possible.""" + if not hasattr(self, "server_cert"): + try: + self.server_cert = self.reg_db.load_item( + self.suite, self.owner, self.host, "certificate") + except PassphraseError: + return False + return self.server_cert + + def _load_contact_info(self): + """Obtain URL info. + + Determine host and port using content in port file, unless already + specified. + + """ + if ((self.host is None or self.port is None) and + 'CYLC_SUITE_RUN_DIR' in os.environ): + # Looks like we are in a running task job, so we should be able to + # use "cylc-suite-env" file under the suite running directory + try: + suite_env = CylcSuiteEnv.load( + self.suite, os.environ['CYLC_SUITE_RUN_DIR']) + except CylcSuiteEnvLoadError: + if cylc.flags.debug: + import traceback + traceback.print_exc() + else: + self.host = suite_env.suite_host + self.port = suite_env.suite_port + self.owner = suite_env.suite_owner + + if self.host is None or self.port is None: + from cylc.cfgspec.globalcfg import GLOBAL_CFG + port_file_path = os.path.join( + GLOBAL_CFG.get( + ['communication', 'ports directory']), self.suite) + if is_remote_host(self.host) or is_remote_user(self.owner): + import shlex + from subprocess import Popen, PIPE + ssh_tmpl = str(GLOBAL_CFG.get_host_item( + 'remote shell template', self.host, self.owner)) + ssh_tmpl = ssh_tmpl.replace(' %s', '') + user_at_host = '' + if self.owner: + user_at_host = self.owner + '@' + if self.host: + user_at_host += self.host + else: + user_at_host += 'localhost' + r_port_file_path = port_file_path.replace( + os.environ['HOME'], '$HOME') + command = shlex.split(ssh_tmpl) + [ + user_at_host, 'cat', r_port_file_path] + proc = Popen(command, stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + ret_code = proc.wait() + if ret_code: + if cylc.flags.debug: + print >> sys.stderr, { + "code": ret_code, + "command": command, + "stdout": out, + "stderr": err} + raise PortFileError( + "Port file '%s:%s' not found - suite not running?." % + (user_at_host, r_port_file_path)) + else: + try: + out = open(port_file_path).read() + except IOError: + raise PortFileError( + "Port file '%s' not found - suite not running?." % + (port_file_path)) + lines = out.splitlines() + try: + if self.port is None: + self.port = int(lines[0]) + except (IndexError, ValueError): + raise PortFileError( + "ERROR, bad content in port file: %s" % port_file_path) + if self.host is None: + if len(lines) >= 2: + self.host = lines[1].strip() + else: + self.host = get_hostname() + + def reset(self, *args, **kwargs): + pass + + def signout(self, *args, **kwargs): + pass + + +class BaseCommsClientAnon(BaseCommsClient): + + """Anonymous access class for clients.""" + + ACCESS_DESCRIPTION = 'public' + + def __init__(self, *args, **kwargs): + # We don't necessarily have certificate access for anon suites. + warnings.filterwarnings("ignore", "Unverified HTTPS request") + super(BaseCommsClientAnon, self).__init__(*args, **kwargs) + + def _get_auth(self): + """Return a user/password Digest Auth.""" + return 'anon', NO_PASSPHRASE + + def _get_verify(self): + """Other suites' certificates may not be accessible.""" + return False diff --git a/lib/cylc/network/https/base_server.py b/lib/cylc/network/https/base_server.py new file mode 100644 index 00000000000..000bfadd2b4 --- /dev/null +++ b/lib/cylc/network/https/base_server.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Base classes for HTTPS subsystem server.""" + +from cylc.network.https.client_reporter import CommsClientReporter + + +class BaseCommsServer(object): + """Base class for server-side suite object interfaces.""" + + def __init__(self): + self.client_reporter = CommsClientReporter.get_inst() + + def signout(self): + """Wrap client_reporter.signout.""" + self.client_reporter.signout(self) + + def report(self, command): + """Wrap client_reporter.report.""" + self.client_reporter.report(command, self) diff --git a/lib/cylc/network/client_reporter.py b/lib/cylc/network/https/client_reporter.py similarity index 78% rename from lib/cylc/network/client_reporter.py rename to lib/cylc/network/https/client_reporter.py index 56c24f98d90..6760966ec83 100644 --- a/lib/cylc/network/client_reporter.py +++ b/lib/cylc/network/https/client_reporter.py @@ -23,8 +23,10 @@ import cylc.flags from cylc.suite_logging import LOG +from cylc.network import get_client_info, get_client_connection_denied -class PyroClientReporter(object): + +class CommsClientReporter(object): """For logging cylc client requests with identifying information.""" _INSTANCE = None @@ -35,6 +37,8 @@ class PyroClientReporter(object): LOG_IDENTIFY_TMPL = '[client-identify] %d id requests in PT%dS' LOG_SIGNOUT_TMPL = '[client-sign-out] %s@%s:%s %s' LOG_FORGET_TMPL = '[client-forget] %s' + LOG_CONNECT_DENIED_TMPL = "[client-connect] DENIED %s@%s:%s %s" + LOG_CONNECT_ALLOWED_TMPL = "[client-connect] %s@%s:%s privilege='%s' %s" @classmethod def get_inst(cls): @@ -58,24 +62,27 @@ def report(self, request, server_obj): if threading.current_thread().__class__.__name__ == '_MainThread': # Server methods may be called internally as well as by clients. return + auth_user, prog_name, user, host, uuid, priv_level = get_client_info() name = server_obj.__class__.__name__ - caller = server_obj.getLocalStorage().caller log_me = ( cylc.flags.debug or name in ["SuiteCommandServer", "ExtTriggerServer", "BroadcastServer"] or (name not in ["SuiteIdServer", "TaskMessageServer"] and - caller.uuid not in self.clients)) + uuid not in self.clients)) if log_me: + LOG.debug( + self.__class__.LOG_CONNECT_ALLOWED_TMPL % ( + user, host, prog_name, priv_level, uuid) + ) LOG.info( self.__class__.LOG_COMMAND_TMPL % ( - request, caller.user, caller.host, caller.prog_name, - caller.uuid)) + request, user, host, prog_name, uuid)) if name == "SuiteIdServer": self._num_id_requests += 1 self.report_id_requests() - self.clients[caller.uuid] = datetime.datetime.utcnow() + self.clients[uuid] = datetime.datetime.utcnow() self._housekeep() def report_id_requests(self): @@ -97,6 +104,24 @@ def report_id_requests(self): self._id_start_time = current_time self._num_id_requests = 0 + def report_connection_if_denied(self): + """Log an (un?)successful connection attempt.""" + try: + (auth_user, prog_name, user, host, uuid, + priv_level) = get_client_info() + except Exception: + LOG.warn( + self.__class__.LOG_CONNECT_DENIED_TMPL % ( + "unknown", "unknown", "unknown", "unknown") + ) + return + connection_denied = get_client_connection_denied() + if connection_denied: + LOG.warn( + self.__class__.LOG_CONNECT_DENIED_TMPL % ( + user, host, prog_name, uuid) + ) + def signout(self, server_obj): """Force forget this client (for use by GUI etc.).""" diff --git a/lib/cylc/network/https/daemon.py b/lib/cylc/network/https/daemon.py new file mode 100644 index 00000000000..c0e9e209cb9 --- /dev/null +++ b/lib/cylc/network/https/daemon.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap HTTPS daemon for a suite.""" + +import binascii +import os +import socket +import sys +import traceback + +from cylc.cfgspec.globalcfg import GLOBAL_CFG +from cylc.network import NO_PASSPHRASE +from cylc.network.https.client_reporter import CommsClientReporter +from cylc.owner import USER +from cylc.registration import RegistrationDB, PassphraseError +from cylc.suite_host import get_hostname + +import cherrypy + + +class CommsDaemon(object): + """Wrap HTTPS daemon for a suite.""" + + def __init__(self, suite, suite_dir): + # Suite only needed for back-compat with old clients (see below): + self.suite = suite + + # Figure out the ports we are allowed to use. + base_port = GLOBAL_CFG.get( + ['communication', 'base port']) + max_ports = GLOBAL_CFG.get( + ['communication', 'maximum number of ports']) + self.ok_ports = range( + int(base_port), + int(base_port) + int(max_ports) + ) + + comms_options = GLOBAL_CFG.get(['communication', 'options']) + # HTTP Digest Auth uses MD5 - pretty secure in this use case. + # Extending it with extra algorithms is allowed, but won't be + # supported by most browsers. requests and urllib2 are OK though. + self.hash_algorithm = "MD5" + if "SHA1" in comms_options: + # Note 'SHA' rather than 'SHA1'. + self.hash_algorithm = "SHA" + + self.reg_db = RegistrationDB() + try: + self.cert = self.reg_db.load_item( + suite, USER, None, "certificate", create_ok=True) + self.pkey = self.reg_db.load_item( + suite, USER, None, "private_key", create_ok=True) + except PassphraseError: + # No OpenSSL installed. + self.cert = None + self.pkey = None + self.suite = suite + passphrase = self.reg_db.load_passphrase(suite, USER, None) + userpassdict = {'cylc': passphrase, 'anon': NO_PASSPHRASE} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain( + userpassdict, algorithm=self.hash_algorithm) + self.get_ha1 = get_ha1 + del passphrase + del userpassdict + self.client_reporter = CommsClientReporter.get_inst() + self.start() + + def start(self): + _ws_init(self) + + def shutdown(self): + """Shutdown the daemon.""" + if hasattr(self, "engine"): + self.engine.exit() + self.engine.block() + + def connect(self, obj, name): + """Connect obj and name to the daemon.""" + import cherrypy + cherrypy.tree.mount(obj, "/" + name) + + def disconnect(self, obj): + """Disconnect obj from the daemon.""" + pass + + def get_port(self): + """Return the daemon port.""" + return self.port + + def report_connection_if_denied(self): + self.client_reporter.report_connection_if_denied() + + +def can_ssl(): + """Return whether we can run HTTPS under cherrypy on this machine.""" + try: + from OpenSSL import SSL + from OpenSSL import crypto + except ImportError: + return False + return True + + +def _ws_init(service_inst, *args, **kwargs): + """Start quick web service.""" + # cherrypy.config["tools.encode.on"] = True + # cherrypy.config["tools.encode.encoding"] = "utf-8" + cherrypy.config["server.socket_host"] = '0.0.0.0' + cherrypy.config["engine.autoreload.on"] = False + if can_ssl(): + cherrypy.config['server.ssl_module'] = 'pyopenSSL' + cherrypy.config['server.ssl_certificate'] = service_inst.cert + cherrypy.config['server.ssl_private_key'] = service_inst.pkey + else: + sys.stderr.write("WARNING: no HTTPS support: cannot import OpenSSL\n") + cherrypy.config['log.screen'] = None + key = binascii.hexlify(os.urandom(16)) + cherrypy.config.update({ + 'tools.auth_digest.on': True, + 'tools.auth_digest.realm': service_inst.suite, + 'tools.auth_digest.get_ha1': service_inst.get_ha1, + 'tools.auth_digest.key': key, + 'tools.auth_digest.algorithm': service_inst.hash_algorithm + }) + cherrypy.tools.connect_log = cherrypy.Tool( + 'on_end_resource', service_inst.report_connection_if_denied) + cherrypy.config['tools.connect_log.on'] = True + host = get_hostname() + service_inst.engine = cherrypy.engine + for port in service_inst.ok_ports: + my_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + my_socket.bind((host, port)) + except socket.error: + # Host busy. + my_socket.close() + continue + my_socket.close() + cherrypy.config["server.socket_port"] = port + try: + cherrypy.engine.start() + cherrypy.engine.wait(cherrypy.engine.states.STARTED) + if cherrypy.engine.state != cherrypy.engine.states.STARTED: + continue + except (socket.error, IOError): + pass + except: + import traceback + traceback.print_exc() + else: + service_inst.port = port + return + # We need to reinitialise the httpserver for each port attempt. + cherrypy.server.httpserver = None + raise Exception("No available ports") diff --git a/lib/cylc/network/https/ext_trigger_client.py b/lib/cylc/network/https/ext_trigger_client.py new file mode 100644 index 00000000000..09c03049741 --- /dev/null +++ b/lib/cylc/network/https/ext_trigger_client.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +from time import sleep +from cylc.network import COMMS_EXT_TRIG_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient +from cylc.suite_logging import OUT, ERR + + +class ExtTriggerClient(BaseCommsClient): + """Client-side external trigger interface.""" + + MAX_N_TRIES = 5 + RETRY_INTVL_SECS = 10.0 + + MSG_SEND_FAILED = "Send message: try %s of %s failed" + MSG_SEND_RETRY = "Retrying in %s seconds, timeout is %s" + MSG_SEND_SUCCEED = "Send message: try %s of %s succeeded" + + def put(self, event_message, event_id): + return self.call_server_func(COMMS_EXT_TRIG_OBJ_NAME, "put", + event_message=event_message, + event_id=event_id) + + def send_retry(self, event_message, event_id, + max_n_tries, retry_intvl_secs): + """CLI external trigger interface.""" + + max_n_tries = int(max_n_tries or self.__class__.MAX_N_TRIES) + retry_intvl_secs = float( + retry_intvl_secs or self.__class__.RETRY_INTVL_SECS) + + sent = False + i_try = 0 + while not sent and i_try < max_n_tries: + i_try += 1 + try: + self.put(event_message, event_id) + except Exception as exc: + ERR.error(exc) + OUT.info(self.__class__.MSG_SEND_FAILED % ( + i_try, + max_n_tries, + )) + if i_try >= max_n_tries: + break + OUT.info(self.__class__.MSG_SEND_RETRY % ( + retry_intvl_secs, + self.timeout + )) + sleep(retry_intvl_secs) + else: + if i_try > 1: + OUT.info(self.__class__.MSG_SEND_SUCCEEDED % ( + i_try, + max_n_tries + )) + sent = True + break + if not sent: + sys.exit('ERROR: send failed') + return sent diff --git a/lib/cylc/network/ext_trigger.py b/lib/cylc/network/https/ext_trigger_server.py similarity index 56% rename from lib/cylc/network/ext_trigger.py rename to lib/cylc/network/https/ext_trigger_server.py index f21d6380ba5..5a949730fb0 100644 --- a/lib/cylc/network/ext_trigger.py +++ b/lib/cylc/network/https/ext_trigger_server.py @@ -16,20 +16,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -from time import sleep +import cherrypy + from Queue import Queue, Empty -import Pyro.errors import cylc.flags -from cylc.network import PYRO_EXT_TRIG_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer -from cylc.network.suite_broadcast import BroadcastServer +from cylc.network.https.base_server import BaseCommsServer +from cylc.network.https.suite_broadcast_server import BroadcastServer from cylc.network import check_access_priv -from cylc.suite_logging import OUT, ERR from cylc.task_id import TaskID -class ExtTriggerServer(PyroServer): +class ExtTriggerServer(BaseCommsServer): """Server-side external trigger interface.""" _INSTANCE = None @@ -45,6 +42,8 @@ def __init__(self): super(ExtTriggerServer, self).__init__() self.queue = Queue() + @cherrypy.expose + @cherrypy.tools.json_out() def put(self, event_message, event_id): """Server-side external event trigger interface.""" @@ -90,72 +89,11 @@ def retrieve(self, itask): 'environment': { 'CYLC_EXT_TRIGGER_ID': qid } - }] + }], + not_from_client=True ) used.append((qmsg, qid)) break for q in queued: if q not in used: self.queue.put(q) - - -class ExtTriggerClient(PyroClient): - """Client-side external trigger interface.""" - - target_server_object = PYRO_EXT_TRIG_OBJ_NAME - - MAX_N_TRIES = 5 - RETRY_INTVL_SECS = 10.0 - - MSG_SEND_FAILED = "Send message: try %s of %s failed" - MSG_SEND_RETRY = "Retrying in %s seconds, timeout is %s" - MSG_SEND_SUCCEED = "Send message: try %s of %s succeeded" - - def put(self, *args): - return self.call_server_func("put", *args) - - def send_retry(self, event_message, event_id, - max_n_tries, retry_intvl_secs): - """CLI external trigger interface.""" - - max_n_tries = int(max_n_tries or self.__class__.MAX_N_TRIES) - retry_intvl_secs = float( - retry_intvl_secs or self.__class__.RETRY_INTVL_SECS) - - sent = False - i_try = 0 - while not sent and i_try < max_n_tries: - i_try += 1 - try: - self.put(event_message, event_id) - except Pyro.errors.NamingError as exc: - ERR.error(sys.stderr, exc) - OUT.info(self.__class__.MSG_SEND_FAILED % ( - i_try, - max_n_tries, - )) - break - except Exception as exc: - ERR.error(sys.stderr, exc) - OUT.info(self.__class__.MSG_SEND_FAILED % ( - i_try, - max_n_tries, - )) - if i_try >= max_n_tries: - break - OUT.info(self.__class__.MSG_SEND_RETRY % ( - retry_intvl_secs, - self.pyro_timeout - )) - sleep(retry_intvl_secs) - else: - if i_try > 1: - OUT.info(self.__class__.MSG_SEND_SUCCEEDED % ( - i_try, - max_n_tries - )) - sent = True - break - if not sent: - sys.exit('ERROR: send failed') - return sent diff --git a/lib/cylc/network/https/port_scan.py b/lib/cylc/network/https/port_scan.py new file mode 100644 index 00000000000..3241d5db405 --- /dev/null +++ b/lib/cylc/network/https/port_scan.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Port scan utilities.""" + +from multiprocessing import cpu_count, Pool +import sys +from time import sleep +import traceback +from uuid import uuid4 + +from cylc.cfgspec.globalcfg import GLOBAL_CFG +import cylc.flags +from cylc.network import NO_PASSPHRASE, ConnectionDeniedError +from cylc.network.https.suite_state_client import SuiteStillInitialisingError +from cylc.network.https.suite_identifier_client import ( + SuiteIdClientAnon, SuiteIdClient) +from cylc.owner import USER +from cylc.registration import RegistrationDB +from cylc.suite_host import get_hostname, is_remote_host + + +def scan(host=None, db=None, timeout=None): + """Scan ports, return a list of suites found: [(port, suite.identify())]. + + Note that we could easily scan for a given suite+owner and return its + port instead of reading port files, but this may not always be fast enough. + """ + if host is None: + host = get_hostname() + base_port = GLOBAL_CFG.get( + ['communication', 'base port']) + last_port = base_port + GLOBAL_CFG.get( + ['communication', 'maximum number of ports']) + if timeout: + timeout = float(timeout) + else: + timeout = None + + reg_db = RegistrationDB(db) + results = [] + my_uuid = uuid4() + for port in range(base_port, last_port): + client = SuiteIdClientAnon(None, host=host, port=port, my_uuid=my_uuid) + try: + result = (port, client.identify()) + except ConnectionDeniedError as exc: + if cylc.flags.debug: + traceback.print_exc() + continue + except Exception as exc: + if cylc.flags.debug: + traceback.print_exc() + raise + else: + owner = result[1].get('owner') + name = result[1].get('name') + states = result[1].get('states', None) + if cylc.flags.debug: + print ' suite:', name, owner + if states is None: + # This suite keeps its state info private. + # Try again with the passphrase if I have it. + pphrase = reg_db.load_passphrase(name, owner, host) + if pphrase: + client = SuiteIdClient(name, owner=owner, host=host, + port=port, my_uuid=my_uuid, + timeout=timeout) + try: + result = (port, client.identify()) + except Exception: + # Nope (private suite, wrong passphrase). + if cylc.flags.debug: + print ' (wrong passphrase)' + else: + reg_db.cache_passphrase( + name, owner, host, pphrase) + if cylc.flags.debug: + print ' (got states with passphrase)' + results.append(result) + return results + + +def scan_all(hosts=None, reg_db_path=None, timeout=None): + """Scan all hosts.""" + if not hosts: + hosts = GLOBAL_CFG.get(["suite host scanning", "hosts"]) + # Ensure that it does "localhost" only once + hosts = set(hosts) + for host in list(hosts): + if not is_remote_host(host): + hosts.remove(host) + hosts.add("localhost") + proc_pool_size = GLOBAL_CFG.get(["process pool size"]) + if proc_pool_size is None: + proc_pool_size = cpu_count() + if proc_pool_size > len(hosts): + proc_pool_size = len(hosts) + proc_pool = Pool(proc_pool_size) + async_results = {} + for host in hosts: + async_results[host] = proc_pool.apply_async( + scan, [host, reg_db_path, timeout]) + proc_pool.close() + scan_results = [] + scan_results_hosts = [] + while async_results: + sleep(0.05) + for host, async_result in async_results.items(): + if async_result.ready(): + async_results.pop(host) + try: + res = async_result.get() + except Exception: + if cylc.flags.debug: + traceback.print_exc() + else: + scan_results.extend(res) + scan_results_hosts.extend([host] * len(res)) + proc_pool.join() + return zip(scan_results_hosts, scan_results) diff --git a/lib/cylc/network/https/suite_broadcast_client.py b/lib/cylc/network/https/suite_broadcast_client.py new file mode 100644 index 00000000000..35bf5890e2f --- /dev/null +++ b/lib/cylc/network/https/suite_broadcast_client.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cylc.network import COMMS_BCAST_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient + + +class BroadcastClient(BaseCommsClient): + """Client-side suite broadcast interface.""" + + def broadcast(self, cmd, **kwargs): + if cmd == "get": + return self.call_server_func(COMMS_BCAST_OBJ_NAME, cmd, + method=self.METHOD_GET, **kwargs) + if cmd == "expire": + return self.call_server_func(COMMS_BCAST_OBJ_NAME, cmd, **kwargs) + return self.call_server_func( + COMMS_BCAST_OBJ_NAME, cmd, payload=kwargs) diff --git a/lib/cylc/network/suite_broadcast.py b/lib/cylc/network/https/suite_broadcast_server.py similarity index 89% rename from lib/cylc/network/suite_broadcast.py rename to lib/cylc/network/https/suite_broadcast_server.py index 97c96bb8934..9259a6b0cc0 100644 --- a/lib/cylc/network/suite_broadcast.py +++ b/lib/cylc/network/https/suite_broadcast_server.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json import re import sys import cPickle as pickle @@ -27,15 +28,18 @@ get_broadcast_bad_options_report) from cylc.cycling.loader import get_point, standardise_point_string from cylc.wallclock import get_current_time_string -from cylc.network import PYRO_BCAST_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer +from cylc.network import COMMS_BCAST_OBJ_NAME +from cylc.network.https.base_server import BaseCommsServer +from cylc.network.https.util import unicode_encode from cylc.network import check_access_priv from cylc.suite_logging import LOG from cylc.task_id import TaskID from cylc.rundb import CylcSuiteDAO +import cherrypy -class BroadcastServer(PyroServer): + +class BroadcastServer(BaseCommsServer): """Server-side suite broadcast interface. Examples: @@ -110,7 +114,11 @@ def _addict(self, target, source): else: target[key] = source[key] - def put(self, point_strings, namespaces, settings): + @cherrypy.expose + @cherrypy.tools.json_in() + @cherrypy.tools.json_out() + def put(self, point_strings=None, namespaces=None, settings=None, + not_from_client=False): """Add new broadcast settings (server side interface). Return a tuple (modified_settings, bad_options) where: @@ -120,6 +128,17 @@ def put(self, point_strings, namespaces, settings): """ check_access_priv(self, 'full-control') self.report('broadcast_put') + if not not_from_client: + point_strings = ( + cherrypy.request.json.get("point_strings", point_strings)) + namespaces = ( + cherrypy.request.json.get("namespaces", namespaces)) + settings = ( + cherrypy.request.json.get("settings", settings)) + point_strings = unicode_encode(point_strings) + namespaces = unicode_encode(namespaces) + settings = unicode_encode(settings) + modified_settings = [] bad_point_strings = [] bad_namespaces = [] @@ -160,14 +179,21 @@ def put(self, point_strings, namespaces, settings): bad_options["namespaces"] = bad_namespaces return modified_settings, bad_options + @cherrypy.expose + @cherrypy.tools.json_out() def get(self, task_id=None): """Retrieve all broadcast variables that target a given task ID.""" check_access_priv(self, 'full-read') self.report('broadcast_get') + if task_id == "None": + task_id = None if not task_id: # all broadcast settings requested return self.settings - name, point_string = TaskID.split(task_id) + try: + name, point_string = TaskID.split(task_id) + except ValueError: + raise Exception("Can't split task_id %s" % task_id) ret = {} # The order is: @@ -181,7 +207,9 @@ def get(self, task_id=None): self._addict(ret, self.settings[cycle][namespace]) return ret - def expire(self, cutoff): + @cherrypy.expose + @cherrypy.tools.json_out() + def expire(self, cutoff=None): """Clear all settings targeting cycle points earlier than cutoff.""" point_strings = [] cutoff_point = None @@ -197,6 +225,9 @@ def expire(self, cutoff): return (None, {"expire": [cutoff]}) return self.clear(point_strings=point_strings) + @cherrypy.expose + @cherrypy.tools.json_in() + @cherrypy.tools.json_out() def clear(self, point_strings=None, namespaces=None, cancel_settings=None): """Clear settings globally, or for listed namespaces and/or points. @@ -212,6 +243,17 @@ def clear(self, point_strings=None, namespaces=None, cancel_settings=None): * cancel: a list of tuples. Each tuple contains the keys of a bad setting. """ + + if hasattr(cherrypy.request, "json"): + point_strings = ( + cherrypy.request.json.get("point_strings", point_strings)) + namespaces = ( + cherrypy.request.json.get("namespaces", namespaces)) + cancel_settings = ( + cherrypy.request.json.get("cancel_settings", cancel_settings)) + point_strings = unicode_encode(point_strings) + namespaces = unicode_encode(namespaces) + cancel_settings = unicode_encode(cancel_settings) # If cancel_settings defined, only clear specific settings cancel_keys_list = self._settings_to_keys_list(cancel_settings) @@ -368,12 +410,3 @@ def _append_db_queue(self, modified_settings, is_cancel=False): "namespace": broadcast_change["namespace"], "key": broadcast_change["key"], "value": broadcast_change["value"]}) - - -class BroadcastClient(PyroClient): - """Client-side suite broadcast interface.""" - - target_server_object = PYRO_BCAST_OBJ_NAME - - def broadcast(self, cmd, *args): - return self.call_server_func(cmd, *args) diff --git a/lib/cylc/network/https/suite_command_client.py b/lib/cylc/network/https/suite_command_client.py new file mode 100644 index 00000000000..bb4b80ff6cd --- /dev/null +++ b/lib/cylc/network/https/suite_command_client.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cylc.network import COMMS_CMD_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient + + +class SuiteCommandClient(BaseCommsClient): + """Client-side suite command interface.""" + + def put_command(self, command, **arg_dict): + success, msg = self.call_server_func(COMMS_CMD_OBJ_NAME, command, + **arg_dict) + return (success, msg) diff --git a/lib/cylc/network/https/suite_command_server.py b/lib/cylc/network/https/suite_command_server.py new file mode 100644 index 00000000000..6d1ad555154 --- /dev/null +++ b/lib/cylc/network/https/suite_command_server.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ast +import sys +import os +from Queue import Queue + +import cylc.flags +from cylc.network.https.base_server import BaseCommsServer +from cylc.network import check_access_priv + +import cherrypy + + +class SuiteCommandServer(BaseCommsServer): + """Server-side suite command interface.""" + + def __init__(self): + super(SuiteCommandServer, self).__init__() + self.queue = Queue() + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_stop_cleanly(self, kill_active_tasks=False): + if isinstance(kill_active_tasks, basestring): + kill_active_tasks = ast.literal_eval(kill_active_tasks) + return self._put("set_stop_cleanly", + None, {"kill_active_tasks": kill_active_tasks}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def stop_now(self): + return self._put("stop_now", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_stop_after_point(self, point_string): + return self._put("set_stop_after_point", (point_string,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_stop_after_clock_time(self, datetime_string): + return self._put("set_stop_after_clock_time", (datetime_string,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_stop_after_task(self, task_id): + return self._put("set_stop_after_task", (task_id,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def release_suite(self): + return self._put("release_suite", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def release_tasks(self, items): + if not isinstance(items, list): + items = [items] + return self._put("release_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def remove_cycle(self, point_string, spawn=False): + spawn = ast.literal_eval(spawn) + return self._put("remove_cycle", (point_string,), {"spawn": spawn}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def remove_tasks(self, items, spawn=False): + spawn = ast.literal_eval(spawn) + if not isinstance(items, list): + items = [items] + return self._put("remove_tasks", (items,), {"spawn": spawn}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def hold_suite(self): + return self._put("hold_suite", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def hold_after_point_string(self, point_string): + return self._put("hold_after_point_string", (point_string,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def hold_tasks(self, items): + if not isinstance(items, list): + items = [items] + return self._put("hold_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_runahead(self, interval=None): + interval = ast.literal_eval(interval) + return self._put("set_runahead", None, {"interval": interval}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def set_verbosity(self, level): + return self._put("set_verbosity", (level,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def reset_task_states(self, items, state=None): + if not isinstance(items, list): + items = [items] + return self._put("reset_task_states", (items,), {"state": state}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def trigger_tasks(self, items): + if not isinstance(items, list): + items = [items] + items = [str(item) for item in items] + return self._put("trigger_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def dry_run_tasks(self, items): + if not isinstance(items, list): + items = [items] + return self._put("dry_run_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def nudge(self): + return self._put("nudge", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def insert_tasks(self, items, stop_point_string=None): + if not isinstance(items, list): + items = [items] + if stop_point_string == "None": + stop_point_string = None + return self._put("insert_tasks", (items,), + {"stop_point_string": stop_point_string}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def reload_suite(self): + return self._put("reload_suite", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def poll_tasks(self, items=None): + if items is not None and not isinstance(items, list): + items = [items] + return self._put("poll_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def kill_tasks(self, items): + if not isinstance(items, list): + items = [items] + return self._put("kill_tasks", (items,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def spawn_tasks(self, items): + if not isinstance(items, list): + items = [items] + return self._put("kill_tasks", (items,)) + + def _put(self, command, command_args, command_kwargs=None): + if command_args is None: + command_args = tuple() + if command_kwargs is None: + command_kwargs = {} + if 'stop' in command: + check_access_priv(self, 'shutdown') + else: + check_access_priv(self, 'full-control') + self.report(command) + self.queue.put((command, command_args, command_kwargs)) + return (True, 'Command queued') + + def get_queue(self): + return self.queue diff --git a/lib/cylc/network/https/suite_identifier_client.py b/lib/cylc/network/https/suite_identifier_client.py new file mode 100644 index 00000000000..aa3a5e28aa6 --- /dev/null +++ b/lib/cylc/network/https/suite_identifier_client.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# A minimal server/client pair to allow client programs to identify +# what suite is running at a given cylc port - by suite name and owner. + +from cylc.network import COMMS_SUITEID_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient, BaseCommsClientAnon + + +class SuiteIdClient(BaseCommsClient): + """Client-side suite identity nterface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def identify(self): + return self.call_server_func(COMMS_SUITEID_OBJ_NAME, "identify") + + +class SuiteIdClientAnon(BaseCommsClientAnon): + """Client-side suite identity nterface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def identify(self): + return self.call_server_func(COMMS_SUITEID_OBJ_NAME, "identify") diff --git a/lib/cylc/network/suite_identifier.py b/lib/cylc/network/https/suite_identifier_server.py similarity index 75% rename from lib/cylc/network/suite_identifier.py rename to lib/cylc/network/https/suite_identifier_server.py index 00e41cf7199..ae618e46806 100644 --- a/lib/cylc/network/suite_identifier.py +++ b/lib/cylc/network/https/suite_identifier_server.py @@ -16,23 +16,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# A minimal Pyro-connected object to allow client programs to identify +# A minimal server/client pair to allow client programs to identify # what suite is running at a given cylc port - by suite name and owner. -# All *other* suite objects should be connected to Pyro via qualified -# names: owner.suite.object, to prevent accidental access to the wrong -# suite. This object, however, should be connected unqualified so that -# that same ID method can be called on any active cylc port. - import cylc.flags -from cylc.network.pyro_base import PyroServer -from cylc.network.suite_state import StateSummaryServer +from cylc.network.https.base_server import BaseCommsServer +from cylc.network.https.suite_state_server import StateSummaryServer from cylc.network import access_priv_ok from cylc.config import SuiteConfig +import cherrypy + -class SuiteIdServer(PyroServer): - """Server-side external trigger interface.""" +class SuiteIdServer(BaseCommsServer): + """Server-side identification interface.""" _INSTANCE = None @@ -48,6 +45,8 @@ def __init__(self, name, owner): self.name = name super(SuiteIdServer, self).__init__() + @cherrypy.expose + @cherrypy.tools.json_out() def identify(self): self.report("identify") result = {} @@ -65,8 +64,3 @@ def identify(self): result['tasks-by-state'] = ( StateSummaryServer.get_inst().get_tasks_by_state()) return result - - def id(self): - # Back-compat for older clients <=6.4.1. - # (Allows old scan to see new suites.) - return (self.name, self.owner) diff --git a/lib/cylc/network/https/suite_info_client.py b/lib/cylc/network/https/suite_info_client.py new file mode 100644 index 00000000000..caef3e5b807 --- /dev/null +++ b/lib/cylc/network/https/suite_info_client.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cylc.network import COMMS_INFO_OBJ_NAME +from cylc.network.https.base_client import ( + BaseCommsClient, BaseCommsClientAnon) + + +class SuiteInfoClient(BaseCommsClient): + """Client-side suite information interface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def get_info(self, command, **arg_dict): + return self.call_server_func(COMMS_INFO_OBJ_NAME, command, **arg_dict) + + +class SuiteInfoClientAnon(BaseCommsClientAnon): + """Anonymous client-side suite information interface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def get_info(self, command, **arg_dict): + return self.call_server_func(COMMS_INFO_OBJ_NAME, command, **arg_dict) diff --git a/lib/cylc/network/https/suite_info_server.py b/lib/cylc/network/https/suite_info_server.py new file mode 100644 index 00000000000..84a5313a826 --- /dev/null +++ b/lib/cylc/network/https/suite_info_server.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ast +import sys + +import cylc.flags +from cylc.network.https.base_server import BaseCommsServer +from cylc.network import check_access_priv + +import cherrypy + + +class SuiteInfoServer(BaseCommsServer): + """Server-side suite information interface.""" + + def __init__(self, info_commands): + super(SuiteInfoServer, self).__init__() + self.commands = info_commands + + @cherrypy.expose + @cherrypy.tools.json_out() + def ping_suite(self): + return self._put("ping_suite", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_cylc_version(self): + return self._put("get_cylc_version", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def ping_task(self, task_id, exists_only=False): + if isinstance(exists_only, basestring): + exists_only = ast.literal_eval(exists_only) + return self._put("ping_task", (task_id,), + {"exists_only": exists_only}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_task_jobfile_path(self, task_id): + return self._put("get_task_jobfile_path", (task_id,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_suite_info(self): + return self._put("get_suite_info", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_task_info(self, name): + return self._put("get_task_info", (name,)) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_all_families(self, exclude_root=False): + if isinstance(exclude_root, basestring): + exclude_root = ast.literal_eval(exclude_root) + return self._put("get_all_families", None, + {"exclude_root": exclude_root}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_first_parent_descendants(self): + return self._put("get_first_parent_descendants", None) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_first_parent_ancestors(self, pruned=None): + if isinstance(pruned, basestring): + pruned = ast.literal_eval(pruned) + return self._put("get_first_parent_ancestors", None, + {"pruned": pruned}) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_graph_raw(self, start_point_string, stop_point_string, + group_nodes=None, ungroup_nodes=None, + ungroup_recursive=False, group_all=False, + ungroup_all=False): + if isinstance(group_nodes, basestring): + group_nodes = ast.literal_eval(group_nodes) + if isinstance(ungroup_nodes, basestring): + ungroup_nodes = ast.literal_eval(ungroup_nodes) + if isinstance(ungroup_recursive, basestring): + ungroup_recursive = ast.literal_eval(ungroup_recursive) + if isinstance(group_all, basestring): + group_all = ast.literal_eval(group_all) + if isinstance(ungroup_all, basestring): + ungroup_all = ast.literal_eval(ungroup_all) + return self._put( + "get_graph_raw", (start_point_string, stop_point_string), + {"group_nodes": group_nodes, + "ungroup_nodes": ungroup_nodes, + "ungroup_recursive": ungroup_recursive, + "group_all": group_all, + "ungroup_all": ungroup_all} + ) + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_task_requisites(self, name, point_string): + return self._put("get_task_requisites", (name, point_string)) + + def _put(self, command, command_args, command_kwargs=None): + if command_args is None: + command_args = tuple() + if command_kwargs is None: + command_kwargs = {} + if ('ping' in command or 'version' in command): + # Free info. + pass + elif 'suite' in command and 'info' in command: + # Suite title and description only. + check_access_priv(self, 'description') + else: + check_access_priv(self, 'full-read') + self.report(command) + return self.commands[command](*command_args, **command_kwargs) diff --git a/lib/cylc/network/https/suite_log_client.py b/lib/cylc/network/https/suite_log_client.py new file mode 100644 index 00000000000..3ad3d9c89b9 --- /dev/null +++ b/lib/cylc/network/https/suite_log_client.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cylc.network import COMMS_LOG_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient + + +class SuiteLogClient(BaseCommsClient): + """Client-side suite log interface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def get_err_content(self, prev_size, max_lines): + return self.call_server_func(COMMS_LOG_OBJ_NAME, "get_err_content", + prev_size=prev_size, max_lines=max_lines) diff --git a/lib/cylc/network/suite_log.py b/lib/cylc/network/https/suite_log_server.py similarity index 79% rename from lib/cylc/network/suite_log.py rename to lib/cylc/network/https/suite_log_server.py index 2332daf207b..03ba04af6fb 100644 --- a/lib/cylc/network/suite_log.py +++ b/lib/cylc/network/https/suite_log_server.py @@ -17,13 +17,14 @@ # along with this program. If not, see . import os -from cylc.network import PYRO_LOG_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer +from cylc.network.https.base_server import BaseCommsServer from cylc.network import check_access_priv from cylc.suite_logging import SuiteLog +import cherrypy -class SuiteLogServer(PyroServer): + +class SuiteLogServer(BaseCommsServer): """Server-side suite log interface.""" def __init__(self, log): @@ -40,16 +41,19 @@ def _get_err_size(self): try: size = os.path.getsize(self.err_file) - except (IOError, OSError) as e: - self.log.warning("Could not read suite err log file: %s" % e) + except (IOError, OSError) as exc: + self.log.warn("Could not read suite err log file: %s" % exc) return 0 return size - def get_err_content(self, prev_size=0, max_lines=100): + @cherrypy.expose + @cherrypy.tools.json_out() + def get_err_content(self, prev_size, max_lines): """Return the content and new size of the error file.""" - check_access_priv(self, 'full-read') self.report("get_err_content") + prev_size = int(prev_size) + max_lines = int(max_lines) if not self._get_err_has_changed(prev_size): return [], prev_size try: @@ -63,12 +67,3 @@ def get_err_content(self, prev_size=0, max_lines=100): return "", prev_size new_content_lines = new_content.splitlines()[-max_lines:] return "\n".join(new_content_lines), size - - -class SuiteLogClient(PyroClient): - """Client-side suite log interface.""" - - target_server_object = PYRO_LOG_OBJ_NAME - - def get_err_content(self, *args): - return self.call_server_func("get_err_content", *args) diff --git a/lib/cylc/network/https/suite_state_client.py b/lib/cylc/network/https/suite_state_client.py new file mode 100644 index 00000000000..28fcab04911 --- /dev/null +++ b/lib/cylc/network/https/suite_state_client.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re + +from cylc.task_id import TaskID +from cylc.network import COMMS_STATE_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient +from cylc.network.https.util import unicode_encode +from cylc.task_state import ( + TASK_STATUS_RUNAHEAD, TASK_STATUS_HELD, TASK_STATUS_WAITING, + TASK_STATUS_EXPIRED, TASK_STATUS_QUEUED, TASK_STATUS_READY, + TASK_STATUS_SUBMITTED, TASK_STATUS_SUBMIT_FAILED, + TASK_STATUS_SUBMIT_RETRYING, TASK_STATUS_RUNNING, TASK_STATUS_SUCCEEDED, + TASK_STATUS_FAILED, TASK_STATUS_RETRYING) + + +# Suite status strings. +SUITE_STATUS_HELD = "held" +SUITE_STATUS_RUNNING = "running" +SUITE_STATUS_STOPPING = "stopping" +SUITE_STATUS_RUNNING_TO_STOP = "running to stop at %s" +SUITE_STATUS_RUNNING_TO_HOLD = "running to hold at %s" +# Regex to extract the stop or hold point. +SUITE_STATUS_SPLIT_REC = re.compile('^([a-z ]+ at )(.*)$') + +# Pseudo status strings for use by suite monitors. +# Use before attempting to determine status: +SUITE_STATUS_NOT_CONNECTED = "not connected" +# Use prior to first status update: +SUITE_STATUS_CONNECTED = "connected" +SUITE_STATUS_INITIALISING = "initialising" +# Use when the suite is not running: +SUITE_STATUS_STOPPED = "stopped" +SUITE_STATUS_STOPPED_WITH = "stopped with '%s'" + + +def get_suite_status_string(paused, stopping, will_pause_at, will_stop_at): + """Construct a suite status summary string for client programs. + + This is in a function for re-use in monitor and GUI back-compat code + (clients at cylc version <= 6.9.1 construct their own status string). + + """ + if paused: + return SUITE_STATUS_HELD + elif stopping: + return SUITE_STATUS_STOPPING + elif will_pause_at: + return SUITE_STATUS_RUNNING_TO_HOLD % will_pause_at + elif will_stop_at: + return SUITE_STATUS_RUNNING_TO_STOP % will_stop_at + else: + return SUITE_STATUS_RUNNING + + +class SuiteStillInitialisingError(Exception): + """Exception raised if a summary is requested before the first update. + + This can happen if client connects during start-up for large suites. + + """ + def __str__(self): + return "Suite initializing..." + + +class StateSummaryClient(BaseCommsClient): + """Client-side suite state summary interface.""" + + METHOD = BaseCommsClient.METHOD_GET + + def get_suite_state_summary(self): + return unicode_encode( + self.call_server_func(COMMS_STATE_OBJ_NAME, "get_state_summary")) + + def get_suite_state_summary_update_time(self): + return self.call_server_func(COMMS_STATE_OBJ_NAME, + "get_summary_update_time") + + def get_tasks_by_state(self): + return self.call_server_func(COMMS_STATE_OBJ_NAME, + "get_tasks_by_state") + + +def extract_group_state(child_states, is_stopped=False): + """Summarise child states as a group.""" + + ordered_states = [TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED, + TASK_STATUS_EXPIRED, TASK_STATUS_SUBMIT_RETRYING, + TASK_STATUS_RETRYING, TASK_STATUS_RUNNING, + TASK_STATUS_SUBMITTED, TASK_STATUS_READY, + TASK_STATUS_QUEUED, TASK_STATUS_WAITING, + TASK_STATUS_HELD, TASK_STATUS_SUCCEEDED, + TASK_STATUS_RUNAHEAD] + if is_stopped: + ordered_states = [TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED, + TASK_STATUS_RUNNING, TASK_STATUS_SUBMITTED, + TASK_STATUS_EXPIRED, TASK_STATUS_READY, + TASK_STATUS_SUBMIT_RETRYING, TASK_STATUS_RETRYING, + TASK_STATUS_SUCCEEDED, TASK_STATUS_QUEUED, + TASK_STATUS_WAITING, TASK_STATUS_HELD, + TASK_STATUS_RUNAHEAD] + for state in ordered_states: + if state in child_states: + return state + return None + + +def get_id_summary(id_, task_state_summary, fam_state_summary, id_family_map): + """Return some state information about a task or family id.""" + prefix_text = "" + meta_text = "" + sub_text = "" + sub_states = {} + stack = [(id_, 0)] + done_ids = [] + for summary in [task_state_summary, fam_state_summary]: + if id_ in summary: + title = summary[id_].get('title') + if title: + meta_text += "\n" + title.strip() + description = summary[id_].get('description') + if description: + meta_text += "\n" + description.strip() + while stack: + this_id, depth = stack.pop(0) + if this_id in done_ids: # family dive down will give duplicates + continue + done_ids.append(this_id) + prefix = "\n" + " " * 4 * depth + this_id + if this_id in task_state_summary: + submit_num = task_state_summary[this_id].get('submit_num') + if submit_num: + prefix += "(%02d)" % submit_num + state = task_state_summary[this_id]['state'] + sub_text += prefix + " " + state + sub_states.setdefault(state, 0) + sub_states[state] += 1 + elif this_id in fam_state_summary: + name, point_string = TaskID.split(this_id) + sub_text += prefix + " " + fam_state_summary[this_id]['state'] + for child in reversed(sorted(id_family_map[name])): + child_id = TaskID.get(child, point_string) + stack.insert(0, (child_id, depth + 1)) + if not prefix_text: + prefix_text = sub_text.strip() + sub_text = "" + if len(sub_text.splitlines()) > 10: + state_items = sub_states.items() + state_items.sort() + state_items.sort(lambda x, y: cmp(y[1], x[1])) + sub_text = "" + for state, number in state_items: + sub_text += "\n {0} tasks {1}".format(number, state) + if sub_text and meta_text: + sub_text = "\n" + sub_text + text = prefix_text + meta_text + sub_text + if not text: + return id_ + return text diff --git a/lib/cylc/network/suite_state.py b/lib/cylc/network/https/suite_state_server.py similarity index 58% rename from lib/cylc/network/suite_state.py rename to lib/cylc/network/https/suite_state_server.py index 463323a5eb5..5780eae153b 100644 --- a/lib/cylc/network/suite_state.py +++ b/lib/cylc/network/https/suite_state_server.py @@ -16,75 +16,24 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re import time -import datetime import cylc.flags from cylc.task_id import TaskID from cylc.wallclock import TIME_ZONE_LOCAL_INFO, TIME_ZONE_UTC_INFO from cylc.config import SuiteConfig -from cylc.network import PYRO_STATE_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer +from cylc.network.https.base_server import BaseCommsServer +from cylc.network.https.suite_state_client import ( + get_suite_status_string, SuiteStillInitialisingError, + extract_group_state +) from cylc.network import check_access_priv -from cylc.task_state import ( - TASK_STATUS_RUNAHEAD, TASK_STATUS_HELD, TASK_STATUS_WAITING, - TASK_STATUS_EXPIRED, TASK_STATUS_QUEUED, TASK_STATUS_READY, - TASK_STATUS_SUBMITTED, TASK_STATUS_SUBMIT_FAILED, - TASK_STATUS_SUBMIT_RETRYING, TASK_STATUS_RUNNING, TASK_STATUS_SUCCEEDED, - TASK_STATUS_FAILED, TASK_STATUS_RETRYING) - - -# Suite status strings. -SUITE_STATUS_HELD = "held" -SUITE_STATUS_RUNNING = "running" -SUITE_STATUS_STOPPING = "stopping" -SUITE_STATUS_RUNNING_TO_STOP = "running to stop at %s" -SUITE_STATUS_RUNNING_TO_HOLD = "running to hold at %s" -# Regex to extract the stop or hold point. -SUITE_STATUS_SPLIT_REC = re.compile('^([a-z ]+ at )(.*)$') - -# Pseudo status strings for use by suite monitors. -# Use before attempting to determine status: -SUITE_STATUS_NOT_CONNECTED = "not connected" -# Use prior to first status update: -SUITE_STATUS_CONNECTED = "connected" -SUITE_STATUS_INITIALISING = "initialising" -# Use when the suite is not running: -SUITE_STATUS_STOPPED = "stopped" -SUITE_STATUS_STOPPED_WITH = "stopped with '%s'" - - -def get_suite_status_string(paused, stopping, will_pause_at, will_stop_at): - """Construct a suite status summary string for client programs. - - This is in a function for re-use in monitor and GUI back-compat code - (clients at cylc version <= 6.9.1 construct their own status string). - - """ - if paused: - return SUITE_STATUS_HELD - elif stopping: - return SUITE_STATUS_STOPPING - elif will_pause_at: - return SUITE_STATUS_RUNNING_TO_HOLD % will_pause_at - elif will_stop_at: - return SUITE_STATUS_RUNNING_TO_STOP % will_stop_at - else: - return SUITE_STATUS_RUNNING - - -class SuiteStillInitialisingError(Exception): - """Exception raised if a summary is requested before the first update. - - This can happen if client connects during start-up for large suites. - - """ - def __str__(self): - return "Suite initializing..." - - -class StateSummaryServer(PyroServer): +from cylc.task_state import TASK_STATUS_RUNAHEAD + +import cherrypy + + +class StateSummaryServer(BaseCommsServer): """Server-side suite state summary interface.""" _INSTANCE = None @@ -198,14 +147,6 @@ def update(self, tasks, tasks_rh, min_point, max_point, max_point_rh, global_summary['status_string'] = get_suite_status_string( paused, stopping, will_pause_at, will_stop_at) - # TODO - delete this block post back-compat concerns (<= 6.9.1): - # Report separate status string components for older clients that - # construct their own suite status strings. - global_summary['paused'] = paused - global_summary['stopping'] = stopping - global_summary['will_pause_at'] = will_pause_at - global_summary['will_stop_at'] = will_stop_at - self._summary_update_time = time.time() # Replace the originals (atomic update, for access from other threads). @@ -249,6 +190,8 @@ def get_state_totals(self): # (Access to this is controlled via the suite_identity server.) return (self.state_count_totals, self.state_count_cycles) + @cherrypy.expose + @cherrypy.tools.json_out() def get_state_summary(self): """Return the global, task, and family summary data structures.""" check_access_priv(self, 'full-read') @@ -257,6 +200,18 @@ def get_state_summary(self): raise SuiteStillInitialisingError() return (self.global_summary, self.task_summary, self.family_summary) + @cherrypy.expose + @cherrypy.tools.json_out() + def get_summary_update_time(self): + """Return the last time the summaries were changed (Unix time).""" + check_access_priv(self, 'state-totals') + self.report('get_state_summary_update_time') + if not self.first_update_completed: + raise SuiteStillInitialisingError() + return self._summary_update_time + + @cherrypy.expose + @cherrypy.tools.json_out() def get_tasks_by_state(self): """Returns a dictionary containing lists of tasks by state in the form: {state: [(most_recent_time_string, task_name, point_string), ...]}.""" @@ -286,101 +241,3 @@ def get_tasks_by_state(self): (None, len(ret[state]) - 5, None,)] return ret - - def get_summary_update_time(self): - """Return the last time the summaries were changed (Unix time).""" - check_access_priv(self, 'state-totals') - self.report('get_state_summary_update_time') - if not self.first_update_completed: - raise SuiteStillInitialisingError() - return self._summary_update_time - - -class StateSummaryClient(PyroClient): - """Client-side suite state summary interface.""" - - target_server_object = PYRO_STATE_OBJ_NAME - - def get_suite_state_summary(self): - return self.call_server_func("get_state_summary") - - def get_suite_state_summary_update_time(self): - return self.call_server_func("get_summary_update_time") - - -def extract_group_state(child_states, is_stopped=False): - """Summarise child states as a group.""" - - ordered_states = [TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED, - TASK_STATUS_EXPIRED, TASK_STATUS_SUBMIT_RETRYING, - TASK_STATUS_RETRYING, TASK_STATUS_RUNNING, - TASK_STATUS_SUBMITTED, TASK_STATUS_READY, - TASK_STATUS_QUEUED, TASK_STATUS_WAITING, - TASK_STATUS_HELD, TASK_STATUS_SUCCEEDED, - TASK_STATUS_RUNAHEAD] - if is_stopped: - ordered_states = [TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED, - TASK_STATUS_RUNNING, TASK_STATUS_SUBMITTED, - TASK_STATUS_EXPIRED, TASK_STATUS_READY, - TASK_STATUS_SUBMIT_RETRYING, TASK_STATUS_RETRYING, - TASK_STATUS_SUCCEEDED, TASK_STATUS_QUEUED, - TASK_STATUS_WAITING, TASK_STATUS_HELD, - TASK_STATUS_RUNAHEAD] - for state in ordered_states: - if state in child_states: - return state - return None - - -def get_id_summary(id_, task_state_summary, fam_state_summary, id_family_map): - """Return some state information about a task or family id.""" - prefix_text = "" - meta_text = "" - sub_text = "" - sub_states = {} - stack = [(id_, 0)] - done_ids = [] - for summary in [task_state_summary, fam_state_summary]: - if id_ in summary: - title = summary[id_].get('title') - if title: - meta_text += "\n" + title.strip() - description = summary[id_].get('description') - if description: - meta_text += "\n" + description.strip() - while stack: - this_id, depth = stack.pop(0) - if this_id in done_ids: # family dive down will give duplicates - continue - done_ids.append(this_id) - prefix = "\n" + " " * 4 * depth + this_id - if this_id in task_state_summary: - submit_num = task_state_summary[this_id].get('submit_num') - if submit_num: - prefix += "(%02d)" % submit_num - state = task_state_summary[this_id]['state'] - sub_text += prefix + " " + state - sub_states.setdefault(state, 0) - sub_states[state] += 1 - elif this_id in fam_state_summary: - name, point_string = TaskID.split(this_id) - sub_text += prefix + " " + fam_state_summary[this_id]['state'] - for child in reversed(sorted(id_family_map[name])): - child_id = TaskID.get(child, point_string) - stack.insert(0, (child_id, depth + 1)) - if not prefix_text: - prefix_text = sub_text.strip() - sub_text = "" - if len(sub_text.splitlines()) > 10: - state_items = sub_states.items() - state_items.sort() - state_items.sort(lambda x, y: cmp(y[1], x[1])) - sub_text = "" - for state, number in state_items: - sub_text += "\n {0} tasks {1}".format(number, state) - if sub_text and meta_text: - sub_text = "\n" + sub_text - text = prefix_text + meta_text + sub_text - if not text: - return id_ - return text diff --git a/lib/cylc/network/https/task_msg_client.py b/lib/cylc/network/https/task_msg_client.py new file mode 100644 index 00000000000..dbc689e13fc --- /dev/null +++ b/lib/cylc/network/https/task_msg_client.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cylc.owner import USER +from cylc.suite_host import get_hostname +from cylc.network import COMMS_TASK_MESSAGE_OBJ_NAME +from cylc.network.https.base_client import BaseCommsClient + + +class TaskMessageClient(BaseCommsClient): + """Client-side task messaging interface""" + + def __init__(self, suite, task_id, owner=USER, + host=get_hostname(), timeout=None, port=None): + self.task_id = task_id + super(TaskMessageClient, self).__init__(suite, owner, host, + port=port, timeout=timeout) + + def put(self, priority, message): + return self.call_server_func(COMMS_TASK_MESSAGE_OBJ_NAME, "put", + task_id=self.task_id, + priority=priority, + message=message) diff --git a/lib/cylc/network/task_msgqueue.py b/lib/cylc/network/https/task_msg_server.py similarity index 58% rename from lib/cylc/network/task_msgqueue.py rename to lib/cylc/network/https/task_msg_server.py index b485479bb69..5a144b44635 100644 --- a/lib/cylc/network/task_msgqueue.py +++ b/lib/cylc/network/https/task_msg_server.py @@ -17,39 +17,26 @@ # along with this program. If not, see . from Queue import Queue -from cylc.owner import USER -from cylc.suite_host import get_hostname -from cylc.network.pyro_base import PyroClient, PyroServer from cylc.network import check_access_priv +from cylc.network.https.base_server import BaseCommsServer +import cherrypy -class TaskMessageServer(PyroServer): + +class TaskMessageServer(BaseCommsServer): """Server-side task messaging interface""" - def __init__(self): - super(TaskMessageServer, self).__init__() + def __init__(self, suite): self.queue = Queue() + super(TaskMessageServer, self).__init__() + @cherrypy.expose + @cherrypy.tools.json_out() def put(self, task_id, priority, message): check_access_priv(self, 'full-control') self.report('task_message') - self.queue.put((task_id, priority, message)) - return (True, 'Message queued') + self.queue.put((task_id, priority, str(message))) + return 'Message queued' def get_queue(self): return self.queue - - -class TaskMessageClient(PyroClient): - """Client-side task messaging interface""" - - def __init__(self, suite, task_id, owner=USER, - host=get_hostname(), pyro_timeout=None, port=None): - self.target_server_object = "task_pool" - self.task_id = task_id - super(TaskMessageClient, self).__init__( - suite, owner, host, pyro_timeout, port) - - def put(self, *args): - args = [self.task_id] + list(args) - self.call_server_func('put', *args) diff --git a/lib/cylc/network/https/util.py b/lib/cylc/network/https/util.py new file mode 100644 index 00000000000..6292952cb3e --- /dev/null +++ b/lib/cylc/network/https/util.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Utility classes for HTTPS servers and clients.""" + +import HTMLParser + + +class ExceptionPreReader(HTMLParser.HTMLParser): + + def __init__(self): + self.is_in_traceback_pre = False + self.exception_text = None + # Can't use super because this is an old-style class... :( + HTMLParser.HTMLParser.__init__(self) + + def handle_starttag(self, tag, attributes): + if tag != "pre": + return + for name, value in attributes: + if name == "id" and value == "traceback": + self.is_in_traceback_pre = True + + def handle_endtag(self, tag): + self.is_in_traceback_pre = False + + def handle_data(self, data): + if hasattr(self, "is_in_traceback_pre") and self.is_in_traceback_pre: + if self.exception_text is None: + self.exception_text = "" + self.exception_text += data + + +def get_exception_from_html(html_text): + """Return any content inside a
 block with id 'traceback', or None.
+
+    Return e.g. 'abcdef' for text like '
+    abcdef
+    
'. + + """ + parser = ExceptionPreReader() + try: + parser.feed(parser.unescape(html_text)) + parser.close() + except HTMLParser.HTMLParseError: + return None + return parser.exception_text + + +def unicode_encode(data): + if isinstance(data, unicode): + return data.encode('utf-8') + if isinstance(data, dict): + new_dict = {} + for key, value in data.items(): + new_dict.update( + {unicode_encode(key): unicode_encode(value)} + ) + return new_dict + if isinstance(data, list): + return [unicode_encode(item) for item in data] + return data diff --git a/lib/cylc/network/method.py b/lib/cylc/network/method.py new file mode 100644 index 00000000000..7356bb73984 --- /dev/null +++ b/lib/cylc/network/method.py @@ -0,0 +1,18 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Package for network interfaces to cylc suite server objects.""" + +from cylc.cfgspec.globalcfg import GLOBAL_CFG + +METHOD = GLOBAL_CFG.get(['communication', 'method']) diff --git a/lib/cylc/network/port_scan.py b/lib/cylc/network/port_scan.py index 100e9842caf..3538dc200e5 100644 --- a/lib/cylc/network/port_scan.py +++ b/lib/cylc/network/port_scan.py @@ -1,4 +1,4 @@ -#!/usr/bin/pyro +#!/usr/bin/env python # THIS FILE IS PART OF THE CYLC SUITE ENGINE. # Copyright (C) 2008-2016 NIWA @@ -15,168 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Pyro port scan utilities.""" +"""Wrap communications daemon for a suite.""" -from multiprocessing import cpu_count, Pool -import sys -from time import sleep -import traceback +from cylc.network.method import METHOD -import Pyro.errors -import Pyro.core -from cylc.cfgspec.globalcfg import GLOBAL_CFG -import cylc.flags -from cylc.network import PYRO_SUITEID_OBJ_NAME, NO_PASSPHRASE -from cylc.network.connection_validator import ConnValidator, SCAN_HASH -from cylc.network.suite_state import SuiteStillInitialisingError -from cylc.owner import USER -from cylc.registration import RegistrationDB -from cylc.suite_host import get_hostname, is_remote_host - - -def get_proxy(host, port, pyro_timeout): - """Return Pyro URL of proxy.""" - proxy = Pyro.core.getProxyForURI( - 'PYROLOC://%s:%s/%s' % (host, port, PYRO_SUITEID_OBJ_NAME)) - proxy._setTimeout(pyro_timeout) - return proxy - - -def scan(host=None, db=None, pyro_timeout=None): - """Scan ports, return a list of suites found: [(port, suite.identify())]. - - Note that we could easily scan for a given suite+owner and return its - port instead of reading port files, but this may not always be fast enough. - """ - if host is None: - host = get_hostname() - base_port = GLOBAL_CFG.get(['pyro', 'base port']) - last_port = base_port + GLOBAL_CFG.get(['pyro', 'maximum number of ports']) - if pyro_timeout: - pyro_timeout = float(pyro_timeout) - else: - pyro_timeout = None - - reg_db = RegistrationDB(db) - results = [] - for port in range(base_port, last_port): - try: - proxy = get_proxy(host, port, pyro_timeout) - conn_val = ConnValidator() - conn_val.set_default_hash(SCAN_HASH) - proxy._setNewConnectionValidator(conn_val) - proxy._setIdentification((USER, NO_PASSPHRASE)) - result = (port, proxy.identify()) - except Pyro.errors.ConnectionDeniedError as exc: - if cylc.flags.debug: - print '%s:%s (connection denied)' % (host, port) - # Back-compat <= 6.4.1 - msg = ' Old daemon at %s:%s?' % (host, port) - for pphrase in reg_db.load_all_passphrases(): - try: - proxy = get_proxy(host, port, pyro_timeout) - proxy._setIdentification(pphrase) - info = proxy.id() - result = (port, {'name': info[0], 'owner': info[1]}) - except Pyro.errors.ConnectionDeniedError: - connected = False - else: - connected = True - break - if not connected: - if cylc.flags.verbose: - print >> sys.stderr, msg, "- connection denied (%s)" % exc - continue - else: - if cylc.flags.verbose: - print >> sys.stderr, msg, "- connected with passphrase" - except Pyro.errors.TimeoutError as exc: - # E.g. Ctrl-Z suspended suite - holds up port scanning! - if cylc.flags.debug: - print '%s:%s (connection timed out)' % (host, port) - print >> sys.stderr, ( - 'suite? owner?@%s:%s - connection timed out (%s)' % ( - host, port, exc)) - continue - except (Pyro.errors.ProtocolError, Pyro.errors.NamingError) as exc: - # No suite at this port. - if cylc.flags.debug: - print str(exc) - print '%s:%s (no suite)' % (host, port) - continue - except SuiteStillInitialisingError: - continue - except Exception as exc: - if cylc.flags.debug: - traceback.print_exc() - raise - else: - owner = result[1].get('owner') - name = result[1].get('name') - states = result[1].get('states', None) - if cylc.flags.debug: - print ' suite:', name, owner - if states is None: - # This suite keeps its state info private. - # Try again with the passphrase if I have it. - pphrase = reg_db.load_passphrase(name, owner, host) - if pphrase: - try: - proxy = get_proxy(host, port, pyro_timeout) - conn_val = ConnValidator() - conn_val.set_default_hash(SCAN_HASH) - proxy._setNewConnectionValidator(conn_val) - proxy._setIdentification((USER, pphrase)) - result = (port, proxy.identify()) - except Exception: - # Nope (private suite, wrong passphrase). - if cylc.flags.debug: - print ' (wrong passphrase)' - else: - reg_db.cache_passphrase( - name, owner, host, pphrase) - if cylc.flags.debug: - print ' (got states with passphrase)' - results.append(result) - return results - - -def scan_all(hosts=None, reg_db_path=None, pyro_timeout=None): - """Scan all hosts.""" - if not hosts: - hosts = GLOBAL_CFG.get(["suite host scanning", "hosts"]) - # Ensure that it does "localhost" only once - hosts = set(hosts) - for host in list(hosts): - if not is_remote_host(host): - hosts.remove(host) - hosts.add("localhost") - proc_pool_size = GLOBAL_CFG.get(["process pool size"]) - if proc_pool_size is None: - proc_pool_size = cpu_count() - if proc_pool_size > len(hosts): - proc_pool_size = len(hosts) - proc_pool = Pool(proc_pool_size) - async_results = {} - for host in hosts: - async_results[host] = proc_pool.apply_async( - scan, [host, reg_db_path, pyro_timeout]) - proc_pool.close() - scan_results = [] - scan_results_hosts = [] - while async_results: - sleep(0.05) - for host, async_result in async_results.items(): - if async_result.ready(): - async_results.pop(host) - try: - res = async_result.get() - except Exception: - if cylc.flags.debug: - traceback.print_exc() - else: - scan_results.extend(res) - scan_results_hosts.extend([host] * len(res)) - proc_pool.join() - return zip(scan_results_hosts, scan_results) +if METHOD == "https": + from cylc.network.https.port_scan import scan_all diff --git a/lib/cylc/network/pyro_base.py b/lib/cylc/network/pyro_base.py deleted file mode 100644 index a11fb816eea..00000000000 --- a/lib/cylc/network/pyro_base.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -"""Base classes for Pyro servers and clients.""" - -import os -import shlex -from subprocess import Popen, PIPE -import sys -import traceback -from uuid import uuid4 - -import Pyro.core -import Pyro.errors - -from cylc.cfgspec.globalcfg import GLOBAL_CFG -from cylc.exceptions import PortFileError -import cylc.flags -from cylc.network.client_reporter import PyroClientReporter -from cylc.network.connection_validator import ConnValidator, OK_HASHES -from cylc.owner import is_remote_user, USER -from cylc.registration import RegistrationDB -from cylc.suite_host import get_hostname, is_remote_host -from cylc.suite_env import CylcSuiteEnv, CylcSuiteEnvLoadError - - -class PyroServer(Pyro.core.ObjBase): - """Base class for server-side suite object interfaces.""" - - def __init__(self): - Pyro.core.ObjBase.__init__(self) - self.client_reporter = PyroClientReporter.get_inst() - - def signout(self): - """Wrap client_reporter.signout.""" - self.client_reporter.signout(self) - - def report(self, command): - """Wrap client_reporter.report.""" - self.client_reporter.report(command, self) - - -class PyroClient(object): - """Base class for client-side suite object interfaces.""" - - target_server_object = None - - def __init__(self, suite, owner=USER, host=None, pyro_timeout=None, - port=None, db=None, my_uuid=None, print_uuid=False): - self.suite = suite - self.host = host - self.owner = owner - if pyro_timeout is not None: - pyro_timeout = float(pyro_timeout) - self.pyro_timeout = pyro_timeout - self.port = port - self.pyro_proxy = None - self.my_uuid = my_uuid or uuid4() - self.uri = None - if print_uuid: - print >> sys.stderr, '%s' % self.my_uuid - self.reg_db = RegistrationDB(db) - self.pphrase = None - - def call_server_func(self, fname, *fargs): - """Call server_object.fname(*fargs) - - Get a Pyro proxy for the server object if we don't already have it, - and handle back compat retry for older daemons. - - """ - items = [ - {}, - {"reset": True, "cache_ok": False}, - {"reset": True, "cache_ok": False, "old": True}, - ] - for hash_name in OK_HASHES[1:]: - items.append( - {"reset": True, "cache_ok": False, "hash_name": hash_name}) - for i, proxy_kwargs in enumerate(items): - func = getattr(self._get_proxy(**proxy_kwargs), fname) - try: - ret = func(*fargs) - break - except Pyro.errors.ProtocolError: - if i + 1 == len(items): # final attempt - raise - self.reg_db.cache_passphrase( - self.suite, self.owner, self.host, self.pphrase) - return ret - - def _set_uri(self): - """Set Pyro URI. - - Determine host and port using content in port file, unless already - specified. - - """ - if ((self.host is None or self.port is None) and - 'CYLC_SUITE_RUN_DIR' in os.environ): - # Looks like we are in a running task job, so we should be able to - # use "cylc-suite-env" file under the suite running directory - try: - suite_env = CylcSuiteEnv.load( - self.suite, os.environ['CYLC_SUITE_RUN_DIR']) - except CylcSuiteEnvLoadError: - if cylc.flags.debug: - traceback.print_exc() - else: - self.host = suite_env.suite_host - self.port = suite_env.suite_port - self.owner = suite_env.suite_owner - - if self.host is None or self.port is None: - port_file_path = os.path.join( - GLOBAL_CFG.get(['pyro', 'ports directory']), self.suite) - if is_remote_host(self.host) or is_remote_user(self.owner): - ssh_tmpl = str(GLOBAL_CFG.get_host_item( - 'remote shell template', self.host, self.owner)) - ssh_tmpl = ssh_tmpl.replace(' %s', '') - user_at_host = '' - if self.owner: - user_at_host = self.owner + '@' - if self.host: - user_at_host += self.host - else: - user_at_host += 'localhost' - r_port_file_path = port_file_path.replace( - os.environ['HOME'], '$HOME') - command = shlex.split(ssh_tmpl) + [ - user_at_host, 'cat', r_port_file_path] - proc = Popen(command, stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - ret_code = proc.wait() - if ret_code: - if cylc.flags.debug: - print >> sys.stderr, { - "code": ret_code, - "command": command, - "stdout": out, - "stderr": err} - raise PortFileError( - "Port file '%s:%s' not found - suite not running?." % - (user_at_host, r_port_file_path)) - else: - try: - out = open(port_file_path).read() - except IOError: - raise PortFileError( - "Port file '%s' not found - suite not running?." % - (port_file_path)) - lines = out.splitlines() - try: - if self.port is None: - self.port = int(lines[0]) - except (IndexError, ValueError): - raise PortFileError( - "ERROR, bad content in port file: %s" % port_file_path) - if self.host is None: - if len(lines) >= 2: - self.host = lines[1].strip() - else: - self.host = get_hostname() - - # Qualify the obj name with user and suite name (unnecessary but - # can't change it until we break back-compat with older daemons). - self.uri = ( - 'PYROLOC://%(host)s:%(port)s/%(owner)s.%(suite)s.%(target)s' % { - "host": self.host, - "port": self.port, - "suite": self.suite, - "owner": self.owner, - "target": self.target_server_object}) - - def _get_proxy(self, reset=True, hash_name=None, cache_ok=True, old=False): - """Get a Pyro proxy.""" - if reset or self.pyro_proxy is None: - self._set_uri() - self.pphrase = self.reg_db.load_passphrase( - self.suite, self.owner, self.host, cache_ok) - # Fails only for unknown hosts (no connection till RPC call). - self.pyro_proxy = Pyro.core.getProxyForURI(self.uri) - self.pyro_proxy._setTimeout(self.pyro_timeout) - if old: - self.pyro_proxy._setIdentification(self.pphrase) - else: - conn_val = ConnValidator() - if hash_name is None: - hash_name = getattr(self, "_hash_name", None) - if hash_name is not None and hash_name in OK_HASHES: - conn_val.set_default_hash(hash_name) - self.pyro_proxy._setNewConnectionValidator(conn_val) - self.pyro_proxy._setIdentification( - (self.my_uuid, self.pphrase)) - return self.pyro_proxy - - def reset(self): - """Reset pyro_proxy.""" - self.pyro_proxy = None - - def signout(self): - """Multi-connect clients should call this on exit.""" - try: - self._get_proxy().signout() - except Exception: - # Suite may have stopped before the client exits. - pass diff --git a/lib/cylc/network/pyro_daemon.py b/lib/cylc/network/pyro_daemon.py deleted file mode 100644 index 1f3bef44457..00000000000 --- a/lib/cylc/network/pyro_daemon.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -"""Wrap Pyro daemon for a suite.""" - -import socket -import traceback - -import Pyro -from cylc.cfgspec.globalcfg import GLOBAL_CFG -from cylc.network.connection_validator import ConnValidator -from cylc.owner import USER -from cylc.registration import RegistrationDB - - -class PyroDaemon(object): - """Wrap Pyro daemon for a suite.""" - - def __init__(self, suite, suite_dir): - # Suite only needed for back-compat with old clients (see below): - self.suite = suite - - Pyro.config.PYRO_MULTITHREADED = 1 - # Use dns names instead of fixed ip addresses from /etc/hosts - # (see the Userguide "Networking Issues" section). - Pyro.config.PYRO_DNS_URI = True - - # Base Pyro socket number. - Pyro.config.PYRO_PORT = GLOBAL_CFG.get(['pyro', 'base port']) - # Max number of sockets starting at base. - Pyro.config.PYRO_PORT_RANGE = GLOBAL_CFG.get( - ['pyro', 'maximum number of ports']) - - Pyro.core.initServer() - self.daemon = Pyro.core.Daemon() - cval = ConnValidator() - self.daemon.setNewConnectionValidator(cval) - cval.set_pphrase(RegistrationDB.load_passphrase_from_dir(suite_dir)) - - def shutdown(self): - """Shutdown the daemon.""" - self.daemon.shutdown(True) - # If a suite shuts down via 'stop --now' or # Ctrl-C, etc., - # any existing client end connections will hang for a long time - # unless we do the following (or cylc clients set a timeout, - # presumably) which daemon.shutdown() does not do (why not?): - - try: - self.daemon.sock.shutdown(socket.SHUT_RDWR) - except socket.error: - traceback.print_exc() - - # Force all Pyro threads to stop now, to prevent them from raising any - # exceptions during Python interpreter shutdown - see GitHub #1890. - self.daemon.closedown() - - def connect(self, obj, name): - """Connect obj and name to the daemon.""" - if not obj.__class__.__name__ == 'SuiteIdServer': - # Qualify the obj name with user and suite name (unnecessary but - # can't change it until we break back-compat with older daemons). - name = "%s.%s.%s" % (USER, self.suite, name) - self.daemon.connect(obj, name) - - def disconnect(self, obj): - """Disconnect obj from the daemon.""" - self.daemon.disconnect(obj) - - def handle_requests(self, timeout=None): - """Handle Pyro requests.""" - self.daemon.handleRequests(timeout) - - def get_port(self): - """Return the daemon port.""" - return self.daemon.port diff --git a/lib/cylc/network/suite_broadcast_client.py b/lib/cylc/network/suite_broadcast_client.py new file mode 100644 index 00000000000..3867a3b0cc3 --- /dev/null +++ b/lib/cylc/network/suite_broadcast_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_broadcast_client import BroadcastClient diff --git a/lib/cylc/network/suite_broadcast_server.py b/lib/cylc/network/suite_broadcast_server.py new file mode 100644 index 00000000000..b04d5fb888e --- /dev/null +++ b/lib/cylc/network/suite_broadcast_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_broadcast_server import BroadcastServer diff --git a/lib/cylc/network/suite_command.py b/lib/cylc/network/suite_command.py deleted file mode 100644 index 8f521472f39..00000000000 --- a/lib/cylc/network/suite_command.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import os -from Queue import Queue - -import cylc.flags -from cylc.network import PYRO_CMD_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer -from cylc.network import check_access_priv - -# Back-compat for older suite daemons <= 6.4.1. -back_compat = { - 'set_stop_cleanly': 'stop cleanly', - 'stop_now': 'stop now', - 'set_stop_after_point': 'stop after point', - 'set_stop_after_clock_time': 'stop after clock time', - 'set_stop_after_task': 'stop after task', - 'release_suite': 'release suite', - 'release_task': 'release task', - 'remove_cycle': 'remove cycle', - 'remove_task': 'remove task', - 'hold_suite': 'hold suite now', - 'hold_after_point_string': 'hold suite after', - 'hold_task': 'hold task now', - 'set_runahead': 'set runahead', - 'set_verbosity': 'set verbosity', - 'purge_tree': 'purge tree', - 'reset_task_state': 'reset task state', - 'trigger_task': 'trigger task', - 'dry_run_task': 'dry run task', - 'nudge': 'nudge suite', - 'insert_task': 'insert task', - 'reload_suite': 'reload suite', - 'add_prerequisite': 'add prerequisite', - 'poll_tasks': 'poll tasks', - 'kill_tasks': 'kill tasks', - 'spawn_tasks': 'reset_task_state -s spawn' -} - - -class SuiteCommandServer(PyroServer): - """Server-side suite command interface.""" - - def __init__(self): - super(SuiteCommandServer, self).__init__() - self.queue = Queue() - - def put(self, command, *command_args): - if 'stop' in command: - check_access_priv(self, 'shutdown') - else: - check_access_priv(self, 'full-control') - self.report(command) - self.queue.put((command, command_args)) - return (True, 'Command queued') - - def get_queue(self): - return self.queue - - -class SuiteCommandClient(PyroClient): - """Client-side suite command interface.""" - - target_server_object = PYRO_CMD_OBJ_NAME - - def put_command(self, *args): - success, msg = self.call_server_func("put", *args) - if msg.startswith('ERROR: Illegal command:'): - # Back-compat for older suite daemons <= 6.4.1. - command = back_compat[args[0]] - args = tuple([command]) + args[1:] - success, msg = self.call_server_func("put", *args) - return (success, msg) diff --git a/lib/cylc/network/suite_command_client.py b/lib/cylc/network/suite_command_client.py new file mode 100644 index 00000000000..59c3a15312d --- /dev/null +++ b/lib/cylc/network/suite_command_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_command_client import SuiteCommandClient diff --git a/lib/cylc/network/suite_command_server.py b/lib/cylc/network/suite_command_server.py new file mode 100644 index 00000000000..54b2ea9ee5b --- /dev/null +++ b/lib/cylc/network/suite_command_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_command_server import SuiteCommandServer diff --git a/lib/cylc/network/suite_identifier_client.py b/lib/cylc/network/suite_identifier_client.py new file mode 100644 index 00000000000..3138db621b6 --- /dev/null +++ b/lib/cylc/network/suite_identifier_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_identifier_client import SuiteIdClientAnon diff --git a/lib/cylc/network/suite_identifier_server.py b/lib/cylc/network/suite_identifier_server.py new file mode 100644 index 00000000000..5682b040352 --- /dev/null +++ b/lib/cylc/network/suite_identifier_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_identifier_server import SuiteIdServer diff --git a/lib/cylc/network/suite_info.py b/lib/cylc/network/suite_info.py deleted file mode 100644 index bc99c118c0f..00000000000 --- a/lib/cylc/network/suite_info.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys - -import cylc.flags -from cylc.network import PYRO_INFO_OBJ_NAME -from cylc.network.pyro_base import PyroClient, PyroServer -from cylc.network import check_access_priv -from cylc.network.connection_validator import SCAN_HASH - - -# Back-compat for older suite daemons <= 6.4.1. -back_compat = { - 'ping_suite': 'ping suite', - 'ping_task': 'ping task', - 'get_suite_info': 'suite info', - 'get_task_info': 'task info', - 'get_all_families': 'all families', - 'get_first_parent_ancestors': 'first-parent ancestors', - 'get_first_parent_descendants': 'first-parent descendants', - 'get_graph_raw': 'graph raw', - 'get_task_requisites': 'task requisites', - 'get_cylc_version': 'get cylc version', - 'get_task_jobfile_path': 'task job file path' -} - - -class SuiteInfoServer(PyroServer): - """Server-side suite information interface.""" - - def __init__(self, info_commands): - super(SuiteInfoServer, self).__init__() - self.commands = info_commands - - def get(self, command, *command_args): - if ('ping' in command or 'version' in command): - # Free info. - pass - elif 'suite' in command and 'info' in command: - # Suite title and description only. - check_access_priv(self, 'description') - else: - check_access_priv(self, 'full-read') - self.report(command) - return self.commands[command](*command_args) - - -class SuiteInfoClient(PyroClient): - """Client-side suite information interface.""" - - target_server_object = PYRO_INFO_OBJ_NAME - - def get_info(self, *args): - try: - return self.call_server_func("get", *args) - except KeyError: - # Back-compat for older suite daemons <= 6.4.1. - command = back_compat[args[0]] - args = tuple([command]) + args[1:] - return self.call_server_func("get", *args) - - def set_use_scan_hash(self): - """Use the configured scan hash for backwards compatibility.""" - self._hash_name = SCAN_HASH diff --git a/lib/cylc/network/suite_info_client.py b/lib/cylc/network/suite_info_client.py new file mode 100644 index 00000000000..5cf8e1f33be --- /dev/null +++ b/lib/cylc/network/suite_info_client.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_info_client import ( + SuiteInfoClient, SuiteInfoClientAnon) diff --git a/lib/cylc/network/suite_info_server.py b/lib/cylc/network/suite_info_server.py new file mode 100644 index 00000000000..302596decf0 --- /dev/null +++ b/lib/cylc/network/suite_info_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_info_server import SuiteInfoServer diff --git a/lib/cylc/network/suite_log_client.py b/lib/cylc/network/suite_log_client.py new file mode 100644 index 00000000000..ec02420ec1f --- /dev/null +++ b/lib/cylc/network/suite_log_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_log_client import SuiteLogClient diff --git a/lib/cylc/network/suite_log_server.py b/lib/cylc/network/suite_log_server.py new file mode 100644 index 00000000000..d7836510eb7 --- /dev/null +++ b/lib/cylc/network/suite_log_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.suite_log_server import SuiteLogServer diff --git a/lib/cylc/network/suite_state_client.py b/lib/cylc/network/suite_state_client.py new file mode 100644 index 00000000000..a197a1fc40d --- /dev/null +++ b/lib/cylc/network/suite_state_client.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + +if METHOD == "https": + from cylc.network.https.suite_state_client import ( + StateSummaryClient, extract_group_state, + get_id_summary, SUITE_STATUS_SPLIT_REC, + get_suite_status_string, SuiteStillInitialisingError, + SUITE_STATUS_NOT_CONNECTED, SUITE_STATUS_CONNECTED, + SUITE_STATUS_INITIALISING, SUITE_STATUS_STOPPED, SUITE_STATUS_STOPPING, + SUITE_STATUS_STOPPED_WITH + ) diff --git a/lib/cylc/network/suite_state_server.py b/lib/cylc/network/suite_state_server.py new file mode 100644 index 00000000000..acfc9330c9c --- /dev/null +++ b/lib/cylc/network/suite_state_server.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + +if METHOD == "https": + from cylc.network.https.suite_state_server import StateSummaryServer diff --git a/lib/cylc/network/task_msg_client.py b/lib/cylc/network/task_msg_client.py new file mode 100644 index 00000000000..5ea4b1858dd --- /dev/null +++ b/lib/cylc/network/task_msg_client.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.task_msg_client import TaskMessageClient diff --git a/lib/cylc/network/task_msg_server.py b/lib/cylc/network/task_msg_server.py new file mode 100644 index 00000000000..8b2073e0721 --- /dev/null +++ b/lib/cylc/network/task_msg_server.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# THIS FILE IS PART OF THE CYLC SUITE ENGINE. +# Copyright (C) 2008-2016 NIWA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Wrap communications daemon for a suite.""" + +from cylc.network.method import METHOD + + +if METHOD == "https": + from cylc.network.https.task_msg_server import TaskMessageServer diff --git a/lib/cylc/option_parsers.py b/lib/cylc/option_parsers.py index 312ca1a1481..29f950cdd20 100644 --- a/lib/cylc/option_parsers.py +++ b/lib/cylc/option_parsers.py @@ -77,7 +77,7 @@ class CylcOptionParser(OptionParser): avoid this, use the '--no-multitask-compat' option, or use the new syntax (with a '/' or a '.') when specifying 2 TASKID arguments.""" - def __init__(self, usage, argdoc=None, pyro=False, noforce=False, + def __init__(self, usage, argdoc=None, comms=False, noforce=False, jset=False, multitask=False, prep=False, twosuites=False, auto_add=True): @@ -100,7 +100,7 @@ def __init__(self, usage, argdoc=None, pyro=False, noforce=False, self.n_compulsory_args = 0 self.n_optional_args = 0 self.unlimited_args = False - self.pyro = pyro + self.comms = comms self.jset = jset self.noforce = noforce @@ -185,7 +185,7 @@ def add_std_options(self): except OptionConflictError: pass - if self.pyro: + if self.comms: try: self.add_option( "--port", @@ -219,14 +219,14 @@ def add_std_options(self): try: self.add_option( - "--pyro-timeout", metavar='SEC', + "--comms-timeout", "--pyro-timeout", metavar='SEC', help=( "Set a timeout for network connections " "to the running suite. The default is no timeout. " "For task messaging connections see " "site/user config file documentation." ), - action="store", default=None, dest="pyro_timeout") + action="store", default=None, dest="comms_timeout") except OptionConflictError: pass @@ -418,6 +418,5 @@ def parse_multitask_compat(cls, options, mtask_args): # Element 1 may be a regular expression, so it may contain "." but # should not contain a "/". # All other elements should contain no "." and "/". - return (mtask_args[0], mtask_args[1]) - else: - return (mtask_args, None) + return (mtask_args[0] + "." + mtask_args[1],) + return mtask_args diff --git a/lib/cylc/registration.py b/lib/cylc/registration.py index ccd35829906..173f8b24e72 100644 --- a/lib/cylc/registration.py +++ b/lib/cylc/registration.py @@ -18,21 +18,15 @@ """Simple suite name registration database.""" import os -import random import re -import shlex from string import ascii_letters, digits -from subprocess import Popen, PIPE import sys -from tempfile import NamedTemporaryFile -import traceback -from cylc.cfgspec.globalcfg import GLOBAL_CFG import cylc.flags from cylc.mkdir_p import mkdir_p from cylc.owner import USER, is_remote_user from cylc.regpath import RegPath -from cylc.suite_host import get_hostname, is_remote_host +from cylc.suite_host import get_hostname, is_remote_host, get_local_ip_address REGDB_PATH = os.path.join(os.environ['HOME'], '.cylc', 'REGDB') @@ -56,6 +50,8 @@ class RegistrationDB(object): PASSPHRASE_FILE_BASE = 'passphrase' PASSPHRASE_CHARSET = ascii_letters + digits PASSPHRASE_LEN = 20 + SSL_CERTIFICATE_FILE_BASE = 'ssl.cert' + SSL_PRIVATE_KEY_FILE_BASE = 'ssl.pem' def __init__(self, dbpath=None): self.dbpath = dbpath or REGDB_PATH @@ -92,6 +88,7 @@ def cache_passphrase(self, suite, owner, host, passphrase): self._dump_passphrase_to_dir(path, passphrase) except (IOError, OSError): if cylc.flags.debug: + import traceback traceback.print_exc() def _dump_passphrase_to_dir(self, path, passphrase=None): @@ -104,10 +101,12 @@ def _dump_passphrase_to_dir(self, path, passphrase=None): 3. Perhaps we should use uuid.uuid4() to generate the passphrase? """ mkdir_p(path) + from tempfile import NamedTemporaryFile handle = NamedTemporaryFile( prefix=self.PASSPHRASE_FILE_BASE, dir=path, delete=False) # Note: Perhaps a UUID might be better here? if passphrase is None: + import random passphrase = ''.join( random.sample(self.PASSPHRASE_CHARSET, self.PASSPHRASE_LEN)) handle.write(passphrase) @@ -119,6 +118,72 @@ def _dump_passphrase_to_dir(self, path, passphrase=None): if cylc.flags.verbose: print 'Generated suite passphrase: %s' % passphrase_file_name + def _dump_certificate_and_key_to_dir(self, path, suite): + """Dump SSL certificate to "ssl.cert" file in "path".""" + try: + from OpenSSL import crypto + except ImportError: + # OpenSSL not installed, so we can't use HTTPS anyway. + return + host = get_hostname() + altnames = ["DNS:*", "DNS:%s" % host, + "IP:%s" % get_local_ip_address(host)] + # Workaround for https://github.com/kennethreitz/requests/issues/2621 + altnames.append("DNS:%s" % get_local_ip_address(host)) + + # Use suite name as the 'common name', but no more than 64 chars. + cert_common_name = suite + if len(suite) > 64: + cert_common_name = suite[:61] + "..." + + # Create a private key. + pkey_obj = crypto.PKey() + pkey_obj.generate_key(crypto.TYPE_RSA, 2048) + + # Create a self-signed certificate. + cert_obj = crypto.X509() + cert_obj.get_subject().O = "Cylc" + cert_obj.get_subject().CN = cert_common_name + cert_obj.gmtime_adj_notBefore(0) + cert_obj.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # 10 years. + cert_obj.set_issuer(cert_obj.get_subject()) + cert_obj.set_pubkey(pkey_obj) + cert_obj.sign(pkey_obj, 'sha256') + + cert_obj.add_extensions([ + crypto.X509Extension( + "subjectAltName", False, ", ".join(altnames) + ) + ]) + + mkdir_p(path) + + # Work in a user-read-write-only directory for guaranteed safety. + from tempfile import mkdtemp + work_dir = mkdtemp() + pkey_file = os.path.join(work_dir, self.SSL_PRIVATE_KEY_FILE_BASE) + cert_file = os.path.join(work_dir, self.SSL_CERTIFICATE_FILE_BASE) + + with open(pkey_file, "w") as file_handle: + file_handle.write( + crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey_obj)) + with open(cert_file, "w") as file_handle: + file_handle.write( + crypto.dump_certificate(crypto.FILETYPE_PEM, cert_obj)) + + import stat + os.chmod(pkey_file, stat.S_IRUSR) + os.chmod(cert_file, stat.S_IRUSR) + pkey_dest_file = os.path.join(path, self.SSL_PRIVATE_KEY_FILE_BASE) + cert_dest_file = os.path.join(path, self.SSL_CERTIFICATE_FILE_BASE) + import shutil + shutil.copy(pkey_file, pkey_dest_file) + shutil.copy(cert_file, cert_dest_file) + shutil.rmtree(work_dir) + if cylc.flags.verbose: + print 'Generated suite SSL certificate: %s' % cert_dest_file + print 'Generated suite SSL private key: %s' % pkey_dest_file + def dump_suite_data(self, suite, data): """Dump suite path and title in text file.""" with open(os.path.join(self.dbpath, suite), 'w') as handle: @@ -148,6 +213,7 @@ def load_all_passphrases(self): self.load_passphrase_from_dir(items[1])) except (IOError, PassphraseError): if cylc.flags.debug: + import traceback traceback.print_exc() # Find all passphrases installed under ~/.cylc/ @@ -158,63 +224,83 @@ def load_all_passphrases(self): self.load_passphrase_from_dir(items[0])) except (IOError, PassphraseError): if cylc.flags.debug: + import traceback traceback.print_exc() return self.local_passphrases + def load_item_from_dir(self, path, item): + if item == "passphrase": + return self.load_passphrase_from_dir(path) + file_name = os.path.join(path, item) + try: + content = open(file_name).read() + except IOError: + raise + if not content: + raise PassphraseError("no content in %s" % file_name) + return file_name + def load_passphrase(self, suite, owner, host, cache_ok=True): - """Search for passphrase file for suite, load and return content. + """Search for passphrase file for suite, load and return content.""" + return self.load_item(suite, owner, host, item="passphrase", + cache_ok=cache_ok) - "passphrase" file is searched from these locations in order: + def load_item(self, suite, owner, host, item="certificate", + create_ok=False, cache_ok=False): + """Load or create a passphrase, SSL certificate or a private key. + + SSL files are searched from these locations in order: 1/ For running task jobs: a/ $CYLC_SUITE_RUN_DIR then $CYLC_SUITE_DEF_PATH for remote jobs. b/ $CYLC_SUITE_DEF_PATH_ON_SUITE_HOST for local jobs or remote jobs with SSH messaging. - 2/ From memory cache, for passphrases of remote suites. + 2/ (Passphrases only) From memory cache, for remote suite passphrases. Don't use if cache_ok=False. 3/ For suite on local user@host. The suite definition directory, as registered. (Note: Previously, this needs to be the 1st location, - else sub-suites load their parent suite's passphrase on start-up - because the "cylc run" command runs in a parent suite task execution - environment. This problem no longer exists becase on suite start up, - the "load_passphrase_from_dir" method is called directly instead of - through this method.) + else sub-suites load their parent suite's passphrases, etc, on + start-up because the "cylc run" command runs in a parent suite task + execution environment. This problem no longer exists becase on suite + start up, the "load_item_from_dir" method is called directly + instead of through this method.) - 4/ Locations under $HOME/.cylc/ for remote suite control from accounts + 4/ Location under $HOME/.cylc/ for remote suite control from accounts that do not actually need the suite definition directory to be - installed (a/ is now preferred. b/ c/ d/ are for back compat): - a/ $HOME/.cylc/passphrases/SUITE_OWNER@SUITE_HOST/SUITE_NAME/ - b/ $HOME/.cylc/SUITE_HOST/SUITE_OWNER/SUITE_NAME/ - c/ $HOME/.cylc/SUITE_HOST/SUITE_NAME/ - d/ $HOME/.cylc/SUITE_NAME/ - Don't use if cache_ok=False. + installed: + $HOME/.cylc/passphrases/SUITE_OWNER@SUITE_HOST/SUITE_NAME/ - 5/ For remote suites, try locating the passphrase file from suite - definition directory on remote owner@host via SSH. + 5/ (SSL files only) If create_ok is specified, create the SSL file and + then return it. - """ - self.can_disk_cache_passphrases[(suite, owner, host)] = False - # (1 before 2 else sub-suites load their parent suite's - # passphrase on start-up because the "cylc run" command runs in - # a parent suite task execution environment). + 6/ For remote suites, try locating the file from the suite definition + directory on remote owner@host via SSH. - # 1/ Running tasks: suite run/def dir from the task job environment. - # Test for presence of task execution environment of requested suite. + """ + item_is_passphrase = False + if item == "certificate": + item = self.SSL_CERTIFICATE_FILE_BASE + elif item == "private_key": + item = self.SSL_PRIVATE_KEY_FILE_BASE + elif item == "passphrase": + item_is_passphrase = True + self.can_disk_cache_passphrases[(suite, owner, host)] = False + + suite_host = os.getenv('CYLC_SUITE_HOST') + suite_owner = os.getenv('CYLC_SUITE_OWNER') if suite == os.getenv('CYLC_SUITE_NAME'): - suite_host = os.getenv('CYLC_SUITE_HOST') - suite_owner = os.getenv('CYLC_SUITE_OWNER') env_keys = [] if is_remote_host(suite_host) or is_remote_user(suite_owner): - # 2(i)/ Task messaging call on a remote account. + # 1(a)/ Task messaging call on a remote account. # First look in the remote suite run directory than suite # definition directory ($CYLC_SUITE_DEF_PATH is modified # for remote tasks): env_keys = ['CYLC_SUITE_RUN_DIR', 'CYLC_SUITE_DEF_PATH'] elif suite_host or suite_owner: - # 2(ii)/ Task messaging call on the suite host account. + # 1(b)/ Task messaging call on the suite host account. # Could be a local task or a remote task with 'ssh # messaging = True'. In either case use @@ -222,66 +308,73 @@ def load_passphrase(self, suite, owner, host, cache_ok=True): # changes, not $CYLC_SUITE_DEF_PATH which gets # modified for remote tasks as described above. env_keys = ['CYLC_SUITE_DEF_PATH_ON_SUITE_HOST'] - for env_key in env_keys: + for key in env_keys: try: - return self.load_passphrase_from_dir(os.environ[env_key]) + return self.load_item_from_dir(os.environ[key], item) except (KeyError, IOError, PassphraseError): pass # 2/ From memory cache - if owner is None: - owner = USER - if host is None: - host = get_hostname() - - if cache_ok: + if cache_ok and item_is_passphrase: + pass_owner = owner + pass_host = host + if pass_owner is None: + pass_owner = USER + if pass_host is None: + pass_host = get_hostname() try: - return self.cached_passphrases[(suite, owner, host)] + return self.cached_passphrases[(suite, pass_owner, pass_host)] except KeyError: pass # 3/ Cylc commands with suite definition directory from local reg. if cache_ok or not is_remote_user(owner) and not is_remote_host(host): try: - return self.load_passphrase_from_dir(self.get_suitedir(suite)) + return self.load_item_from_dir(self.get_suitedir(suite), item) except (IOError, PassphraseError, RegistrationError): pass # 4/ Other allowed locations, as documented above. - # For remote control commands, host here will be fully - # qualified or not depending on what's given on the command line. - if cache_ok: - short_host = host.split('.', 1)[0] + prefix = os.path.expanduser(os.path.join('~', '.cylc')) + if host is None: + host = suite_host + if (owner is not None and host is not None and + (not item_is_passphrase or cache_ok)): prefix = os.path.expanduser(os.path.join('~', '.cylc')) paths = [] - for names in [ - (prefix, self.PASSPHRASES_DIR_BASE, - owner + "@" + host, suite), - (prefix, self.PASSPHRASES_DIR_BASE, - owner + "@" + short_host, suite), - (prefix, host, owner, suite), - (prefix, short_host, owner, suite), - (prefix, host, suite), - (prefix, short_host, suite), - (prefix, suite)]: - path = os.path.join(*names) - if path not in paths: - try: - return self.load_passphrase_from_dir(path) - except (IOError, PassphraseError): - pass - paths.append(path) - - # 5/ Try SSH to remote host - passphrase = self._load_passphrase_via_ssh(suite, owner, host) - if passphrase: - self.can_disk_cache_passphrases[(suite, owner, host)] = True - return passphrase - - if passphrase is None and cylc.flags.debug: - print >> sys.stderr, ( - 'ERROR: passphrase for suite %s not found for %s@%s' % ( - suite, owner, host)) + path_types = [(prefix, self.PASSPHRASES_DIR_BASE, + owner + "@" + host, suite)] + short_host = host.split('.', 1)[0] + if short_host != host: + path_types.append((prefix, self.PASSPHRASES_DIR_BASE, + owner + "@" + short_host, suite)) + + for names in path_types: + try: + return self.load_item_from_dir(os.path.join(*names), item) + except (IOError, PassphraseError): + pass + + if create_ok and not item_is_passphrase: + # 5/ Create the SSL file if it doesn't exist. + return self._dump_certificate_and_key_to_dir( + self.get_suitedir(suite), suite) + + load_dest_root = None + if not item_is_passphrase: + load_dest_root = os.path.join( + prefix, self.PASSPHRASES_DIR_BASE, owner + "@" + host, suite) + try: + # 6/ Try ssh-ing to grab the files directly. + content = self._load_item_via_ssh( + item, suite, owner, host, dest_dir=load_dest_root) + if content and item_is_passphrase: + self.can_disk_cache_passphrases[(suite, owner, host)] = True + return content + except Exception as exc: + import traceback + traceback.print_exc() + raise PassphraseError("Couldn't get %s" % item) @classmethod def load_passphrase_from_dir(cls, path): @@ -303,7 +396,11 @@ def load_passphrase_from_dir(cls, path): return passphrase def _load_passphrase_via_ssh(self, suite, owner, host): - """Load passphrase from remote [owner@]host via SSH.""" + return self._load_item_via_ssh( + self.PASSPHRASE_FILE_BASE, suite, owner, host) + + def _load_item_via_ssh(self, item, suite, owner, host, dest_dir=None): + """Load item (e.g. passphrase) from remote [owner@]host via SSH.""" if not is_remote_host(host) and not is_remote_user(owner): return # Prefix STDOUT to ensure returned content is relevant @@ -313,28 +410,43 @@ def _load_passphrase_via_ssh(self, suite, owner, host): script = ( r'''echo -n '%(prefix)s'; ''' r'''sed -n 's/^path=//p' '.cylc/REGDB/%(suite)s' | ''' - r'''xargs -I '{}' cat '{}/passphrase'; ''' + r'''xargs -I '{}' cat '{}/%(item)s'; ''' r'''echo''' - ) % {'prefix': prefix, 'suite': suite} + ) % {'prefix': prefix, 'suite': suite, 'item': item} + from cylc.cfgspec.globalcfg import GLOBAL_CFG ssh_tmpl = str(GLOBAL_CFG.get_host_item( 'remote shell template', host, owner)) ssh_tmpl = ssh_tmpl.replace(' %s', '') # back compat + import shlex command = shlex.split(ssh_tmpl) + ['-n', owner + '@' + host, script] + from subprocess import Popen, PIPE try: proc = Popen(command, stdout=PIPE, stderr=PIPE) except OSError: if cylc.flags.debug: + import traceback traceback.print_exc() return out, err = proc.communicate() ret_code = proc.wait() # Extract passphrase from STDOUT # It should live in the line with the correct prefix - passphrase = None - for line in out.splitlines(): - if line.startswith(prefix): - passphrase = line.replace(prefix, '').strip() - if not passphrase or ret_code: + if item == self.PASSPHRASE_FILE_BASE: + content = None + for line in out.splitlines(): + if line.startswith(prefix): + content = line.replace(prefix, '').strip() + else: + content = [] + content_has_started = False + for line in out.splitlines(): + if line.startswith(prefix): + line = line.replace(prefix, '') + content_has_started = True + if content_has_started: + content.append(line) + content = "\n".join(content) + if not content or ret_code: if cylc.flags.debug: print >> sys.stderr, ( 'ERROR: %(command)s # code=%(ret_code)s\n%(err)s\n' @@ -346,7 +458,17 @@ def _load_passphrase_via_ssh(self, suite, owner, host): 'ret_code': ret_code, } return - return passphrase + if dest_dir is not None: + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + os.chmod(dest_dir, 0700) + dest_item = os.path.join(dest_dir, item) + file_handle = open(dest_item, "w") + file_handle.write(content) + file_handle.close() + os.chmod(dest_item, 0600) + return dest_item + return content def register(self, name, path): """Register a suite, its source path and its title.""" @@ -374,12 +496,19 @@ def register(self, name, path): print 'REGISTER', name + ':', path self.dump_suite_data(name, {'path': path, 'title': title}) - # Create a new passphrase for the suite if necessary + # Create a new passphrase for the suite if necessary. try: - self.load_passphrase_from_dir(path) + self.load_item_from_dir(path, "passphrase") except (IOError, PassphraseError): self._dump_passphrase_to_dir(path) + # Create a new certificate/private key for the suite if necessary. + try: + self.load_item_from_dir(path, self.SSL_PRIVATE_KEY_FILE_BASE) + self.load_item_from_dir(path, self.SSL_CERTIFICATE_FILE_BASE) + except (IOError, PassphraseError): + self._dump_certificate_and_key_to_dir(path, name) + def get_suite_data(self, suite): """Return {"path": path, "title": title} a suite.""" suite = RegPath(suite).get() @@ -441,7 +570,8 @@ def unregister(self, exp): """Un-register a suite.""" unregistered_set = set() skipped_set = set() - ports_d = GLOBAL_CFG.get(['pyro', 'ports directory']) + from cylc.cfgspec.globalcfg import GLOBAL_CFG + ports_d = GLOBAL_CFG.get(['communication', 'ports directory']) for name in sorted(self.list_all_suites()): if not re.match(exp + r'\Z', name): continue @@ -454,7 +584,9 @@ def unregister(self, exp): print >> sys.stderr, ( 'SKIP UNREGISTER %s: port file exists' % (name)) continue - for base_name in ['passphrase', 'suite.rc.processed']: + for base_name in ['passphrase', 'suite.rc.processed', + self.SSL_CERTIFICATE_FILE_BASE, + self.SSL_PRIVATE_KEY_FILE_BASE]: try: os.unlink(os.path.join(data['path'], base_name)) except OSError: diff --git a/lib/cylc/scheduler.py b/lib/cylc/scheduler.py index 4697e1528d9..2231ff58f9c 100644 --- a/lib/cylc/scheduler.py +++ b/lib/cylc/scheduler.py @@ -51,17 +51,17 @@ from cylc.log_diagnosis import LogSpec from cylc.mp_pool import SuiteProcContext, SuiteProcPool from cylc.network import ( - PYRO_SUITEID_OBJ_NAME, PYRO_STATE_OBJ_NAME, - PYRO_CMD_OBJ_NAME, PYRO_BCAST_OBJ_NAME, PYRO_EXT_TRIG_OBJ_NAME, - PYRO_INFO_OBJ_NAME, PYRO_LOG_OBJ_NAME) -from cylc.network.ext_trigger import ExtTriggerServer -from cylc.network.pyro_daemon import PyroDaemon -from cylc.network.suite_broadcast import BroadcastServer -from cylc.network.suite_command import SuiteCommandServer -from cylc.network.suite_identifier import SuiteIdServer -from cylc.network.suite_info import SuiteInfoServer -from cylc.network.suite_log import SuiteLogServer -from cylc.network.suite_state import StateSummaryServer + COMMS_SUITEID_OBJ_NAME, COMMS_STATE_OBJ_NAME, + COMMS_CMD_OBJ_NAME, COMMS_BCAST_OBJ_NAME, COMMS_EXT_TRIG_OBJ_NAME, + COMMS_INFO_OBJ_NAME, COMMS_LOG_OBJ_NAME) +from cylc.network.ext_trigger_server import ExtTriggerServer +from cylc.network.daemon import CommsDaemon +from cylc.network.suite_broadcast_server import BroadcastServer +from cylc.network.suite_command_server import SuiteCommandServer +from cylc.network.suite_identifier_server import SuiteIdServer +from cylc.network.suite_info_server import SuiteInfoServer +from cylc.network.suite_log_server import SuiteLogServer +from cylc.network.suite_state_server import StateSummaryServer from cylc.owner import USER from cylc.registration import RegistrationDB from cylc.regpath import RegPath @@ -95,24 +95,6 @@ class SchedulerStop(CylcError): pass -class PyroRequestHandler(threading.Thread): - """Pyro request handler.""" - - def __init__(self, pyro): - threading.Thread.__init__(self) - self.pyro = pyro - self.quit = False - self.log = LOG - self.log.debug("request handling thread starting") - - def run(self): - while True: - self.pyro.handle_requests(timeout=1) - if self.quit: - break - self.log.debug("request handling thread exiting") - - class Scheduler(object): """Cylc scheduler server.""" @@ -134,15 +116,16 @@ class Scheduler(object): # Dependency negotation etc. will run after these commands PROC_CMDS = ( 'release_suite', - 'release_task', + 'release_tasks', 'kill_tasks', 'set_runahead', - 'reset_task_state', + 'reset_task_states', 'spawn_tasks', - 'trigger_task', + 'trigger_tasks', 'nudge', - 'insert_task', - 'reload_suite') + 'insert_tasks', + 'reload_suite' + ) REF_LOG_TEXTS = ( 'triggered off', 'Initial point', 'Start point', 'Final point') @@ -201,7 +184,7 @@ def __init__(self, is_restart, options, args): self.command_queue = None self.pool = None self.request_handler = None - self.pyro = None + self.comms_daemon = None self._profile_amounts = {} self._profile_update_times = {} @@ -284,14 +267,16 @@ def start(self): pri_dao.close() try: - self._configure_pyro() if not self.options.no_detach and not cylc.flags.debug: daemonize(self) + slog = SuiteLog.get_inst(self.suite) if cylc.flags.debug: slog.pimp(logging.DEBUG) else: slog.pimp() + + self.configure_comms_daemon() self.configure() self.profiler.start() self.run() @@ -334,7 +319,7 @@ def start(self): def _check_port_file_does_not_exist(suite): """Fail if port file exists. Return port file path otherwise.""" port_file_path = os.path.join( - GLOBAL_CFG.get(['pyro', 'ports directory']), suite) + GLOBAL_CFG.get(['communication', 'ports directory']), suite) try: port, host = open(port_file_path).read().splitlines() except (IOError, ValueError): @@ -389,21 +374,6 @@ def _print_blurb(): for i in range(len(logo_lines)): print logo_lines[i], ('{0: ^%s}' % lmax).format(license_lines[i]) - def _configure_pyro(self): - """Create and configure Pyro daemon.""" - self.pyro = PyroDaemon(self.suite, self.suite_dir) - self.port = self.pyro.get_port() - port_file_path = self._check_port_file_does_not_exist(self.suite) - try: - with open(port_file_path, 'w') as handle: - handle.write("%d\n%s\n" % (self.port, self.host)) - except IOError as exc: - ERR.error(str(exc)) - raise SchedulerError( - 'ERROR, cannot write port file: %s' % port_file_path) - else: - self.port_file = port_file_path - def configure(self): """Configure suite daemon.""" self.profiler.log_memory("scheduler.py: start configure") @@ -433,9 +403,7 @@ def configure(self): self.pool = TaskPool( self.suite, self.pri_dao, self.pub_dao, self.final_point, - self.pyro, self.log, self.run_mode) - self.request_handler = PyroRequestHandler(self.pyro) - self.request_handler.start() + self.comms_daemon, self.log, self.run_mode) self.profiler.log_memory("scheduler.py: before load_tasks") if self.is_restart: @@ -675,13 +643,20 @@ def process_command_queue(self): while True: try: - name, args = queue.get(False) + name, args, kwargs = queue.get(False) except Empty: break - log_msg += '\n+\t' + name - cmdstr = name + '(' + ','.join([str(a) for a in args]) + ')' + args_string = ', '.join([str(a) for a in args]) + cmdstr = name + '(' + args_string + kwargs_string = ', '.join( + [key + '=' + str(value) for key, value in kwargs.items()]) + if kwargs_string and args_string: + cmdstr += ', ' + cmdstr += kwargs_string + ')' + log_msg += '\n+\t' + cmdstr try: - n_warnings = getattr(self, "command_%s" % name)(*args) + n_warnings = getattr(self, "command_%s" % name)( + *args, **kwargs) except SchedulerStop: self.log.info('Command succeeded: ' + cmdstr) raise @@ -779,8 +754,10 @@ def info_get_first_parent_ancestors(self, pruned=False): """Single-inheritance hierarchy based on first parents""" return deepcopy(self.config.get_first_parent_ancestors(pruned)) - def info_get_graph_raw(self, cto, ctn, group_nodes, ungroup_nodes, - ungroup_recursive, group_all, ungroup_all): + def info_get_graph_raw(self, cto, ctn, group_nodes=None, + ungroup_nodes=None, + ungroup_recursive=False, group_all=False, + ungroup_all=False): """Return raw graph.""" rgraph = self.config.get_graph_raw( cto, ctn, group_nodes, ungroup_nodes, ungroup_recursive, group_all, @@ -842,25 +819,25 @@ def command_set_stop_after_task(self, task_id): if TaskID.is_valid_id(task_id): self.set_stop_task(task_id) - def command_release_task(self, items, compat=None, _=None): + def command_release_tasks(self, items): """Release tasks.""" - return self.pool.release_tasks(items, compat) + return self.pool.release_tasks(items) - def command_poll_tasks(self, items, compat=None, _=None): + def command_poll_tasks(self, items): """Poll all tasks or a task/family if options are provided.""" - return self.pool.poll_task_jobs(items, compat) + return self.pool.poll_task_jobs(items) - def command_kill_tasks(self, items, compat=None, _=False): + def command_kill_tasks(self, items): """Kill all tasks or a task/family if options are provided.""" - return self.pool.kill_task_jobs(items, compat) + return self.pool.kill_task_jobs(items) def command_release_suite(self): """Release all task proxies in the suite.""" self.release_suite() - def command_hold_task(self, items, compat=None, _=False): + def command_hold_tasks(self, items): """Hold selected task proxies in the suite.""" - return self.pool.hold_tasks(items, compat) + return self.pool.hold_tasks(items) def command_hold_suite(self): """Hold all task proxies in the suite.""" @@ -883,14 +860,13 @@ def command_remove_cycle(self, point_string, spawn=False): """Remove tasks in a cycle.""" return self.pool.remove_tasks(point_string + "/*", spawn) - def command_remove_task(self, items, compat=None, _=None, spawn=False): + def command_remove_tasks(self, items, spawn=False): """Remove tasks.""" - return self.pool.remove_tasks(items, spawn, compat) + return self.pool.remove_tasks(items, spawn) - def command_insert_task( - self, items, compat=None, _=None, stop_point_string=None): + def command_insert_tasks(self, items, stop_point_string=None): """Insert tasks.""" - return self.pool.insert_tasks(items, stop_point_string, compat) + return self.pool.insert_tasks(items, stop_point_string) def command_nudge(self): """Cause the task processing loop to be invoked""" @@ -915,9 +891,9 @@ def command_reload_suite(self): self.pool.put_rundb_suite_params(self.initial_point, self.final_point) self.do_update_state_summary = True - def command_set_runahead(self, *args): + def command_set_runahead(self, interval=None): """Set runahead limit.""" - self.pool.set_runahead(*args) + self.pool.set_runahead(interval=interval) def set_suite_timer(self): """Set suite's timeout timer.""" @@ -942,6 +918,49 @@ def set_suite_inactivity_timer(self, reset=False): self._get_events_conf(self.EVENT_INACTIVITY_TIMEOUT)), get_current_time_string()) + def configure_comms_daemon(self): + """Create and configure daemon.""" + self.comms_daemon = CommsDaemon(self.suite, self.suite_dir) + self.port = self.comms_daemon.get_port() + self.port_file = os.path.join( + GLOBAL_CFG.get(['communication', 'ports directory']), self.suite) + try: + port, host = open(self.port_file).read().splitlines() + except (IOError, ValueError): + # Suite is not likely to be running if port file does not exist + # or if port file does not contain good values of port and host. + pass + else: + sys.stderr.write( + ( + r"""ERROR: port file exists: %(port_file)s + +If %(suite)s is not running, delete the port file and try again. If it is +running but not responsive, kill any left over suite processes too. + +To see if %(suite)s is running on '%(host)s:%(port)s': + * cylc scan -n '\b%(suite)s\b' %(host)s + * cylc ping -v --host=%(host)s %(suite)s + * ssh %(host)s "pgrep -a -P 1 -fu $USER 'cylc-r.* \b%(suite)s\b'" + +""" + ) % { + "host": host, + "port": port, + "port_file": self.port_file, + "suite": self.suite, + } + ) + raise SchedulerError( + "ERROR, port file exists: %s" % self.port_file) + try: + with open(self.port_file, 'w') as handle: + handle.write("%d\n%s\n" % (self.port, self.host)) + except IOError as exc: + sys.stderr.write(str(exc) + "\n") + raise SchedulerError( + 'ERROR, cannot write port file: %s' % self.port_file) + def load_suiterc(self, reconfigure): """Load and log the suite definition.""" SuiteConfig._FORCE = True # Reset the singleton! @@ -1087,32 +1106,32 @@ def configure_suite(self, reconfigure=False): OUT.info("Suite will hold after " + str(self.pool_hold_point)) suite_id = SuiteIdServer.get_inst(self.suite, self.owner) - self.pyro.connect(suite_id, PYRO_SUITEID_OBJ_NAME) + self.comms_daemon.connect(suite_id, COMMS_SUITEID_OBJ_NAME) bcast = BroadcastServer.get_inst( self.config.get_linearized_ancestors()) - self.pyro.connect(bcast, PYRO_BCAST_OBJ_NAME) + self.comms_daemon.connect(bcast, COMMS_BCAST_OBJ_NAME) self.command_queue = SuiteCommandServer() - self.pyro.connect(self.command_queue, PYRO_CMD_OBJ_NAME) + self.comms_daemon.connect(self.command_queue, COMMS_CMD_OBJ_NAME) ets = ExtTriggerServer.get_inst() - self.pyro.connect(ets, PYRO_EXT_TRIG_OBJ_NAME) + self.comms_daemon.connect(ets, COMMS_EXT_TRIG_OBJ_NAME) info_commands = {} for attr_name in dir(self): attr = getattr(self, attr_name) if callable(attr) and attr_name.startswith('info_'): info_commands[attr_name.replace('info_', '')] = attr - self.pyro.connect( - SuiteInfoServer(info_commands), PYRO_INFO_OBJ_NAME) + self.info_interface = SuiteInfoServer(info_commands) + self.comms_daemon.connect(self.info_interface, COMMS_INFO_OBJ_NAME) self.suite_log = SuiteLog.get_inst(self.suite) - self.pyro.connect( - SuiteLogServer(self.suite_log), PYRO_LOG_OBJ_NAME) + log_interface = SuiteLogServer(self.suite_log) + self.comms_daemon.connect(log_interface, COMMS_LOG_OBJ_NAME) self.suite_state = StateSummaryServer.get_inst(self.run_mode) - self.pyro.connect(self.suite_state, PYRO_STATE_OBJ_NAME) + self.comms_daemon.connect(self.suite_state, COMMS_STATE_OBJ_NAME) def configure_suite_environment(self): """Configure suite environment.""" @@ -1128,7 +1147,7 @@ def configure_suite_environment(self): 'CYLC_SUITE_REG_NAME': self.suite, # DEPRECATED 'CYLC_SUITE_HOST': str(self.host), 'CYLC_SUITE_OWNER': self.owner, - 'CYLC_SUITE_PORT': str(self.pyro.get_port()), + 'CYLC_SUITE_PORT': str(self.comms_daemon.get_port()), # DEPRECATED 'CYLC_SUITE_REG_PATH': RegPath(self.suite).get_fpath(), 'CYLC_SUITE_DEF_PATH_ON_SUITE_HOST': self.suite_dir, @@ -1670,24 +1689,25 @@ def shutdown(self, reason=None): proc_pool.join() proc_pool.handle_results_async() - if self.request_handler: - self.request_handler.quit = True - self.request_handler.join() - - if self.pyro: - ifaces = [ - self.command_queue, SuiteIdServer.get_inst(), - StateSummaryServer.get_inst(), ExtTriggerServer.get_inst(), - BroadcastServer.get_inst()] + if self.comms_daemon: + ifaces = [self.command_queue, + SuiteIdServer.get_inst(), StateSummaryServer.get_inst(), + ExtTriggerServer.get_inst(), BroadcastServer.get_inst()] if self.pool is not None: ifaces.append(self.pool.message_queue) for iface in ifaces: try: - self.pyro.disconnect(iface) + self.comms_daemon.disconnect(iface) except KeyError: # Wasn't connected yet. pass - self.pyro.shutdown() + self.comms_daemon.shutdown() + + # Make sure errors and info are written before the port file goes. + sys.stdout.flush() + sys.stderr.flush() + + if self.comms_daemon: try: os.unlink(self.port_file) except OSError as exc: @@ -1783,21 +1803,22 @@ def will_pause_at(self): """Return self.pool.get_hold_point().""" return self.pool.get_hold_point() - def command_trigger_task(self, items, compat=None, _=None): + def command_trigger_tasks(self, items): """Trigger tasks.""" - return self.pool.trigger_tasks(items, compat) + print "Trigger", items + return self.pool.trigger_tasks(items) - def command_dry_run_task(self, items, compat=None): - """Dry-run a task, e.g. edit run.""" - return self.pool.dry_run_task(items, compat) + def command_dry_run_tasks(self, items): + """Dry-run tasks, e.g. edit run.""" + return self.pool.dry_run_task(items) - def command_reset_task_state(self, items, compat=None, state=None, _=None): + def command_reset_task_states(self, items, state=None): """Reset the state of tasks.""" - return self.pool.reset_task_states(items, state, compat) + return self.pool.reset_task_states(items, state) - def command_spawn_tasks(self, items, compat=None, _=None): + def command_spawn_tasks(self, items): """Force spawn task successors.""" - return self.pool.spawn_tasks(items, compat) + return self.pool.spawn_tasks(items) def filter_initial_task_list(self, inlist): """Return list of initial tasks after applying a filter.""" diff --git a/lib/cylc/task_message.py b/lib/cylc/task_message.py index 1504a6d291b..8f70d6194aa 100644 --- a/lib/cylc/task_message.py +++ b/lib/cylc/task_message.py @@ -20,7 +20,6 @@ import os import sys from time import sleep -import traceback from cylc.remote import remrun from cylc.suite_env import CylcSuiteEnv, CylcSuiteEnvLoadError from cylc.wallclock import get_current_time_string @@ -122,11 +121,11 @@ def send(self, messages): if self.ssh_messaging and self._send_by_ssh(): return - self._send_by_pyro(messages) + self._send_by_remote_port(messages) def _get_client(self): - """Return the Pyro client.""" - from cylc.network.task_msgqueue import TaskMessageClient + """Return the communication client.""" + from cylc.network.task_msg_client import TaskMessageClient return TaskMessageClient( self.suite, self.task_id, self.owner, self.host, self.try_timeout, self.port) @@ -141,6 +140,7 @@ def _load_suite_contact_file(self): suite_env = CylcSuiteEnv.load(self.suite, self.suite_run_dir) except CylcSuiteEnvLoadError: if cylc.flags.debug: + import traceback traceback.print_exc() else: for key, attr_key in suite_env.ATTRS.items(): @@ -164,10 +164,9 @@ def _print_messages(self, messages): print >>sys.stderr, "%s%s %s" % ( prefix, self.priority, message) - def _send_by_pyro(self, messages): - """Send message by Pyro.""" + def _send_by_remote_port(self, messages): + """Send message by talking to the daemon (remote?) port.""" self._print_messages(messages) - from Pyro.errors import NamingError sent = False i_try = 0 while not sent and i_try < self.max_tries: @@ -178,16 +177,7 @@ def _send_by_pyro(self, messages): client = self._get_client() for message in messages: client.put(self.priority, message) - except NamingError, exc: - print >> sys.stderr, exc - print "Send message: try %s of %s failed: %s" % ( - i_try, - self.max_tries, - exc - ) - print "Task proxy removed from suite daemon? Aborting." - break - except Exception, exc: + except Exception as exc: print >> sys.stderr, exc print "Send message: try %s of %s failed: %s" % ( i_try, @@ -255,7 +245,7 @@ def _send_by_ssh(self): path = os.path.join(self.env_map['CYLC_DIR_ON_SUITE_HOST'], 'bin') # Return here if remote re-invocation occurred, - # otherwise drop through to local Pyro messaging. + # otherwise drop through to local messaging. # Note: do not sys.exit(0) here as the commands do, it # will cause messaging failures on the remote host. try: diff --git a/lib/cylc/task_pool.py b/lib/cylc/task_pool.py index 2274dc6283a..73d73ce27ae 100644 --- a/lib/cylc/task_pool.py +++ b/lib/cylc/task_pool.py @@ -40,7 +40,8 @@ from time import time import traceback -from cylc.network.task_msgqueue import TaskMessageServer +from cylc.network import COMMS_TASK_MESSAGE_OBJ_NAME +from cylc.network.task_msg_server import TaskMessageServer from cylc.batch_sys_manager import BATCH_SYS_MANAGER from cylc.broker import broker from cylc.cfgspec.globalcfg import GLOBAL_CFG @@ -51,8 +52,8 @@ import cylc.flags from cylc.get_task_proxy import get_task_proxy from cylc.mp_pool import SuiteProcPool, SuiteProcContext -from cylc.network.ext_trigger import ExtTriggerServer -from cylc.network.suite_broadcast import BroadcastServer +from cylc.network.ext_trigger_server import ExtTriggerServer +from cylc.network.suite_broadcast_server import BroadcastServer from cylc.owner import is_remote_user from cylc.rundb import CylcSuiteDAO from cylc.suite_host import is_remote_host @@ -87,10 +88,10 @@ class TaskPool(object): TABLE_TASK_POOL = CylcSuiteDAO.TABLE_TASK_POOL TABLE_CHECKPOINT_ID = CylcSuiteDAO.TABLE_CHECKPOINT_ID - def __init__(self, suite, pri_dao, pub_dao, stop_point, pyro, log, + def __init__(self, suite, pri_dao, pub_dao, stop_point, comms_daemon, log, run_mode): self.suite_name = suite - self.pyro = pyro + self.comms_daemon = comms_daemon self.run_mode = run_mode self.log = log self.stop_point = stop_point @@ -106,9 +107,10 @@ def __init__(self, suite, pri_dao, pub_dao, stop_point, pyro, log, config.get_max_num_active_cycle_points()) self._prev_runahead_base_point = None self._prev_runahead_sequence_points = None - self.message_queue = TaskMessageServer() + self.message_queue = TaskMessageServer(self.suite_name) - self.pyro.connect(self.message_queue, "task_pool") + self.comms_daemon.connect( + self.message_queue, COMMS_TASK_MESSAGE_OBJ_NAME) self.pool = {} self.runahead_pool = {} @@ -146,11 +148,9 @@ def assign_queues(self): for taskname in qconfig[queue]['members']: self.myq[taskname] = queue - def insert_tasks(self, items, stop_point_str, compat=None): + def insert_tasks(self, items, stop_point_str): """Insert tasks.""" n_warnings = 0 - if isinstance(items, str) or compat is not None: - items = [items + "." + compat] config = SuiteConfig.get_inst() names = config.get_task_name_list() fams = config.runtime['first-parent descendants'] @@ -853,7 +853,7 @@ def report_stalled_task_deps(self): for unsatisfied in prereqs['prereqs']: self.log.warning(" * %s" % unsatisfied) - def poll_task_jobs(self, items=None, compat=None): + def poll_task_jobs(self, items=None): """Poll jobs of active tasks. If items is specified, poll active tasks matching given IDs. @@ -861,7 +861,7 @@ def poll_task_jobs(self, items=None, compat=None): """ if self.run_mode == 'simulation': return - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) active_itasks = [] for itask in itasks: if itask.state.status in TASK_STATUSES_ACTIVE: @@ -885,13 +885,13 @@ def poll_task_jobs_callback(self, ctx): }, ) - def kill_task_jobs(self, items=None, compat=None): + def kill_task_jobs(self, items=None): """Kill jobs of active tasks. If items is specified, kill active tasks matching given IDs. """ - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) active_itasks = [] for itask in itasks: is_active = itask.state.status in TASK_STATUSES_ACTIVE @@ -977,16 +977,16 @@ def set_hold_point(self, point): if itask.point > point: itask.state.reset_state(TASK_STATUS_HELD) - def hold_tasks(self, items, compat=None): + def hold_tasks(self, items): """Hold tasks with IDs matching any item in "ids".""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: itask.state.reset_state(TASK_STATUS_HELD) return n_warnings - def release_tasks(self, items, compat=None): + def release_tasks(self, items): """Release held tasks with IDs matching any item in "ids".""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: itask.state.release() return n_warnings @@ -1381,20 +1381,20 @@ def remove_spent_tasks(self): self.remove(itask) return len(spent) - def spawn_tasks(self, items, compat): + def spawn_tasks(self, items): """Force tasks to spawn successors if they haven't already. """ - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: if not itask.has_spawned: itask.log(INFO, "forced spawning") self.force_spawn(itask) return n_warnings - def reset_task_states(self, items, status, compat): + def reset_task_states(self, items, status): """Reset task states.""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: itask.log(INFO, "resetting state to %s" % status) if status == TASK_STATUS_READY: @@ -1413,18 +1413,18 @@ def reset_task_states(self, items, status, compat): itask.state.reset_state(status) return n_warnings - def remove_tasks(self, items, spawn=False, compat=None): + def remove_tasks(self, items, spawn=False): """Remove tasks from pool.""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: if spawn: self.force_spawn(itask) self.remove(itask, 'by request') return n_warnings - def trigger_tasks(self, items, compat=None): + def trigger_tasks(self, items): """Trigger tasks.""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) for itask in itasks: if itask.state.status in TASK_STATUSES_ACTIVE: self.log.warning('%s: already triggered' % itask.identity) @@ -1435,14 +1435,10 @@ def trigger_tasks(self, items, compat=None): itask.state.reset_state(TASK_STATUS_READY) return n_warnings - def dry_run_task(self, items, compat=None): + def dry_run_task(self, items): """Create job file for "cylc trigger --edit".""" - itasks, n_warnings = self._filter_task_proxies(items, compat) + itasks, n_warnings = self._filter_task_proxies(items) if len(itasks) > 1: - if isinstance(items, str) and compat is not None: - items = items + "." + compat - elif compat is not None: - items = "*." + compat self.log.warning("Unique task match not found: %s" % items) n_warnings += 1 else: @@ -1639,7 +1635,7 @@ def put_rundb_task_pool(self): "time": get_current_time_string(), "event": CylcSuiteDAO.CHECKPOINT_LATEST_EVENT}) - def _filter_task_proxies(self, items, compat=None): + def _filter_task_proxies(self, items): """Return task proxies that match names, points, states in items. In the new form, the arguments should look like: @@ -1647,41 +1643,12 @@ def _filter_task_proxies(self, items, compat=None): the general form name[.point][:state] or [point/]name[:state] where name is a glob-like pattern for matching a task name or a family name. - compat -- not used - - In the old form, "items" is a string containing a regular expression - for matching task/family names, and "compat" is a string containing a - point string. """ itasks = [] n_warnings = 0 - if not items and compat is None: + if not items: itasks += self.get_all_tasks() - elif isinstance(items, str) or compat is not None: - try: - point_str = standardise_point_string(compat) - except ValueError as exc: - self.log.warning( - self.ERR_PREFIX_TASKID_MATCH + - ("%s.%s: %s" % (items, compat, exc))) - n_warnings += 1 - else: - name_rec = re.compile(items) - for itask in self.get_all_tasks(): - nss = itask.tdef.namespace_hierarchy - if ( - (point_str is None or - str(itask.point) == point_str) and - (name_rec.match(itask.tdef.name) or - any([name_rec.match(ns) for ns in nss])) - ): - itasks.append(itask) - if not itasks: - self.log.warning( - self.ERR_PREFIX_TASKID_MATCH + - ("%s.%s" % (items, compat))) - n_warnings += 1 else: for item in items: point_str, name_str, status = self._parse_task_item(item) diff --git a/lib/cylc/wallclock.py b/lib/cylc/wallclock.py index 3231e31af60..661070082ff 100644 --- a/lib/cylc/wallclock.py +++ b/lib/cylc/wallclock.py @@ -20,7 +20,6 @@ from calendar import timegm from datetime import datetime, timedelta -from isodatetime.data import Duration from isodatetime.parsers import TimePointParser from isodatetime.timezone import ( get_local_time_zone_format, get_local_time_zone) @@ -243,4 +242,5 @@ def get_unix_time_from_time_string(datetime_string): def get_seconds_as_interval_string(seconds): """Convert a number of seconds into an ISO 8601 duration string.""" + from isodatetime.data import Duration return str(Duration(seconds=seconds, standardize=True)) diff --git a/licences/LICENSE-MIT-PYRO b/licences/LICENSE-MIT-PYRO deleted file mode 100644 index 7a47de06368..00000000000 --- a/licences/LICENSE-MIT-PYRO +++ /dev/null @@ -1,28 +0,0 @@ - -PYRO - Python Remote Objects -Software License, copyright, and disclaimer - - PYRO is Copyright (c) by Irmen de Jong (irmen@razorvine.net) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - -This is the "MIT Software License" which is OSI-certified, and GPL-compatible. -See http://www.opensource.org/licenses/mit-license.php - diff --git a/tests/authentication/00-identity.t b/tests/authentication/00-identity.t index 1e23a44a5f7..e51bfacde33 100644 --- a/tests/authentication/00-identity.t +++ b/tests/authentication/00-identity.t @@ -52,13 +52,13 @@ __END__ TEST_NAME="${TEST_NAME_BASE}-show" run_fail "${TEST_NAME}" cylc show "${SUITE_NAME}" cylc log "${SUITE_NAME}" > suite.log1 -grep_ok "\[client-connect] DENIED (privilege 'identity' < 'description') ${USER}@.*:cylc-show" suite.log1 +grep_ok "\[client-connect\] DENIED (privilege 'identity' < 'description') ${USER}@.*:cylc-show" suite.log1 # Commands should be denied. TEST_NAME="${TEST_NAME_BASE}-stop" run_fail "${TEST_NAME}" cylc stop "${SUITE_NAME}" cylc log "${SUITE_NAME}" > suite.log2 -grep_ok "\[client-connect] DENIED (privilege 'identity' < 'shutdown') ${USER}@.*:cylc-stop" suite.log2 +grep_ok "\[client-connect\] DENIED (privilege 'identity' < 'shutdown') ${USER}@.*:cylc-stop" suite.log2 # Restore the passphrase. mv "${TEST_DIR}/${SUITE_NAME}/passphrase.DIS" \ diff --git a/tests/authentication/05-full-control.t b/tests/authentication/05-full-control.t index 64d5732a95e..e8cf367d3bf 100644 --- a/tests/authentication/05-full-control.t +++ b/tests/authentication/05-full-control.t @@ -55,23 +55,23 @@ __END__ TEST_NAME="${TEST_NAME_BASE}-show1" run_ok "${TEST_NAME}" cylc show "${SUITE_NAME}" cylc log "${SUITE_NAME}" > suite.log1 -grep_ok "\[client-command] get_suite_info ${USER}@.*:cylc-show" suite.log1 +grep_ok "\[client-command\] get_suite_info ${USER}@.*:cylc-show" suite.log1 # "cylc show" (task info) OK. TEST_NAME="${TEST_NAME_BASE}-show2" run_ok "${TEST_NAME}" cylc show "${SUITE_NAME}" foo.1 cylc log "${SUITE_NAME}" > suite.log2 -grep_ok "\[client-command] get_task_info ${USER}@.*:cylc-show" suite.log2 +grep_ok "\[client-command\] get_task_info ${USER}@.*:cylc-show" suite.log2 # Commands OK. # (Reset to same state). TEST_NAME="${TEST_NAME_BASE}-trigger" run_ok "${TEST_NAME}" cylc reset "${SUITE_NAME}" -s failed foo 1 cylc log "${SUITE_NAME}" > suite.log3 -grep_ok "\[client-command] reset_task_state ${USER}@.*:cylc-reset" suite.log3 +grep_ok "\[client-command\] reset_task_states ${USER}@.*:cylc-reset" suite.log3 # Shutdown and purge. TEST_NAME="${TEST_NAME_BASE}-stop" -run_ok "${TEST_NAME}" cylc stop --max-polls=10 --interval=1 "${SUITE_NAME}" +run_ok "${TEST_NAME}" cylc stop --max-polls=20 --interval=1 "${SUITE_NAME}" purge_suite "${SUITE_NAME}" exit diff --git a/tests/authentication/07-back-compat.t b/tests/authentication/07-back-compat.t deleted file mode 100644 index eaa448ea03c..00000000000 --- a/tests/authentication/07-back-compat.t +++ /dev/null @@ -1,172 +0,0 @@ -#!/bin/bash -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Test authentication - ignore old client denials, report bad new clients. - -. $(dirname $0)/test_header -set_test_number 23 - -# Set things up and run the suite. -# Choose the default global.rc hash settings, for reference. -create_test_globalrc '' ' -[authentication] - hashes = sha256,md5 - scan hash = md5' -install_suite "${TEST_NAME_BASE}" basic -TEST_NAME="${TEST_NAME_BASE}-validate" -run_ok "${TEST_NAME}" cylc validate "${SUITE_NAME}" -cylc run "${SUITE_NAME}" - -# Scan to grab the suite's port. -sleep 5 # Wait for the suite to initialize. -PORT=$(cylc ping -v "${SUITE_NAME}" | cut -d':' -f 2) - -# Simulate an old client with the wrong passphrase. -ERR_PATH="$(cylc get-global-config --print-run-dir)/${SUITE_NAME}/log/suite/err" -TEST_NAME="${TEST_NAME_BASE}-old-client-checkpoint-err" -run_ok "${TEST_NAME}" cp "${ERR_PATH}" err-before-scan -TEST_NAME="${TEST_NAME_BASE}-old-client-simulate" -run_fail "${TEST_NAME}" python -c " -import sys -import Pyro.core -uri = 'PYROLOC://localhost:' + sys.argv[1] + '/cylcid' -proxy = Pyro.core.getProxyForURI(uri) -proxy._setIdentification('0123456789abcdef') -name, owner = proxy.id()" "${PORT}" -grep_ok "ConnectionDeniedError" "${TEST_NAME}.stderr" - -# Check that the old client connection is not logged. -# Get any new lines added to the error file. -comm -13 err-before-scan "${ERR_PATH}" >"${TEST_NAME_BASE}-old-client-err-diff" -TEST_NAME="${TEST_NAME_BASE}-log-old-client" -run_fail "${TEST_NAME}" grep "WARNING - \[client-connect\] DENIED" \ - "${TEST_NAME_BASE}-old-client-err-diff" - -# Simulate an old client with the right passphrase. -ERR_PATH="$(cylc get-global-config --print-run-dir)/${SUITE_NAME}/log/suite/err" -TEST_NAME="${TEST_NAME_BASE}-old-client-checkpoint-ok" -run_ok "${TEST_NAME}" cp "${ERR_PATH}" err-before-scan -TEST_NAME="${TEST_NAME_BASE}-old-client-simulate-ok" -PASSPHRASE=$(cat $(cylc get-dir $SUITE_NAME)/passphrase) -run_ok "${TEST_NAME}" python -c " -import sys -import Pyro.core -uri = 'PYROLOC://localhost:' + sys.argv[1] + '"/${USER}.${SUITE_NAME}.suite-info"' -print >> sys.stderr, uri -proxy = Pyro.core.getProxyForURI(uri) -proxy._setIdentification('"$PASSPHRASE"') -info = proxy.get('get_suite_info')" "${PORT}" -grep_ok "\[client-command\] get_suite_info (user)@(host):(OLD_CLIENT) (uuid)" "$(cylc cat-log -l $SUITE_NAME)" - -# Simulate a new, suspicious client. -TEST_NAME="${TEST_NAME_BASE}-new-bad-client-checkpoint-err" -run_ok "${TEST_NAME}" cp "${ERR_PATH}" err-before-scan -TEST_NAME="${TEST_NAME_BASE}-new-bad-client-simulate" -run_fail "${TEST_NAME}" python -c ' -import sys -import Pyro.core, Pyro.protocol - -class MyConnValidator(Pyro.protocol.DefaultConnValidator): - - """Create an incorrect but plausible auth token.""" - - def createAuthToken(self, authid, challenge, peeraddr, URI, daemon): - return "colonel_mustard:drawing_room:dagger:mystery:decea5ede57abbed" - -uri = "PYROLOC://localhost:" + sys.argv[1] + "/cylcid" -proxy = Pyro.core.getProxyForURI(uri) -proxy._setNewConnectionValidator(MyConnValidator()) -proxy._setIdentification("0123456789abcdef") -proxy.identify()' "${PORT}" -grep_ok "ConnectionDeniedError" "${TEST_NAME}.stderr" - -# Check that the new client connection failure is logged (it is suspicious). -TEST_NAME="${TEST_NAME_BASE}-log-new-client" -# Get any new lines added to the error file. -comm -13 err-before-scan "${ERR_PATH}" >"${TEST_NAME}-new-client-err-diff" -# Check the new lines for a connection denied report. -grep_ok "WARNING - \[client-connect\] DENIED colonel_mustard@drawing_room:mystery dagger$" \ - "${TEST_NAME}-new-client-err-diff" - -# Simulate a client with the wrong hash. -TEST_NAME="${TEST_NAME_BASE}-new-wrong-hash-client-checkpoint-err" -run_ok "${TEST_NAME}" cp "${ERR_PATH}" err-before-scan -create_test_globalrc '' ' -[authentication] - hashes = sha1 - scan hash = sha1' -run_ok "${TEST_NAME}" cylc scan -fb -n "${SUITE_NAME}" 'localhost' -comm -13 err-before-scan "${ERR_PATH}" >"${TEST_NAME}-diff" -# Wrong hash usage should not be logged as the hash choice may change. -cat "${TEST_NAME}-diff" >/dev/tty -diff "${TEST_NAME}-diff" - /dev/tty -cmp_ok "${TEST_NAME}-diff" '/dev/null' \ - | sed -e 's/.*@localhost://') - -# Connect using SHA256 hash. -create_test_globalrc '' ' -[authentication] - hashes = sha256,md5' - -# Connect using SHA256 hash. -TEST_NAME="${TEST_NAME_BASE}-new-scan-md5-sha256" -run_ok "${TEST_NAME}" cylc trigger "${SUITE_NAME}" bar 1 -grep_ok "INFO - \[client-command\] trigger_task" "$(cylc cat-log -l $SUITE_NAME)" - -# Shutdown using SHA256. -TEST_NAME="${TEST_NAME_BASE}-stop-md5-sha256" -run_ok "${TEST_NAME}" cylc stop --max-polls=10 --interval=1 "${SUITE_NAME}" - -# Double check shutdown. -TEST_NAME="${TEST_NAME_BASE}-stop-md5" -sleep 2 -run_fail "${TEST_NAME}" cylc stop --max-polls=10 --interval=1 "${SUITE_NAME}" - -# Purge. -purge_suite "${SUITE_NAME}" -exit diff --git a/tests/authentication/09-remote-suite-same-name.t b/tests/authentication/09-remote-suite-same-name.t index 42e0855a781..f5bf2c3738f 100755 --- a/tests/authentication/09-remote-suite-same-name.t +++ b/tests/authentication/09-remote-suite-same-name.t @@ -15,9 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . #------------------------------------------------------------------------------- -# Test Pyro communication from a remote host (non-shared file system) when it -# has a suite with the same name registered, but not running. (Obviously, it -# will be very confused if it is running under its ~/cylc-run/SUITE as well.) +# Test communication from a remote host (non-shared file system) when it has +# a suite with the same name registered, but not running. (Obviously, it will +# be very confused if it is running under its ~/cylc-run/SUITE as well.) CYLC_TEST_IS_GENERIC=false . "$(dirname "$0")/test_header" diff --git a/tests/broadcast/09-remote/suite.rc b/tests/broadcast/09-remote/suite.rc index fa64eb84b73..7578bf53959 100644 --- a/tests/broadcast/09-remote/suite.rc +++ b/tests/broadcast/09-remote/suite.rc @@ -13,7 +13,7 @@ graph="""t1 => t2""" [runtime] [[t1]] - script = cylc broadcast "${CYLC_SUITE_NAME}" -n t2 -s 'script=true' + script = cylc broadcast -v -v --debug "${CYLC_SUITE_NAME}" -n t2 -s 'script=true' [[[remote]]] host = {{environ["CYLC_TEST_HOST"]}} [[t2]] diff --git a/tests/cylc-scan/01-hosts.t b/tests/cylc-scan/01-hosts.t index 231050df402..64957cdaa52 100644 --- a/tests/cylc-scan/01-hosts.t +++ b/tests/cylc-scan/01-hosts.t @@ -46,6 +46,8 @@ for HOST in $(tr -d ',' <<<"${HOSTS}"); do mkdir -p "${HOME}/.cylc/passphrases/${USER}@${HOST}/${UUID}-${HOST}" ${SCP} -p "${HOST}:${HOST_WORK_DIR}/passphrase" \ "${HOME}/.cylc/passphrases/${USER}@${HOST}/${UUID}-${HOST}/" + ${SCP} -p "${HOST}:${HOST_WORK_DIR}/ssl.*" \ + "${HOME}/.cylc/passphrases/${USER}@${HOST}/${UUID}-${HOST}/" cylc run "--host=${HOST}" "${UUID}-${HOST}" 1>/dev/null 2>&1 poll '!' ${SSH} -n "${HOST}" "test -e '.cylc/ports/${UUID}-${HOST}'" fi diff --git a/tests/lib/bash/test_header b/tests/lib/bash/test_header index 7a34d010d1f..b7b4151749e 100644 --- a/tests/lib/bash/test_header +++ b/tests/lib/bash/test_header @@ -198,7 +198,7 @@ function cmp_ok() { local FILE_CONTROL=${2:--} local TEST_NAME=$(basename $FILE_TEST)-cmp-ok local DIFF_CMD=${CYLC_TEST_DIFF_CMD:-'diff -u'} - if ${DIFF_CMD} "$FILE_TEST" "$FILE_CONTROL" >"${TEST_NAME}.stderr" 2>&1;then + if ${DIFF_CMD} "$FILE_CONTROL" "$FILE_TEST" >"${TEST_NAME}.stderr" 2>&1;then ok $TEST_NAME return else diff --git a/tests/restart/broadcast/suite.rc b/tests/restart/broadcast/suite.rc index 3956496d837..385f093d36d 100644 --- a/tests/restart/broadcast/suite.rc +++ b/tests/restart/broadcast/suite.rc @@ -4,7 +4,7 @@ UTC mode = True [[events]] timeout handler = shutdown_this_suite_hook - timeout = PT3M + timeout = PT1M [scheduling] initial cycle time = 20130923T00 final cycle time = 20130923T00 diff --git a/tests/startup/02-state-summary.t b/tests/startup/02-state-summary.t index 48e2a1158ab..042958aa5a3 100644 --- a/tests/startup/02-state-summary.t +++ b/tests/startup/02-state-summary.t @@ -34,7 +34,7 @@ sleep 5 cylc dump $SUITE_NAME > dump.out TEST_NAME=$TEST_NAME_BASE-grep # State summary should not just say "Initializing..." -grep_ok "state totals = {u'failed': 1, u'succeeded': 1}" dump.out +grep_ok "state totals = {'failed': 1, 'succeeded': 1}" dump.out #------------------------------------------------------------------------------- cylc stop --max-polls=10 --interval=2 $SUITE_NAME purge_suite $SUITE_NAME diff --git a/tests/validate/50-fail-authentication-hashes.t b/tests/validate/50-fail-authentication-hashes.t deleted file mode 100755 index 0bee0038fd5..00000000000 --- a/tests/validate/50-fail-authentication-hashes.t +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2016 NIWA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -#------------------------------------------------------------------------------- -# Test fail validation of bad vis node attributes. -. $(dirname $0)/test_header -#------------------------------------------------------------------------------- -set_test_number 4 -#------------------------------------------------------------------------------- -create_test_globalrc '' ' -[authentication] - hashes = sha1048576' - -TEST_NAME="$TEST_NAME_BASE-hashes-get-global-config" -run_fail $TEST_NAME cylc get-global-config -grep_ok "\[authentication\]hashes = sha1048576" "$TEST_NAME.stderr" -#------------------------------------------------------------------------------- -create_test_globalrc '' ' -[authentication] - scan hash = sha1048576' -TEST_NAME="$TEST_NAME_BASE-scan-hash-get-global-config" -run_fail $TEST_NAME cylc get-global-config -grep_ok "\[authentication\]scan hash = sha1048576" "$TEST_NAME.stderr" -exit