Esempio n. 1
0
    def test_execute_process_no_error_not_required_params(self):
        """
        Optional parameters for execute job shouldn't raise an error if omitted,
        and should resolve to default values if any was specified.
        """
        # get basic mock/data templates
        name = fully_qualified_name(self)
        execute_mock_data_tests = list()
        for i in range(2):
            mock_execute = mocked_process_job_runner("job-{}-{}".format(
                name, i))
            data_execute = self.get_process_execute_template("{}-{}".format(
                name, i))
            execute_mock_data_tests.append((mock_execute, data_execute))

        # apply modifications for testing
        execute_mock_data_tests[0][1].pop(
            "inputs"
        )  # no inputs is valid (although can be required for WPS process)
        execute_mock_data_tests[0][1]["outputs"][0].pop(
            "transmissionMode")  # should resolve to default value

        for mock_execute, data_execute in execute_mock_data_tests:
            with contextlib.ExitStack() as stack:
                for exe in mock_execute:
                    stack.enter_context(exe)
                path = "/processes/{}/jobs".format(
                    self.process_public.identifier)
                resp = self.app.post_json(path,
                                          params=data_execute,
                                          headers=self.json_headers)
                assert resp.status_code == 201, "Expected job submission without inputs created without error."
Esempio n. 2
0
    def test_execute_process_missing_required_params(self):
        execute_data = self.get_process_execute_template(
            fully_qualified_name(self))

        # remove components for testing different cases
        execute_data_tests = [deepcopy(execute_data) for _ in range(7)]
        execute_data_tests[0].pop("outputs")
        execute_data_tests[1].pop("mode")
        execute_data_tests[2].pop("response")
        execute_data_tests[3]["mode"] = "random"
        execute_data_tests[4]["response"] = "random"
        execute_data_tests[5]["inputs"] = [{
            "test_input": "test_value"
        }]  # bad format
        execute_data_tests[6]["outputs"] = [{
            "id": "test_output",
            "transmissionMode": "random"
        }]

        uri = "/processes/{}/jobs".format(self.process_public.identifier)
        for i, exec_data in enumerate(execute_data_tests):
            resp = self.app.post_json(uri,
                                      params=exec_data,
                                      headers=self.json_headers,
                                      expect_errors=True)
            msg = "Failed with test variation '{}' with value '{}'."
            assert resp.status_code in [400,
                                        422], msg.format(i, resp.status_code)
            assert resp.content_type == CONTENT_TYPE_APP_JSON, msg.format(
                i, resp.content_type)
Esempio n. 3
0
 def test_execute_process_mode_sync_not_supported(self):
     execute_data = self.get_process_execute_template(
         fully_qualified_name(self))
     execute_data["mode"] = EXECUTE_MODE_SYNC
     uri = "/processes/{}/jobs".format(self.process_public.identifier)
     resp = self.app.post_json(uri,
                               params=execute_data,
                               headers=self.json_headers,
                               expect_errors=True)
     assert resp.status_code == 501
     assert resp.content_type == CONTENT_TYPE_APP_JSON
Esempio n. 4
0
 def test_execute_process_transmission_mode_value_not_supported(self):
     execute_data = self.get_process_execute_template(
         fully_qualified_name(self))
     execute_data["outputs"][0][
         "transmissionMode"] = EXECUTE_TRANSMISSION_MODE_VALUE
     uri = "/processes/{}/jobs".format(self.process_public.identifier)
     with contextlib.ExitStack() as stack_exec:
         for mock_exec in mocked_execute_process():
             stack_exec.enter_context(mock_exec)
         resp = self.app.post_json(uri,
                                   params=execute_data,
                                   headers=self.json_headers,
                                   expect_errors=True)
     assert resp.status_code == 501
     assert resp.content_type == CONTENT_TYPE_APP_JSON
