class BackendParameters(Interface):
    """Display available backend parameters.
    """

    _params_ = dict(
        backends=Parameter(args=("backends", ),
                           metavar="BACKEND",
                           doc="""Restrict output to this backend.""",
                           constraints=EnsureStr(),
                           nargs="*"))

    @staticmethod
    def __call__(backends=None):
        backends = backends or discover_types()
        for backend, cls in get_resource_classes(backends):
            param_doc = "\n".join([
                "  {}: {}".format(p, pdoc)
                for p, pdoc in sorted(get_resource_backends(cls).items())
            ])
            if param_doc:
                out = "Backend parameters for '{}'\n{}".format(
                    backend, param_doc)
            else:
                out = "No backend parameters for '{}'".format(backend)
            print(out)
Exemple #2
0
class Run(Interface):
    """Run a command on the specified resource.

    Two main options control how the job is executed: the orchestator and the
    submitter. The orchestrator that is selected controls details like how the
    data is made available on the resource and how the results are fetched. The
    submitter controls how the job is submitted on the resource (e.g., as a
    condor job). Use --list to see information on the available orchestrators
    and submitters.

    Unless --follow is specified, the job is started and detached. Use
    `reproman jobs` to list and fetch detached jobs.
    """
    _params_ = dict(
        resref=resref_opt,
        resref_type=resref_type_opt,
        list_=Parameter(
            args=("--list", ),
            dest="list_",
            choices=('submitters', 'orchestrators', 'parameters', ''),
            doc="""Show available submitters, orchestrators, or job parameters.
            If an empty string is given, show all."""),
        submitter=Parameter(
            args=("--submitter", "--sub"),
            metavar="NAME",
            constraints=EnsureChoice(None, *SUBMITTERS),
            doc=(JOB_PARAMETERS["submitter"] +
                 "[CMD:  Use --list to see available submitters CMD]")),
        orchestrator=Parameter(
            args=("--orchestrator", "--orc"),
            metavar="NAME",
            constraints=EnsureChoice(None, *ORCHESTRATORS),
            doc=(JOB_PARAMETERS["orchestrator"] +
                 "[CMD:  Use --list to see available orchestrators CMD]")),
        batch_spec=Parameter(
            args=("--batch-spec", "--bs"),
            dest="batch_spec",
            metavar="PATH",
            doc=(JOB_PARAMETERS["batch_spec"] +
                 " See [CMD: --batch-parameter CMD][PY: `batch_parameters` PY]"
                 " for an alternative method for simple combinations.")),
        batch_parameters=Parameter(
            args=("--batch-parameter", "--bp"),
            dest="batch_parameters",
            action="append",
            metavar="PATH",
            doc=(JOB_PARAMETERS["batch_parameters"] +
                 " See [CMD: --batch-spec CMD][PY: `batch_spec` PY]"
                 " for specifying more complex records." +
                 _more_than_once_doc)),
        job_specs=Parameter(
            args=("--job-spec", "--js"),
            dest="job_specs",
            metavar="PATH",
            action="append",
            doc="""YAML files that define job parameters. Multiple paths can be
            given. If a parameter is defined in multiple specs, the value from
            the last path that defines it is used[CMD: . Use --list to see
            available parameters for the built-in templates CMD].""" +
            _more_than_once_doc),
        job_parameters=Parameter(
            metavar="PARAM",
            dest="job_parameters",
            args=("--job-parameter", "--jp"),
            # TODO: Use nargs=+ like create's --backend-parameters?  I'd rather
            # use 'append' there.
            action="append",
            doc="""A job parameter in the form KEY=VALUE. If the same parameter
            is defined via a job spec, the value given here takes precedence.
            The values are available as fields in the templates used to
            generate both the run script and submission script[CMD: . Use
            --list to see available parameters for the built-in templates
            CMD].""" + _more_than_once_doc),
        inputs=Parameter(
            args=("-i", "--input"),
            dest="inputs",
            metavar="PATH",
            action="append",
            doc="""An input path to the command. How input paths are used
            depends on the orchestrator, but, at the very least, the
            orchestrator should try to make these paths available on the
            resource.""" + _more_than_once_doc),
        outputs=Parameter(
            args=("-o", "--output"),
            dest="outputs",
            metavar="PATH",
            action="append",
            doc="""An output path to the command. How output paths are handled
            depends on the orchestrator.""" + _more_than_once_doc),
        follow=Parameter(
            args=("--follow", ),
            metavar="ACTION",
            const=True,
            nargs="?",
            constraints=EnsureChoice(False, True, "stop", "stop-if-success",
                                     "delete", "delete-if-success"),
            doc="""Continue to follow the submitted command instead of
            submitting it and detaching."""),
        command=Parameter(args=("command", ),
                          nargs=REMAINDER,
                          metavar="COMMAND",
                          doc="command for execution"),
        message=Parameter(args=("-m", "--message"),
                          metavar="MESSAGE",
                          doc=JOB_PARAMETERS["message"]),
    )

    @staticmethod
    def __call__(command=None,
                 message=None,
                 resref=None,
                 resref_type="auto",
                 list_=None,
                 submitter=None,
                 orchestrator=None,
                 batch_spec=None,
                 batch_parameters=None,
                 job_specs=None,
                 job_parameters=None,
                 inputs=None,
                 outputs=None,
                 follow=False):
        if list_ is not None:
            wrapper = textwrap.TextWrapper(initial_indent="    ",
                                           subsequent_indent="    ")

            def get_doc(x):
                doc = x if isinstance(x, str) else x.__doc__
                paragraphs = doc.replace("\n\n", "\0").split("\0")
                # Collapse whitespace.
                paragraphs = (" ".join(p.strip().split()) for p in paragraphs)
                return "\n\n".join(wrapper.fill(p) for p in paragraphs)

            def fmt(d):
                return ["  {}\n{}".format(k, get_doc(v)) for k, v in d.items()]

            # FIXME: We shouldn't bother calling fmt on items that aren't
            # selected by list=X.
            categories = [
                ("submitters", ["Submitters"] + fmt(SUBMITTERS)),
                ("orchestrators", ["Orchestrator"] + fmt(ORCHESTRATORS)),
                ("parameters", ["Job parameters"] + fmt(JOB_PARAMETERS)),
            ]
            items = []
            for c, lines in categories:
                if not list_ or c == list_:
                    items.extend(lines)
                    items.append("")
            print("\n".join(items))
            return

        # TODO: globbing for inputs/outputs and command string formatting is
        # only supported for DataLad-based orchestrators.

        # CLI things that can also be specified in spec.
        cli_spec = {
            k: v
            for k, v in {
                "message": message,
                "submitter": submitter,
                "orchestrator": orchestrator,
                "batch_spec": batch_spec,
                "batch_parameters": batch_parameters,
                "inputs": inputs,
                "outputs": outputs,
            }.items() if v is not None
        }

        job_parameters = parse_kv_list(job_parameters)

        # Precedence: CLI option > CLI job parameter > spec file
        spec = _combine_job_specs(
            _load_specs(job_specs or []) + [job_parameters, cli_spec])

        spec["_resolved_batch_parameters"] = _resolve_batch_parameters(
            spec.get("batch_spec"), spec.get("batch_parameters"))

        # Treat "command" as a special case because it's a list and the
        # template expects a string.
        if not command and "command_str" in spec:
            spec["_resolved_command_str"] = spec["command_str"]
        elif not command and "command" not in spec:
            raise InsufficientArgumentsError(
                "No command specified via CLI or job spec")
        else:
            command = command or spec["command"]
            # Unlike datalad run, we're only accepting a list form for now.
            spec["command"] = command
            spec["_resolved_command_str"] = " ".join(map(shlex_quote, command))

        if resref is None:
            if "resource_id" in spec:
                resref = spec["resource_id"]
                resref_type = "id"
            elif "resource_name" in spec:
                resref = spec["resource_name"]
                resref_type = "name"
            else:
                raise InsufficientArgumentsError("No resource specified")
        manager = get_manager()
        resource = manager.get_resource(resref, resref_type)

        if "orchestrator" not in spec:
            # TODO: We could just set this as the default for the Parameter,
            # but it probably makes sense to have the default configurable per
            # resource.
            lgr.debug("No orchestrator specified; setting to 'plain'")
            spec["orchestrator"] = "plain"
        orchestrator_class = ORCHESTRATORS[spec["orchestrator"]]
        orc = orchestrator_class(resource, spec.get("submitter"), spec)

        orc.prepare_remote()
        # TODO: Add support for templates via CLI.
        orc.submit()

        lreg = LocalRegistry()
        lreg.register(orc.jobid, orc.as_dict())

        if follow:
            orc.follow()
            if follow is True:
                remote_fn = None
            else:
                only_on_success = follow.endswith("-if-success")
                do_delete = follow.split("-")[0] == "delete"

                def remote_fn(res, failed):
                    if failed and only_on_success:
                        lgr.info(
                            "Not stopping%s resource '%s' "
                            "because there were failed jobs",
                            " or deleting" if do_delete else "", res.name)
                    else:
                        lgr.info("Stopping%s resource '%s' after %s run",
                                 " and deleting" if do_delete else "",
                                 res.name,
                                 "failed" if failed else "successful")
                        manager.stop(res)
                        if do_delete:
                            manager.delete(res)

            orc.fetch(on_remote_finish=remote_fn)
            lreg.unregister(orc.jobid)
            # TODO: this would duplicate what is done in each .fetch
            # implementation above anyways.  We might want to make
            # fetch return a record with fetched content and failed subjobs
            failed = orc.get_failed_subjobs()
            if failed:
                raise JobError(failed=failed)
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Common interface options

