def main(): """Implement the "rose config-dump" command.""" opt_parser = RoseOptionParser() opt_parser.add_my_options("conf_dir", "files", "no_pretty_mode") opts, args = opt_parser.parse_args() verbosity = opts.verbosity - opts.quietness report = Reporter(verbosity) fs_util = FileSystemUtil(report) if opts.conf_dir: fs_util.chdir(opts.conf_dir) file_names = [] if opts.files: file_names = opts.files else: for dirpath, dirnames, filenames in os.walk("."): for filename in fnmatch.filter(filenames, "rose-*.conf"): p = os.path.join(dirpath, filename)[2:] # remove leading ./ file_names.append(p) for file_name in file_names: t = NamedTemporaryFile() node = ConfigLoader()(file_name) if (not opts.no_pretty_mode and os.path.basename(file_name) != META_CONFIG_NAME): pretty_format_config(node, ignore_error=True) ConfigDumper()(node, t) t.seek(0) if not filecmp.cmp(t.name, file_name, shallow=False): report(ConfigDumpEvent(file_name)) ConfigDumper()(node, file_name)
def restart(self, suite_name=None, host=None, gcontrol_mode=None, args=None): """Restart a "cylc" suite.""" # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() if not suite_name: suite_name = get_suite_name(self.event_handler) suite_dir = self.suite_engine_proc.get_suite_dir(suite_name) if not os.path.exists(suite_dir): raise SuiteNotFoundError(suite_dir) # Ensure suite is not running self.suite_engine_proc.check_suite_not_running(suite_name) # Determine suite host to restart suite if host: hosts = [host] else: hosts = [] val = ResourceLocator.default().get_conf().get_value( ["rose-suite-run", "hosts"], "localhost") for known_host in val.split(): if known_host not in hosts: hosts.append(known_host) if hosts == ["localhost"]: host = hosts[0] else: host = self.host_selector(hosts)[0][0] self.handle_event(SuiteHostSelectEvent(suite_name, "restart", host)) # Suite host environment run_conf_file_name = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") try: run_conf = ConfigLoader().load(run_conf_file_name) except (ConfigSyntaxError, IOError): environ = None else: run_conf_tree = ConfigTree() run_conf_tree.node = run_conf environ = self.config_pm(run_conf_tree, "env") # Restart the suite self.suite_engine_proc.run(suite_name, host, environ, "restart", args) # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return
def _get_access_info(self, repos, path_head, txn=None): """Return the owner and the access list of a suite (path_head).""" opt_txn = [] if txn is not None: opt_txn = ["-t", txn] t_handle = tempfile.TemporaryFile() path = path_head + self.TRUNK_INFO_FILE t_handle.write(self._svnlook("cat", repos, path, *opt_txn)) t_handle.seek(0) node = ConfigLoader()(t_handle) t_handle.close() owner = node.get_value(["owner"]) access_list = node.get_value(["access-list"], "").split() access_list.sort() return owner, access_list
def _get_info(self, repos, path_head, txn=None): """Return a ConfigNode for the "rose-suite.info" of a suite. The suite is located under path_head. """ opt_txn = [] if txn is not None: opt_txn = ["-t", txn] t_handle = tempfile.TemporaryFile() path = path_head + self.TRUNK_INFO_FILE t_handle.write(self._svnlook("cat", repos, path, *opt_txn)) t_handle.seek(0) info_node = ConfigLoader()(t_handle) t_handle.close() return info_node
def get_conf(self): """Return the site/user configuration root node.""" if self.conf is None: paths = [self.SITE_CONF_PATH, self.USER_CONF_PATH] if "ROSE_CONF_PATH" in os.environ: paths_str = os.getenv("ROSE_CONF_PATH").strip() if paths_str: paths = paths_str.split(os.pathsep) else: paths = [] self.conf = ConfigNode() config_loader = ConfigLoader() for path in paths: name = os.path.join(path, self.ROSE_CONF) if os.path.isfile(name) and os.access(name, os.R_OK): config_loader.load_with_opts(name, self.conf) return self.conf
def run_impl(self, opts, args, uuid, work_files): # Log file, temporary if hasattr(self.event_handler, "contexts"): t_file = TemporaryFile() log_context = ReporterContext(None, self.event_handler.VV, t_file) self.event_handler.contexts[uuid] = log_context # Check suite engine specific compatibility self.suite_engine_proc.check_global_conf_compat() # Suite name from the current working directory if opts.conf_dir: self.fs_util.chdir(opts.conf_dir) opts.conf_dir = os.getcwd() if opts.defines_suite: suite_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF if not opts.defines: opts.defines = [] for define in opts.defines_suite: opts.defines.append("[" + suite_section + "]" + define) # --remote=KEY=VALUE,... 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]) suite_name = opts.name if not opts.name: suite_name = os.path.basename(os.getcwd()) # Automatic Rose constants # ROSE_ORIG_HOST: originating host # ROSE_VERSION: Rose version (not retained in run_mode=="reload") # Suite engine version jinja2_section = "jinja2:" + self.suite_engine_proc.SUITE_CONF my_rose_version = ResourceLocator.default().get_version() suite_engine_key = self.suite_engine_proc.get_version_env_name() if opts.run_mode in ["reload", "restart"]: prev_config_path = self.suite_engine_proc.get_suite_dir( suite_name, "log", "rose-suite-run.conf") prev_config = ConfigLoader()(prev_config_path) suite_engine_version = prev_config.get_value( ["env", suite_engine_key]) else: suite_engine_version = self.suite_engine_proc.get_version() auto_items = { "ROSE_ORIG_HOST": self.host_selector.get_local_host(), "ROSE_VERSION": ResourceLocator.default().get_version(), suite_engine_key: suite_engine_version } for key, val in auto_items.items(): requested_value = conf_tree.node.get_value(["env", key]) if requested_value: if key == "ROSE_VERSION" and val != requested_value: exc = VersionMismatchError(requested_value, val) raise ConfigValueError(["env", key], requested_value, exc) val = requested_value else: conf_tree.node.set(["env", key], val, state=conf_tree.node.STATE_NORMAL) conf_tree.node.set([jinja2_section, key], '"' + val + '"') # See if suite is running or not hosts = [] if opts.host: hosts.append(opts.host) if opts.run_mode == "reload": suite_run_hosts = self.suite_engine_proc.get_suite_run_hosts( None, suite_name, hosts) if not suite_run_hosts: raise SuiteNotRunningError(suite_name) hosts = suite_run_hosts else: self.suite_engine_proc.check_suite_not_running(suite_name, hosts) # Install the suite to its run location suite_dir_rel = self._suite_dir_rel(suite_name) suite_dir = os.path.join(os.path.expanduser("~"), suite_dir_rel) suite_conf_dir = os.getcwd() locs_conf = ConfigNode() if opts.new_mode: if os.getcwd() == suite_dir: raise NewModeError("PWD", os.getcwd()) elif opts.run_mode in ["reload", "restart"]: raise NewModeError("--run", opts.run_mode) self.suite_run_cleaner.clean(suite_name) if os.getcwd() != suite_dir: if opts.run_mode == "run": self._run_init_dir(opts, suite_name, conf_tree, locs_conf=locs_conf) os.chdir(suite_dir) # Housekeep log files if not opts.install_only_mode and not opts.local_install_only_mode: self._run_init_dir_log(opts) self.fs_util.makedirs("log/suite") # Rose configuration and version logs self.fs_util.makedirs("log/rose-conf") run_mode = opts.run_mode if run_mode not in ["reload", "restart", "run"]: run_mode = "run" mode = run_mode if opts.install_only_mode: mode = "install-only" elif opts.local_install_only_mode: mode = "local-install-only" prefix = "rose-conf/%s-%s" % (strftime("%Y%m%dT%H%M%S"), mode) # Dump the actual configuration as rose-suite-run.conf ConfigDumper()(conf_tree.node, "log/" + prefix + ".conf") # Install version information file write_source_vc_info(suite_conf_dir, "log/" + prefix + ".version", self.popen) # If run through rose-stem, install version information files for # each source tree if they're a working copy if hasattr(opts, 'source') and hasattr(opts, 'project'): for i, url in enumerate(opts.source): if os.path.isdir(url): write_source_vc_info( url, "log/" + opts.project[i] + "-" + str(i) + ".version", self.popen) for ext in [".conf", ".version"]: self.fs_util.symlink(prefix + ext, "log/rose-suite-run" + ext) # Move temporary log to permanent log if hasattr(self.event_handler, "contexts"): log_file_path = os.path.abspath( os.path.join("log", "rose-suite-run.log")) log_file = open(log_file_path, "ab") temp_log_file = self.event_handler.contexts[uuid].handle temp_log_file.seek(0) log_file.write(temp_log_file.read()) self.event_handler.contexts[uuid].handle = log_file temp_log_file.close() # Install share/work directories (local) for name in ["share", "share/cycle", "work"]: self._run_init_dir_work(opts, suite_name, name, conf_tree, locs_conf=locs_conf) # Process Environment Variables environ = self.config_pm(conf_tree, "env") # Process Files cwd = os.getcwd() for rel_path, conf_dir in conf_tree.files.items(): if (conf_dir == cwd or any([ fnmatchcase(os.sep + rel_path, exclude) for exclude in self.SYNC_EXCLUDES ]) or conf_tree.node.get(["jinja2:" + rel_path]) is not None): continue # No sub-directories, very slow otherwise if os.sep in rel_path: rel_path = rel_path.split(os.sep, 1)[0] target_key = self.config_pm.get_handler("file").PREFIX + rel_path target_node = conf_tree.node.get([target_key]) if target_node is None: conf_tree.node.set([target_key]) target_node = conf_tree.node.get([target_key]) elif target_node.is_ignored(): continue source_node = target_node.get("source") if source_node is None: target_node.set(["source"], os.path.join(conf_dir, rel_path)) elif source_node.is_ignored(): continue self.config_pm(conf_tree, "file", no_overwrite_mode=opts.no_overwrite_mode) # Process Jinja2 configuration self.config_pm(conf_tree, "jinja2") # Ask suite engine to parse suite configuration # and determine if it is up to date (unchanged) suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf( suite_name, opts.run_mode, opts.strict_mode, opts.debug_mode) if opts.local_install_only_mode: return # Install suite files to each remote [user@]host for name in ["", "log/", "share/", "share/cycle/", "work/"]: uuid_file = os.path.abspath(name + uuid) open(uuid_file, "w").close() work_files.append(uuid_file) # Install items to user@host conf = ResourceLocator.default().get_conf() 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] # Remote shell command = self.popen.get_cmd("ssh", "-n", auth) # Provide ROSE_VERSION and CYLC_VERSION in the environment shcommand = "env ROSE_VERSION=%s %s=%s" % ( my_rose_version, suite_engine_key, suite_engine_version) # Use login shell? no_login_shell = self._run_conf("remote-no-login-shell", host=host, conf_tree=conf_tree) if not no_login_shell or no_login_shell.lower() != "true": shcommand += r""" bash -l -c '"$0" "$@"'""" # Path to "rose" command, if applicable rose_bin = self._run_conf("remote-rose-bin", host=host, conf_tree=conf_tree, default="rose") # Build remote "rose suite-run" command shcommand += " %s suite-run -vv -n %s" % (rose_bin, suite_name) for key in ["new", "debug", "install-only"]: attr = key.replace("-", "_") + "_mode" if getattr(opts, attr, None) is not None: shcommand += " --%s" % key if opts.log_keep: shcommand += " --log-keep=%s" % opts.log_keep if opts.log_name: shcommand += " --log-name=%s" % opts.log_name if not opts.log_archive_mode: shcommand += " --no-log-archive" shcommand += " --run=%s" % opts.run_mode # Build --remote= option shcommand += " --remote=uuid=%s" % uuid host_confs = [ "root-dir", "root-dir{share}", "root-dir{share/cycle}", "root-dir{work}" ] locs_conf.set([auth]) for key in host_confs: value = self._run_conf(key, host=host, conf_tree=conf_tree) if value is not None: val = self.popen.list_to_shell_str([str(value)]) shcommand += ",%s=%s" % (key, val) locs_conf.set([auth, key], value) command.append(shcommand) proc = self.popen.run_bg(*command) proc_queue.append([proc, command, "ssh", auth]) while proc_queue: sleep(self.SLEEP_PIPE) proc, command, command_name, auth = proc_queue.pop(0) if proc.poll() is None: # put it back in proc_queue proc_queue.append([proc, command, command_name, auth]) continue ret_code = proc.wait() out, err = proc.communicate() if ret_code: raise RosePopenError(command, ret_code, out, err) if command_name == "rsync": self.handle_event(out, level=Event.VV) continue else: self.handle_event(out, level=Event.VV, prefix="[%s] " % auth) for line in out.split("\n"): if "/" + uuid == line.strip(): locs_conf.unset([auth]) break else: filters = {"excludes": [], "includes": []} for name in ["", "log/", "share/", "share/cycle/", "work/"]: filters["excludes"].append(name + uuid) target = auth + ":" + suite_dir_rel cmd = self._get_cmd_rsync(target, **filters) proc_queue.append( [self.popen.run_bg(*cmd), cmd, "rsync", auth]) # Install ends ConfigDumper()(locs_conf, os.path.join("log", "rose-suite-run.locs")) if opts.install_only_mode: return elif opts.run_mode == "reload" and suite_conf_unchanged: conf_name = self.suite_engine_proc.SUITE_CONF self.handle_event(SkipReloadEvent(suite_name, conf_name)) return # Start the suite self.fs_util.chdir("log") ret = 0 # FIXME: should sync files to suite host? if opts.run_mode != "reload": if opts.host: hosts = [opts.host] else: names = shlex.split( conf.get_value(["rose-suite-run", "hosts"], "")) if names: hosts += self.host_selector.expand(names)[0] if (hosts and len(hosts) == 1 and self.host_selector.is_local_host(hosts[0])): host = "localhost" elif hosts: host = self.host_selector(hosts)[0][0] else: host = "localhost" self.handle_event(SuiteHostSelectEvent(suite_name, run_mode, host)) # FIXME: values in environ were expanded in the localhost self.suite_engine_proc.run(suite_name, host, environ, opts.run_mode, args) open("rose-suite-run.host", "w").write(host + "\n") # Disconnect log file handle, so monitoring tool command will no longer # be associated with the log file. self.event_handler.contexts[uuid].handle.close() self.event_handler.contexts.pop(uuid) # Launch the monitoring tool # Note: maybe use os.ttyname(sys.stdout.fileno())? if os.getenv("DISPLAY") and host and opts.gcontrol_mode: self.suite_engine_proc.gcontrol(suite_name, host) return ret
def main(): """Implement the "rose config" command.""" opt_parser = RoseOptionParser() opt_parser.add_my_options("default", "env_var_process_mode", "files", "keys", "meta", "meta_key", "no_ignore", "no_opts", "print_conf_mode") opts, args = opt_parser.parse_args() report = Reporter(opts.verbosity - opts.quietness) rose.macro.add_meta_paths() if opts.meta_key: opts.meta = True if opts.files and opts.meta_key: report(Exception("Cannot specify both a file and meta key.")) sys.exit(1) config_loader = ConfigLoader() sources = [] if opts.files: root_node = ConfigNode() for fname in opts.files: if fname == "-": sources.append(sys.stdin) else: if opts.meta: try: root_node = config_loader.load(fname) except ConfigSyntaxError as exc: report(exc) sys.exit(1) rel_path = os.sep.join(fname.split(os.sep)[:-1]) fpath = get_meta_path(root_node, rel_path) if fpath is None: report(MetadataNotFoundEvent(fname)) else: sources.append(fpath) else: sources.append(fname) elif opts.meta: root_node = ConfigNode() if opts.meta_key: root_node.set(["meta"], opts.meta_key) else: fname = os.path.join(os.getcwd(), rose.SUB_CONFIG_NAME) try: root_node = config_loader.load(fname) except ConfigSyntaxError as exc: report(exc) sys.exit(1) fpath = get_meta_path(root_node, meta_key=opts.meta_key) root_node.unset(["meta"]) if fpath is None: report(Exception("Metadata not found")) sys.exit(1) else: sources.append(fpath) else: root_node = ResourceLocator.default().get_conf() for source in sources: try: if opts.meta or opts.no_opts: config_loader.load(source, root_node) else: config_loader.load_with_opts(source, root_node) except (ConfigSyntaxError, IOError) as exc: report(exc) sys.exit(1) if source is sys.stdin: source.close() if opts.quietness: sys.exit(root_node.get(args, opts.no_ignore) is None) if opts.keys_mode: try: keys = root_node.get(args, opts.no_ignore).value.keys() except AttributeError: sys.exit(1) keys.sort() for key in keys: print key sys.exit() conf_dump = ConfigDumper() if len(args) == 0: conf_dump(root_node, concat_mode=opts.print_conf_mode) sys.exit() node = root_node.get(args, opts.no_ignore) if node is not None and isinstance(node.value, dict): if opts.print_conf_mode: conf_dump(ConfigNode().set(args, node.value), concat_mode=True) sys.exit() keys = node.value.keys() keys.sort() for key in keys: node_of_key = node.get([key], opts.no_ignore) if node_of_key: value = node_of_key.value state = node_of_key.state string = "%s%s=%s" % (state, key, value) lines = string.splitlines() print lines[0] i_equal = len(state + key) + 1 for line in lines[1:]: print " " * i_equal + line sys.exit() if node is None: if opts.default is None: sys.exit(1) value = opts.default elif opts.env_var_process_mode: value = env_var_process(node.value) else: value = node.value if opts.print_conf_mode: conf_dump(ConfigNode().set(args, value), concat_mode=True) else: print value sys.exit()
def __init__(self, *args, **kwargs): self.node_loader = ConfigLoader(*args, **kwargs)
def _clean(self, suite_name, only_items=None): """Perform the cleaning operations.""" engine = self.suite_engine_proc suite_dir_rel = engine.get_suite_dir_rel(suite_name) locs_file_path = engine.get_suite_dir(suite_name, "log", "rose-suite-run.locs") locs_conf = ConfigNode().set(["localhost"], {}) try: ConfigLoader().load(locs_file_path, locs_conf) except IOError: pass items = self.CLEANABLE_PATHS + [""] if only_items: items = only_items items.sort() uuid_str = str(uuid4()) for auth, node in sorted(locs_conf.value.items(), self._auth_node_cmp): locs = [] roots = set([""]) for item in items: if item: locs.append(os.path.join(suite_dir_rel, item)) else: locs.append(suite_dir_rel) if item and os.path.normpath(item) in self.CLEANABLE_PATHS: item_root = node.get_value(["root-dir{" + item + "}"]) if item_root is None: # backward compat item_root = node.get_value(["root-dir-" + item]) elif item == "": item_root = node.get_value(["root-dir"]) else: continue if item_root: loc_rel = suite_dir_rel if item: loc_rel = os.path.join(suite_dir_rel, item) locs.append(os.path.join(item_root, loc_rel)) roots.add(item_root) locs.reverse() # Invoke bash as a login shell. The root location of a path may be # in $DIR syntax, which can only be expanded correctly in a login # shell. However, profile scripts invoked on login shell may print # lots of junks. Hence we use a UUID here as a delimiter. Only # output after the UUID lines are desirable lines. command = ["bash", "-l", "-O", "extglob", "-c"] sh_command = "cd; echo '%s'" % (uuid_str, ) if not self.host_selector.is_local_host(auth): command = engine.popen.get_cmd("ssh", auth) + command sh_command += "; ls -d -r %(locs)s; rm -fr %(locs)s" % { "locs": engine.popen.list_to_shell_str(locs) } if not only_items: # Clean empty directories # Change directory to root level to avoid cleaning them as # well For cylc suites, e.g. it can clean up to an empty # "cylc-run/" directory. for root in roots: names = [] # Reverse sort to ensure that e.g. "share/cycle/" is # cleaned before "share/" for name in sorted(self.CLEANABLE_PATHS, reverse=True): names.append(os.path.join(suite_dir_rel, name)) if os.sep in suite_dir_rel: names.append(os.path.dirname(suite_dir_rel)) sh_command += ( "; " + "(cd %(root)s; " + "rmdir -p %(names)s 2>/dev/null || true)") % { "root": root, "names": engine.popen.list_to_shell_str(names), } if self.host_selector.is_local_host(auth): command.append(sh_command) else: command.append(quote(sh_command)) is_after_uuid_str = False for line in engine.popen(*command)[0].splitlines(): if is_after_uuid_str: engine.handle_event( FileSystemEvent(FileSystemEvent.DELETE, auth + ":" + line.strip())) elif line == uuid_str: is_after_uuid_str = True
def _clean(self, suite_name, only_items=None): """Perform the cleaning operations.""" engine = self.suite_engine_proc suite_dir_rel = engine.get_suite_dir_rel(suite_name) locs_file_path = engine.get_suite_dir(suite_name, "log", "rose-suite-run.locs") locs_conf = ConfigNode().set(["localhost"], {}) try: ConfigLoader().load(locs_file_path, locs_conf) except IOError: pass items = self.CLEANABLE_ROOTS + [""] if only_items: items = only_items items.sort() uuid_str = str(uuid4()) for auth, node in sorted(locs_conf.value.items(), self._auth_node_cmp): locs = [] for item in items: if item: locs.append(os.path.join(suite_dir_rel, item)) else: locs.append(suite_dir_rel) if item and os.path.normpath(item) in self.CLEANABLE_ROOTS: conf_key = "root-dir-" + item elif item == "": conf_key = "root-dir" else: continue item_root = node.get_value([conf_key]) if item_root: loc_rel = suite_dir_rel if item: loc_rel = os.path.join(suite_dir_rel, item) locs.append(os.path.join(item_root, loc_rel)) if auth == "localhost": for loc in locs: loc = os.path.abspath(env_var_process(loc)) for name in sorted(glob(loc)): engine.fs_util.delete(name) else: # Invoke bash as a login shell. The root location of a path may # be in $DIR syntax, which can only be expanded correctly in a # login shell. However, profile scripts invoked on login to the # remote host may print lots of junks. Hence we use a UUID here # as a delimiter. Only output after the UUID lines are # desirable lines. command = engine.popen.get_cmd("ssh", auth, "bash", "-l", "-c") command += [ "'echo %(uuid)s; ls -d %(locs)s|sort; rm -rf %(locs)s'" % { "locs": engine.popen.list_to_shell_str(locs), "uuid": uuid_str, }, ] is_after_uuid_str = False for line in engine.popen(*command)[0].splitlines(): if is_after_uuid_str: engine.handle_event( FileSystemEvent(FileSystemEvent.DELETE, auth + ":" + line.strip())) elif line == uuid_str: is_after_uuid_str = True
def _clean(self, suite_name, only_items=None): """Perform the cleaning operations.""" engine = self.suite_engine_proc suite_dir_rel = engine.get_suite_dir_rel(suite_name) locs_file_path = engine.get_suite_dir(suite_name, "log", "rose-suite-run.locs") locs_conf = ConfigNode().set(["localhost"], {}) try: ConfigLoader().load(locs_file_path, locs_conf) except IOError: pass items = self.CLEANABLE_PATHS + [""] if only_items: items = only_items items.sort() uuid_str = str(uuid4()) for auth, node in sorted(locs_conf.value.items(), self._auth_node_cmp): locs = [] roots = set([""]) for item in items: if item: locs.append(os.path.join(suite_dir_rel, item)) else: locs.append(suite_dir_rel) if item and os.path.normpath(item) in self.CLEANABLE_PATHS: item_root = node.get_value(["root-dir{" + item + "}"]) if item_root is None: # backward compat item_root = node.get_value(["root-dir-" + item]) elif item == "": item_root = node.get_value(["root-dir"]) else: continue if item_root: loc_rel = suite_dir_rel if item: loc_rel = os.path.join(suite_dir_rel, item) locs.append(os.path.join(item_root, loc_rel)) roots.add(item_root) if self.host_selector.is_local_host(auth): # Clean relevant items for loc in locs: loc = os.path.abspath(env_var_process(loc)) for name in sorted(glob(loc)): engine.fs_util.delete(name) # Clean empty directories # Change directory to root level to avoid cleaning them as well # For cylc suites, e.g. it can clean up to an empty "cylc-run/" # directory. for root in sorted(roots): cwd = os.getcwd() if root: try: os.chdir(env_var_process(root)) except OSError: continue # Reverse sort to ensure that e.g. "share/cycle/" is # cleaned before "share/" for name in sorted(self.CLEANABLE_PATHS, reverse=True): try: os.removedirs(os.path.join(suite_dir_rel, name)) except OSError: pass try: os.removedirs(suite_dir_rel) except OSError: pass if root: os.chdir(cwd) else: # Invoke bash as a login shell. The root location of a path may # be in $DIR syntax, which can only be expanded correctly in a # login shell. However, profile scripts invoked on login to the # remote host may print lots of junks. Hence we use a UUID here # as a delimiter. Only output after the UUID lines are # desirable lines. command = engine.popen.get_cmd("ssh", auth, "bash", "-l", "-c") sh_command = ( "echo %(uuid)s; ls -d %(locs)s|sort; rm -fr %(locs)s") % { "locs": engine.popen.list_to_shell_str(locs), "uuid": uuid_str, } # Clean empty directories # Change directory to root level to avoid cleaning them as well # For cylc suites, e.g. it can clean up to an empty "cylc-run/" # directory. for root in roots: names = [] # Reverse sort to ensure that e.g. "share/cycle/" is # cleaned before "share/" for name in sorted(self.CLEANABLE_PATHS, reverse=True): names.append(os.path.join(suite_dir_rel, name)) sh_command += ( "; " + "(cd %(root)s; rmdir -p %(names)s 2>/dev/null || true)" ) % { "root": root, "names": engine.popen.list_to_shell_str(names), } command.append(quote(sh_command)) is_after_uuid_str = False for line in engine.popen(*command)[0].splitlines(): if is_after_uuid_str: engine.handle_event( FileSystemEvent(FileSystemEvent.DELETE, auth + ":" + line.strip())) elif line == uuid_str: is_after_uuid_str = True