Esempio n. 5
0
    def save_log(self,
                 errors=None,       # type: Optional[Union[str, List[WPSException]]]
                 logger=None,       # type: Optional[Logger]
                 message=None,      # type: Optional[str]
                 level=INFO,        # type: int
                 ):                 # type: (...) -> None
        """
        Logs the specified error and/or message, and adds the log entry to the complete job log.

        For each new log entry, additional :class:`Job` properties are added according to :meth:`Job._get_log_msg`
        and the format defined by :func:`get_job_log_msg`.

        :param errors:
            An error message or a list of WPS exceptions from which to log and save generated message stack.
        :param logger:
            An additional :class:`Logger` for which to propagate logged messages on top saving them to the job.
        :param message:
            Explicit string to be logged, otherwise use the current :py:attr:`Job.status_message` is used.
        :param level:
            Logging level to apply to the logged ``message``. This parameter is ignored if ``errors`` are logged.

        .. note::
            The job object is updated with the log but still requires to be pushed to database to actually persist it.
        """
        if isinstance(errors, str):
            log_msg = [(ERROR, self._get_log_msg(message))]
            self.exceptions.append(errors)
        elif isinstance(errors, list):
            log_msg = [(ERROR, self._get_log_msg("{0.text} - code={0.code} - locator={0.locator}".format(error)))
                       for error in errors]
            self.exceptions.extend([{
                "Code": error.code,
                "Locator": error.locator,
                "Text": error.text
            } for error in errors])
        else:
            log_msg = [(level, self._get_log_msg(message))]
        for lvl, msg in log_msg:
            fmt_msg = get_log_fmt() % dict(asctime=now().strftime(get_log_date_fmt()),
                                           levelname=getLevelName(lvl),
                                           name=fully_qualified_name(self),
                                           message=msg)
            if len(self.logs) == 0 or self.logs[-1] != fmt_msg:
                self.logs.append(fmt_msg)
                if logger:
                    logger.log(lvl, msg)
Esempio n. 6
0
    def test_execute_process_language(self):
        uri = "/processes/{}/jobs".format(self.process_public.identifier)
        data = self.get_process_execute_template()
        task = "job-{}".format(fully_qualified_name(self))
        mock_execute = mocked_process_job_runner(task)

        with contextlib.ExitStack() as stack:
            for exe in mock_execute:
                stack.enter_context(exe)
            headers = self.json_headers.copy()
            headers["Accept-Language"] = "fr-CA"
            resp = self.app.post_json(uri, params=data, headers=headers)
            assert resp.status_code == 201, "Error: {}".format(resp.text)
            try:
                job = self.job_store.fetch_by_id(resp.json["jobID"])
            except JobNotFound:
                self.fail("Job should have been created and be retrievable.")
            assert job.id == resp.json["jobID"]
            assert job.accept_language == "fr-CA"
Esempio n. 7
0
    def test_execute_process_success(self):
        uri = "/processes/{}/jobs".format(self.process_public.identifier)
        data = self.get_process_execute_template()
        task = "job-{}".format(fully_qualified_name(self))
        mock_execute = mocked_process_job_runner(task)

        with contextlib.ExitStack() as stack:
            for exe in mock_execute:
                stack.enter_context(exe)
            resp = self.app.post_json(uri,
                                      params=data,
                                      headers=self.json_headers)
            assert resp.status_code == 201, "Error: {}".format(resp.text)
            assert resp.content_type == CONTENT_TYPE_APP_JSON
            assert resp.json["location"].endswith(resp.json["jobID"])
            assert resp.headers["Location"] == resp.json["location"]
            try:
                job = self.job_store.fetch_by_id(resp.json["jobID"])
            except JobNotFound:
                self.fail("Job should have been created and be retrievable.")
            assert job.id == resp.json["jobID"]
            assert job.task_id == STATUS_ACCEPTED  # temporary value until processed by celery
Esempio n. 8
0
    def test_execute_process_dont_cast_one_of(self):
        """
        When validating the schema for OneOf values, don't cast the result to the first valid schema.
        """
        # get basic mock/data templates
        name = fully_qualified_name(self)
        execute_mock_data_tests = list()

        mock_execute = mocked_process_job_runner("job-{}".format(name))
        data_execute = self.get_process_execute_template("100")
        execute_mock_data_tests.append((mock_execute, data_execute))

        with contextlib.ExitStack() as stack:
            for exe in mock_execute:
                stack.enter_context(exe)
            path = "/processes/{}/jobs".format(self.process_public.identifier)
            resp = self.app.post_json(path,
                                      params=data_execute,
                                      headers=self.json_headers)
            assert resp.status_code == 201, "Expected job submission without inputs created without error."
            job = self.job_store.fetch_by_id(resp.json["jobID"])
            assert job.inputs[0][
                "data"] == "100"  # not cast to float or integer
Esempio n. 9
0
 def fully_qualified_test_process_name(self):
     return fully_qualified_name(self).replace(".", "-")