"""

__docformat__ = 'restructuredtext'

from reproman.support.param import Parameter
from reproman.support.constraints import EnsureChoice
from reproman.support.constraints import EnsureInt, EnsureNone, EnsureStr


trace_opt = Parameter(
    args=("--trace",),
    action="store_true",
    doc="""if set, trace execution within the environment""")


#
# Resource specifications
#

resref_arg = Parameter(
    args=("resref",),
    metavar="RESOURCE",
    doc="""Name or ID of the resource to operate on. To see available resources, run
    'reproman ls'""",
    constraints=EnsureStr() | EnsureNone())

resref_opt = Parameter(
Exemple #4
0
class Jobs(Interface):
    """View and manage `reproman run` jobs.

    The possible actions are

      - list: Display a oneline list of all registered jobs

      - show: Display more information for each job over multiple lines

      - delete: Unregister a job locally

      - fetch: Fetch a completed job

      - auto: If jobs are specified (via JOB or --all), behave like 'fetch'.
        Otherwise, behave like 'list'.
    """

    _params_ = dict(
        queries=Parameter(
            metavar="JOB",
            nargs="*",
            doc="""A full job ID or a unique substring."""),
        action=Parameter(
            args=("-a", "--action"),
            constraints=EnsureChoice(
                "auto", "list", "show",
                "delete", "fetch"),
            doc="""Operation to perform on the job(s)."""),
        all_=Parameter(
            dest="all_",
            args=("--all",),
            action="store_true",
            doc="Operate on all jobs"),
        status=Parameter(
            dest="status",
            args=("-s", "--status"),
            action="store_true",
            doc="""Query the resource for status information when listing or
            showing jobs"""),
        # TODO: Add ability to restrict to resource.
    )

    @staticmethod
    def __call__(queries, action="auto", all_=False, status=False):
        job_files = LREG.find_job_files()

        if not job_files:
            lgr.info("No jobs found")
            return

        if all_:
            matched_ids = job_files.keys()
        else:
            matched_ids = []
            for query in queries:
                m = match(query, job_files)
                if m:
                    matched_ids.append(m)
                else:
                    lgr.warning("No jobs matched query %s", query)

        if not matched_ids and action in ["delete", "fetch"]:
            # These are actions where we don't want to just conveniently
            # default to "all" unless --all is explicitly specified.
            raise ValueError("Must specify jobs to {}".format(action))

        # We don't need to load the job to delete it, so check that first.
        if action == "delete":
            for i in matched_ids:
                LREG.unregister(i)
        else:
            jobs = [_load(job_files[i]) for i in matched_ids or job_files]

            if action == "fetch" or (action == "auto" and matched_ids):
                fn = fetch
            elif action == "list" or action == "auto":
                fn = partial(show_oneline, status=status)
            elif action == "show":
                fn = partial(show, status=status)
            else:
                raise RuntimeError("Unknown action: {}".format(action))

            for job in jobs:
                try:
                    fn(job)
                except OrchestratorError as exc:
                    lgr.error("job %s failed: %s", job["_jobid"], exc_str(exc))
                except ResourceNotFoundError:
                    lgr.error("Resource %s (%s) no longer exists",
                              job["resource_id"], job["resource_name"])