diff --git a/lib/python/rose/opt_parse.py b/lib/python/rose/opt_parse.py index f8028a3f68..8d99ee5f2e 100644 --- a/lib/python/rose/opt_parse.py +++ b/lib/python/rose/opt_parse.py @@ -328,6 +328,16 @@ class RoseOptionParser(OptionParser): {"action": "store", "metavar": "NAME", "help": "Specify the suite name."}], + "run_dir": [ + ["--run-dir"], + {"action": "store", + "metavar": "DIR", + "help": "Suite run directory."}], + "work_dir": [ + ["--work-dir"], + {"action": "store", + "metavar": "DIR", + "help": "Suite work and share directory."}], "new_mode": [ ["--new", "-N"], {"action": "store_true", diff --git a/lib/python/rose/suite_engine_procs/cylc.py b/lib/python/rose/suite_engine_procs/cylc.py index 19501d36b6..a6259f7968 100644 --- a/lib/python/rose/suite_engine_procs/cylc.py +++ b/lib/python/rose/suite_engine_procs/cylc.py @@ -67,6 +67,21 @@ def __init__(self, *args, **kwargs): self.host = None self.user = None + def get_suite_run_work_dir(self, item, suite_name, host, user): + """Get suite run or work (item) directory on given host.""" + try: + out = self.popen( + "cylc", "get-global-config", "-i", + "[hosts][%s]%s directory" % (host, item))[0] + except RosePopenError: + out = self.SUITE_DIR_REL_ROOT + the_d = out.splitlines()[0] + # TODO - DOCUMENT ONLY '$USER' is replaced in 'run directory' + # (env vars would have to be evaluated on the remote host). + # (can be done for rose-suite-run remote, but not rsync!). + the_d = the_d.replace('$USER', user) + return "%s/%s" % (the_d, suite_name) + def check_global_conf_compat(self): """Raise exception on incompatible Cylc global configuration.""" expected = os.path.join("~", self.SUITE_DIR_REL_ROOT) diff --git a/lib/python/rose/suite_run.py b/lib/python/rose/suite_run.py index eb0b78f9a4..67eb44554a 100644 --- a/lib/python/rose/suite_run.py +++ b/lib/python/rose/suite_run.py @@ -84,6 +84,8 @@ class SuiteRunner(Runner): "log_keep", "log_name", "name", + "run_dir", + "work_dir", "new_mode", "no_overwrite_mode", "opt_conf_keys", @@ -113,6 +115,16 @@ def __init__(self, *args, **kwargs): host_selector=self.host_selector, suite_engine_proc=self.suite_engine_proc) + @staticmethod + def _auth_split(auth): + """Return (user, host) from "[user@]host", defaulting to $USER.""" + host = auth + if "@" in auth: + user, host = auth.split("@", 1) + else: + user = os.environ['USER'] + return (user, host) + def run_impl(self, opts, args, uuid, work_files): # Log file, temporary if hasattr(self.event_handler, "contexts"): @@ -132,7 +144,6 @@ def run_impl(self, opts, args, uuid, work_files): if opts.remote: # opts.name always set for remote. return self._run_remote(opts, opts.name) - conf_tree = self.config_load(opts) self.fs_util.chdir(conf_tree.conf_dirs[0]) @@ -216,6 +227,7 @@ def run_impl(self, opts, args, uuid, work_files): suite_dir = os.path.join(temp_dir, suite_dir_rel) os.makedirs(suite_dir, 0o0700) else: + # TODO - LOCALHOST ONLY (leave as ~?) suite_dir = os.path.join( os.path.expanduser("~"), suite_dir_rel) @@ -356,9 +368,7 @@ def run_impl(self, opts, args, uuid, work_files): auths = self.suite_engine_proc.get_tasks_auths(suite_name) proc_queue = [] # [[proc, command, "ssh"|"rsync", auth], ...] for auth in sorted(auths): - host = auth - if "@" in auth: - host = auth.split("@", 1)[1] + user, host = self._auth_split(auth) # Remote shell command = self.popen.get_cmd("ssh", "-n", auth) # Provide ROSE_VERSION and CYLC_VERSION in the environment @@ -375,6 +385,11 @@ def run_impl(self, opts, args, uuid, work_files): default="rose") # Build remote "rose suite-run" command shcommand += " %s suite-run -vv -n %s" % (rose_bin, suite_name) + shcommand += " --run-dir=%s --work-dir=%s" % ( + self.suite_engine_proc.get_suite_run_work_dir( + "run", suite_name, host, user), + self.suite_engine_proc.get_suite_run_work_dir( + "work", suite_name, host, user)) for key in ["new", "debug", "install-only"]: attr = key.replace("-", "_") + "_mode" if getattr(opts, attr, None) is not None: @@ -429,7 +444,10 @@ def run_impl(self, opts, args, uuid, work_files): filters = {"excludes": [], "includes": []} for name in ["", "log/", "share/", "share/cycle/", "work/"]: filters["excludes"].append(name + uuid) - target = auth + ":" + suite_dir_rel + user, host = self._auth_split(auth) + target = (auth + ":" + + self.suite_engine_proc.get_suite_run_work_dir( + "run", suite_name, host, user)) cmd = self._get_cmd_rsync(target, **filters) proc_queue.append( [self.popen.run_bg(*cmd), cmd, "rsync", auth]) @@ -496,18 +514,25 @@ def _run_conf( def _run_init_dir(self, opts, suite_name, conf_tree=None, r_opts=None, locs_conf=None): """Create the suite's directory.""" - suite_dir_rel = self._suite_dir_rel(suite_name) - home = os.path.expanduser("~") + if opts.run_dir: + suite_dir_home = opts.run_dir + else: + suite_dir_rel = self._suite_dir_rel(suite_name) + home = os.path.expanduser("~") + suite_dir_home = os.path.join(home, suite_dir_rel) suite_dir_root = self._run_conf("root-dir", conf_tree=conf_tree, r_opts=r_opts) if suite_dir_root: if locs_conf is not None: locs_conf.set(["localhost", "root-dir"], suite_dir_root) suite_dir_root = env_var_process(suite_dir_root) - suite_dir_home = os.path.join(home, suite_dir_rel) if (suite_dir_root and - os.path.realpath(home) != os.path.realpath(suite_dir_root)): - suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel) + os.path.realpath(suite_dir_home) != + os.path.realpath(suite_dir_root)): + if opts.run_dir: + suite_dir_real = os.path.join(suite_dir_root, suite_name) + else: + suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel) self.fs_util.makedirs(suite_dir_real) self.fs_util.symlink(suite_dir_real, suite_dir_home, opts.no_overwrite_mode) @@ -595,7 +620,11 @@ def _run_init_dir_work(self, opts, suite_name, name, conf_tree=None, if locs_conf is not None: locs_conf.set(["localhost", key], item_root) item_root = env_var_process(item_root) - suite_dir_rel = self._suite_dir_rel(suite_name) + if (opts.work_dir and name == "work" or + opts.run_dir and name == "share"): + suite_dir_rel = suite_name + else: + suite_dir_rel = self._suite_dir_rel(suite_name) if os.path.isabs(item_root): item_path_source = os.path.join(item_root, suite_dir_rel, name) else: @@ -637,13 +666,17 @@ def _run_remote(self, opts, suite_name): self.fs_util.delete(suite_dir_rel) if opts.run_mode == "run" or not os.path.exists(suite_dir_rel): self._run_init_dir(opts, suite_name, r_opts=r_opts) - os.chdir(suite_dir_rel) + # make work and share dirs relative to work_dir + self.fs_util.makedirs(opts.work_dir) + os.chdir(opts.work_dir) for name in ["share", "share/cycle", "work"]: uuid_file = os.path.join(name, r_opts["uuid"]) if os.path.exists(uuid_file): self.handle_event(name + "/" + r_opts["uuid"] + "\n", level=0) else: self._run_init_dir_work(opts, suite_name, name, r_opts=r_opts) + # make log dirs relative to run_dir + os.chdir(opts.run_dir) if not opts.install_only_mode: uuid_file = os.path.join("log", r_opts["uuid"]) if os.path.exists(uuid_file):