Esempio n. 10
0
def ows_response_tween_factory_excview(handler, registry):  # noqa: F811
    """
    Tween factory which produces a tween which transforms common exceptions into OWS specific exceptions.
    """
    return lambda request: ows_response_tween(request, handler)


def ows_response_tween_factory_ingress(handler, registry):  # noqa: F811
    """
    Tween factory which produces a tween which transforms common exceptions into OWS specific exceptions.
    """
    def handle_ows_tween(request):
        # because the EXCVIEW will also wrap any exception raised that should before be handled by OWS response
        # to allow conversions to occur, use a flag that will re-raise the result
        setattr(handler, OWS_TWEEN_HANDLED, True)
        return ows_response_tween(request, handler)
    return handle_ows_tween


# names must differ to avoid conflicting configuration error
OWS_RESPONSE_EXCVIEW = fully_qualified_name(ows_response_tween_factory_excview)
OWS_RESPONSE_INGRESS = fully_qualified_name(ows_response_tween_factory_ingress)


def includeme(config):
    # using 'INGRESS' to run `weaver.wps_restapi.api` views that fix HTTP code before OWS response
    config.add_tween(OWS_RESPONSE_INGRESS, under=INGRESS)
    # using 'EXCVIEW' to run over any other 'valid' exception raised to adjust formatting and log
    config.add_tween(OWS_RESPONSE_EXCVIEW, over=EXCVIEW)
Esempio n. 11
0
def get_processes(request):
    # type: (PyramidRequest) -> AnyViewResponse
    """
    List registered processes (GetCapabilities).

    Optionally list both local and provider processes.
    """
    try:
        params = sd.GetProcessesQuery().deserialize(request.params)
    except colander.Invalid as ex:
        raise HTTPBadRequest(json={
            "code": "ProcessInvalidParameter",
            "description": "Process query parameters failed validation.",
            "error": colander.Invalid.__name__,
            "cause": str(ex),
            "value": repr_json(ex.value or dict(request.params), force_string=False),
        })

    detail = asbool(params.get("detail", True))
    ignore = asbool(params.get("ignore", True))
    try:
        # get local processes and filter according to schema validity
        # (previously deployed process schemas can become invalid because of modified schema definitions
        results = get_processes_filtered_by_valid_schemas(request)
        processes, invalid_processes, paging, with_providers, total_processes = results
        if invalid_processes:
            raise HTTPServiceUnavailable(
                "Previously deployed processes are causing invalid schema integrity errors. "
                f"Manual cleanup of following processes is required: {invalid_processes}"
            )

        body = {"processes": processes if detail else [get_any_id(p) for p in processes]}  # type: JSON
        if not with_providers:
            paging = {"page": paging.get("page"), "limit": paging.get("limit")}  # remove other params
            body.update(paging)
        else:
            paging = {}  # disable to remove paging-related links

        try:
            body["links"] = get_process_list_links(request, paging, total_processes)
        except IndexError as exc:
            raise HTTPBadRequest(json={
                "description": str(exc),
                "cause": "Invalid paging parameters.",
                "error": type(exc).__name__,
                "value": repr_json(paging, force_string=False)
            })

        # if 'EMS/HYBRID' and '?providers=True', also fetch each provider's processes
        if with_providers:
            # param 'check' enforced because must fetch for listing of available processes (GetCapabilities)
            # when 'ignore' is not enabled, any failing definition should raise any derived 'ServiceException'
            services = get_provider_services(request, ignore=ignore, check=True)
            body.update({
                "providers": [svc.summary(request, ignore=ignore) if detail else {"id": svc.name} for svc in services]
            })
            invalid_services = [False] * len(services)
            for i, provider in enumerate(services):
                # ignore failing parsing of the service description
                if body["providers"][i] is None:
                    invalid_services[i] = True
                    continue
                # attempt parsing available processes and ignore again failing items
                processes = provider.processes(request, ignore=ignore)
                if processes is None:
                    invalid_services[i] = True
                    continue
                total_processes += len(processes)
                body["providers"][i].update({
                    "processes": processes if detail else [get_any_id(proc) for proc in processes]
                })
            if any(invalid_services):
                LOGGER.debug("Invalid providers dropped due to failing parsing and ignore query: %s",
                             [svc.name for svc, status in zip(services, invalid_services) if status])
                body["providers"] = [svc for svc, ignore in zip(body["providers"], invalid_services) if not ignore]

        body["total"] = total_processes
        body["description"] = sd.OkGetProcessesListResponse.description
        LOGGER.debug("Process listing generated, validating schema...")
        body = sd.MultiProcessesListing().deserialize(body)
        return HTTPOk(json=body)

    except ServiceException as exc:
        LOGGER.debug("Error when listing provider processes using query parameter raised: [%s]", exc, exc_info=exc)
        raise HTTPServiceUnavailable(json={
            "description": "At least one provider could not list its processes. "
                           "Failing provider errors were requested to not be ignored.",
            "exception": fully_qualified_name(exc),
            "error": str(exc)
        })
    except HTTPException:
        raise
    except colander.Invalid as exc:
        raise HTTPBadRequest(json={
            "type": "InvalidParameterValue",
            "title": "Invalid parameter value.",
            "description": "Submitted request parameters are invalid or could not be processed.",
            "cause": clean_json_text_body(f"Invalid schema: [{exc.msg or exc!s}]"),
            "error": exc.__class__.__name__,
            "value": repr_json(exc.value, force_string=False),
        })
Esempio n. 12
0
    def execute(self, workflow_inputs, out_dir, expected_outputs):
        # type: (CWL_RuntimeInputsMap, str, CWL_ExpectedOutputs) -> None
        """
        Execute the core operation of the remote :term:`Process` using the given inputs.

        The function is expected to monitor the process and update the status.
        Retrieve the expected outputs and store them in the ``out_dir``.

        :param workflow_inputs: `CWL` job dict
        :param out_dir: directory where the outputs must be written
        :param expected_outputs: expected value outputs as `{'id': 'value'}`
        """
        self.update_status("Preparing process for remote execution.",
                           REMOTE_JOB_PROGRESS_PREPARE, STATUS_RUNNING)
        self.prepare()
        self.update_status("Process ready for execute remote process.",
                           REMOTE_JOB_PROGRESS_READY, STATUS_RUNNING)

        self.update_status("Staging inputs for remote execution.",
                           REMOTE_JOB_PROGRESS_STAGE_IN, STATUS_RUNNING)
        staged_inputs = self.stage_inputs(workflow_inputs)

        self.update_status("Preparing inputs/outputs for remote execution.",
                           REMOTE_JOB_PROGRESS_FORMAT_IO, STATUS_RUNNING)
        expect_outputs = [{"id": output} for output in expected_outputs]
        process_inputs = self.format_inputs(staged_inputs)
        process_outputs = self.format_outputs(expect_outputs)

        try:
            self.update_status("Executing remote process job.",
                               REMOTE_JOB_PROGRESS_EXECUTE, STATUS_RUNNING)
            monitor_ref = self.dispatch(process_inputs, process_outputs)
            self.update_status(
                "Monitoring remote process job until completion.",
                REMOTE_JOB_PROGRESS_MONITOR, STATUS_RUNNING)
            job_success = self.monitor(monitor_ref)
            if not job_success:
                raise PackageExecutionError(
                    "Failed dispatch and monitoring of remote process execution."
                )
        except Exception as exc:
            err_msg = "{0}: {1!s}".format(fully_qualified_name(exc), exc)
            err_ctx = "Dispatch and monitoring of remote process caused an unhandled error."
            LOGGER.exception("%s [%s]", err_ctx, err_msg, exc_info=exc)
            self.update_status(
                "Running final cleanup operations following failed execution.",
                REMOTE_JOB_PROGRESS_CLEANUP, STATUS_RUNNING)
            self.cleanup()
            raise PackageExecutionError(err_ctx) from exc

        self.update_status("Retrieving job results definitions.",
                           REMOTE_JOB_PROGRESS_RESULTS, STATUS_RUNNING)
        results = self.get_results(monitor_ref)
        self.update_status("Staging job outputs from remote process.",
                           REMOTE_JOB_PROGRESS_STAGE_OUT, STATUS_RUNNING)
        self.stage_results(results, expected_outputs, out_dir)

        self.update_status(
            "Running final cleanup operations before completion.",
            REMOTE_JOB_PROGRESS_CLEANUP, STATUS_RUNNING)
        self.cleanup()

        self.update_status(
            "Execution of remote process execution completed successfully.",
            REMOTE_JOB_PROGRESS_COMPLETED, STATUS_SUCCEEDED)