def test_run_process_probe_can_timeout():
    probe = probes.ProcProbe
    probe["provider"]["timeout"] = 0.0001

    with pytest.raises(ActivityFailed) as exc:
        run_activity(probes.ProcProbe, config.EmptyConfig,
                     experiments.Secrets).decode("utf-8")
    assert "activity took too long to complete" in str(exc.value)
def test_run_http_probe_can_expect_failure():
    with requests_mock.mock() as m:
        m.post("http://example.com", status_code=404, text="Not found!")

        probe = probes.HTTPProbe.copy()
        probe["provider"]["expected_status"] = 404

        try:
            run_activity(probe, config.EmptyConfig, experiments.Secrets)
        except ActivityFailed:
            pytest.fail("activity should not have failed")
def test_run_http_probe_should_return_parsed_json_value():
    with requests_mock.mock() as m:
        headers = {"Content-Type": "application/json"}
        m.post("http://example.com", json=["well done"], headers=headers)
        result = run_activity(probes.HTTPProbe, config.EmptyConfig,
                              experiments.Secrets)
        assert result["body"] == ["well done"]
示例#4
0
def test_run_process_probe_should_return_raw_value():
    v = "Python {v}\n".format(v=sys.version.split(" ")[0])

    result = run_activity(probes.ProcProbe, config.EmptyConfig,
                          experiments.Secrets)
    assert type(result) is tuple
    assert result == (0, v, '')
示例#5
0
def test_run_process_probe_should_pass_arguments_in_array():
    args = "['-c', '--empty', '--number', '1', '--string', 'with spaces', '--string', 'a second string with the same option']\n"

    result = run_activity(probes.ProcEchoArrayProbe, config.EmptyConfig,
                          experiments.Secrets)
    assert type(result) is tuple
    assert result == (0, args, '')
def test_run_http_probe_must_be_serializable_to_json():
    with requests_mock.mock() as m:
        headers = {"Content-Type": "application/json"}
        m.post("http://example.com", json=["well done"], headers=headers)
        result = run_activity(probes.HTTPProbe, config.EmptyConfig,
                              experiments.Secrets)
        assert json.dumps(result) is not None
def test_run_http_probe_can_retry():
    """
    this test embeds a fake HTTP server to test the retry part
    it can't be easily tested with libraries like requests_mock or responses
    we could mock urllib3 retry mechanism as it is used in the requests library but it
    implies to understand how requests works which is not the idea of this test

    in this test, the first call will lead to a ConnectionAbortedError and the second
    will work
    """
    class MockServerRequestHandler(BaseHTTPRequestHandler):
        """
        mock of a real HTTP server to simulate the behavior of
        a connection aborted error on first call
        """

        call_count = 0

        def do_GET(self):
            MockServerRequestHandler.call_count += 1
            if MockServerRequestHandler.call_count == 1:
                raise ConnectionAbortedError
            self.send_response(200)
            self.end_headers()
            return

    # get a free port to listen on
    s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
    s.bind(("localhost", 0))
    address, port = s.getsockname()
    s.close()

    # start the fake HTTP server in a dedicated thread on the selected port
    server = HTTPServer(("localhost", port), MockServerRequestHandler)
    t = Thread(target=server.serve_forever)
    t.setDaemon(True)
    t.start()

    # change probe URL to call the selected port
    probe = probes.PythonModuleProbeWithHTTPMaxRetries.copy()
    probe["provider"]["url"] = f"http://localhost:{port}"
    try:
        run_activity(probe, config.EmptyConfig, experiments.Secrets)
    except ActivityFailed:
        pytest.fail("activity should not have failed")
def test_run_process_probe_should_return_raw_value():
    v = "Python {v}\n".format(v=sys.version.split(" ")[0])

    result = run_activity(probes.ProcProbe, config.EmptyConfig,
                          experiments.Secrets)
    assert type(result) is dict
    assert result["status"] == 0
    assert result["stdout"] == v
    assert result["stderr"] == ""
示例#9
0
def test_run_process_probe_can_pass_arguments_as_string():
    args = "['-c', '--empty', '--number', '1', '--string', 'with spaces', '--string', 'a second string with the same option']\n"

    result = run_activity(probes.ProcEchoStrProbe, config.EmptyConfig,
                          experiments.Secrets)
    assert type(result) is dict
    assert result["status"] == 0
    assert result["stdout"] == args
    assert result["stderr"] == ''
示例#10
0
def execute_activity(experiment: Experiment, probe: Probe,
                     configuration: Configuration, secrets: Secrets) -> Run:
    """
    Low-level wrapper around the actual activity provider call to collect
    some meta data (like duration, start/end time, exceptions...) during
    the run.
    """
    ref = probe.get("ref")
    if ref:
        probe = lookup_activity(ref)
        if not probe:
            raise ActivityFailed(
                "could not find referenced activity '{r}'".format(r=ref))

    with controls(level="activity",
                  experiment=experiment,
                  context=probe,
                  configuration=configuration,
                  secrets=secrets) as control:
        pauses = probe.get("pauses", {})
        pause_before = pauses.get("before")
        if pause_before:
            time.sleep(pause_before)

        start = datetime.utcnow()

        run = {"activity": probe.copy(), "output": None}

        result = None
        try:
            result = run_activity(probe, configuration, secrets)
            run["output"] = result
            run["status"] = "succeeded"
        except ActivityFailed as x:
            run["status"] = "failed"
            run["output"] = result
            run["exception"] = traceback.format_exception(type(x), x, None)
        finally:
            end = datetime.utcnow()
            run["start"] = start.isoformat()
            run["end"] = end.isoformat()
            run["duration"] = (end - start).total_seconds()

            pause_after = pauses.get("after")
            if pause_after:
                time.sleep(pause_after)

        control.with_state(run)

    return run
示例#11
0
def _(tolerance: dict, value: Any, secrets: Secrets = None) -> bool:
    tolerance_type = tolerance.get("type")

    if tolerance_type == "probe":
        tolerance["arguments"]["value"] = value
        run = run_activity(tolerance, secrets)
        return run["status"] == "succeeded"
    elif tolerance_type == "regex":
        target = tolerance.get("target")
        pattern = tolerance.get("pattern")
        rx = re.compile(pattern)
        if target:
            value = value.get(target, value)
        return rx.search(value) is not None
    elif tolerance_type == "jsonpath":
        target = tolerance.get("target")
        path = tolerance.get("path")
        count_value = tolerance.get("count", None)
        px = jparse.parse(path)

        if target:
            # if no target was provided, we use the tested value as-is
            value = value.get(target, value)

        if isinstance(value, bytes):
            value = value.decode('utf-8')

        if isinstance(value, str):
            try:
                value = json.loads(value)
            except json.decoder.JSONDecodeError:
                pass

        items = px.find(value)

        result = len(items) > 0

        if count_value is not None:
            result = len(items) == count_value

        if "expect" in tolerance:
            expect = tolerance["expect"]
            values = [item.value for item in items]
            if len(values) == 1:
                result = expect in [values[0], values]
            else:
                result = values == expect

        return result
示例#12
0
def test_run_python_probe_should_return_raw_value():
    # our probe checks a file exists
    assert (run_activity(probes.PythonModuleProbe, config.EmptyConfig,
                         experiments.Secrets) is True)
示例#13
0
def test_run_http_probe_should_return_raw_text_value():
    with requests_mock.mock() as m:
        m.post("http://example.com", text="['well done']")
        result = run_activity(probes.HTTPProbe, config.EmptyConfig,
                              experiments.Secrets)
        assert result["body"] == "['well done']"
示例#14
0
def _(tolerance: dict, value: Any, secrets: Secrets = None) -> bool:
    tolerance["arguments"]["value"] = value
    run = run_activity(tolerance, secrets)
    return run["status"] == "succeeded"
示例#15
0
def _(tolerance: dict,
      value: Any,
      configuration: Configuration = None,
      secrets: Secrets = None) -> bool:
    tolerance_type = tolerance.get("type")

    if tolerance_type == "probe":
        tolerance["provider"]["arguments"]["value"] = value
        try:
            run_activity(tolerance, configuration, secrets)
            return True
        except ActivityFailed:
            return False
    elif tolerance_type == "regex":
        target = tolerance.get("target")
        pattern = tolerance.get("pattern")
        rx = re.compile(pattern)
        if target:
            value = value.get(target, value)
        return rx.search(value) is not None
    elif tolerance_type == "jsonpath":
        target = tolerance.get("target")
        path = tolerance.get("path")
        count_value = tolerance.get("count", None)
        px = jparse.parse(path)

        if target:
            # if no target was provided, we use the tested value as-is
            value = value.get(target, value)

        if isinstance(value, bytes):
            value = value.decode('utf-8')

        if isinstance(value, str):
            try:
                value = json.loads(value)
            except json.decoder.JSONDecodeError:
                pass

        items = px.find(value)

        result = len(items) > 0

        if count_value is not None:
            result = len(items) == count_value

        if "expect" in tolerance:
            expect = tolerance["expect"]
            values = [item.value for item in items]
            if len(values) == 1:
                result = expect in [values[0], values]
            else:
                result = values == expect

        return result
    elif tolerance_type == "range":
        target = tolerance.get("target")
        if target:
            value = value.get(target, value)

        try:
            value = Decimal(value)
        except InvalidOperation:
            logger.debug("range check expects a number value")
            return False

        the_range = tolerance.get("range")
        min_value = the_range[0]
        max_value = the_range[1]
        return Decimal(min_value) <= value <= Decimal(max_value)
示例#16
0
def _(tolerance: dict,
      value: Any,
      configuration: Configuration = None,
      secrets: Secrets = None) -> bool:
    tolerance_type = tolerance.get("type")

    if tolerance_type == "probe":
        tolerance["provider"]["arguments"]["value"] = value
        try:
            rtn = run_activity(tolerance, configuration, secrets)
            if rtn:
                return True
            else:
                return False
        except ActivityFailed:
            return False
    elif tolerance_type == "regex":
        target = tolerance.get("target")
        pattern = tolerance.get("pattern")
        pattern = substitute(pattern, configuration, secrets)
        logger.debug("Applied pattern is: {}".format(pattern))
        rx = re.compile(pattern)
        if target:
            value = value.get(target, value)
        return rx.search(value) is not None
    elif tolerance_type == "jsonpath":
        target = tolerance.get("target")
        path = tolerance.get("path")
        count_value = tolerance.get("count", None)
        path = substitute(path, configuration, secrets)
        logger.debug("Applied jsonpath is: {}".format(path))
        px = JSONPath.parse_str(path)

        if target:
            # if no target was provided, we use the tested value as-is
            value = value.get(target, value)

        if isinstance(value, bytes):
            value = value.decode('utf-8')

        if isinstance(value, str):
            try:
                value = json.loads(value)
            except json.decoder.JSONDecodeError:
                pass

        values = list(map(lambda m: m.current_value, px.match(value)))
        result = len(values) > 0
        if count_value is not None:
            result = len(values) == count_value

        expect = tolerance.get("expect")
        if "expect" in tolerance:
            if not isinstance(expect, list):
                result = values == [expect]
            else:
                result = values == expect

        if result is False:
            if "expect" in tolerance:
                logger.debug("jsonpath found '{}' but expected '{}'".format(
                    str(values), str(tolerance["expect"])))
            else:
                logger.debug("jsonpath found '{}'".format(str(values)))

        return result
    elif tolerance_type == "range":
        target = tolerance.get("target")
        if target:
            value = value.get(target, value)

        try:
            value = Decimal(value)
        except InvalidOperation:
            logger.debug("range check expects a number value")
            return False

        the_range = tolerance.get("range")
        min_value = the_range[0]
        max_value = the_range[1]
        return Decimal(min_value) <= value <= Decimal(max_value)
def load_dynamic_configuration(
    config: Configuration, secrets: Secrets = None
) -> Configuration:
    """
    This is for loading a dynamic configuration if exists.
    The dynamic config is a regular activity (probe) in the configuration
    section. If there's a use-case for setting a configuration dynamically
    right before the experiment is starting. It executes the probe,
    and then the return value of this probe will be the config you wish to set.
    The dictionary needs to have a key named `type` and as a value `probe`,
    alongside the rest of the probe props.
    (No need for the `tolerance` key).

    For example:

    ```json
    "some_dynamic_config": {
      "name": "some config probe",
      "type": "probe",
      "provider": {
        "type": "python",
        "module": "src.probes",
        "func": "config_probe",
        "arguments": {
            "arg1":"arg1"
        }
      }
    }
    ```

    `some_dynamic_config` will be set with the return value
    of the function config_probe.

    Side Note: the probe type can be the same as a regular probe can be,
    python, process or http. The config argument contains all the
    configurations of the experiment including the raw config_probe
    configuration that can be dynamically injected.

    The configurations contain as well all the env vars after they are set in
    `load_configuration`.

    The `secrets` argument contains all the secrets of the experiment.

    For `process` probes, the stdout value (stripped of endlines)
    is stored into the configuration.
    For `http` probes, the `body` value is stored.
    For `python` probes, the output of the function will be stored.

    We do not stop on errors but log a debug message and do not include the
    key into the result dictionary.
    """
    # we delay this so that the configuration module can be imported leanly
    # from elsewhere
    from chaoslib.activity import run_activity

    conf = {}
    secrets = secrets or {}

    had_errors = False
    logger.debug("Loading dynamic configuration...")
    for (key, value) in config.items():
        if not (isinstance(value, dict) and value.get("type") == "probe"):
            conf[key] = config.get(key, value)
            continue

        # we have a dynamic config
        name = value.get("name")
        provider_type = value["provider"]["type"]
        value["provider"]["secrets"] = deepcopy(secrets)
        try:
            output = run_activity(value, conf, secrets)
        except Exception:
            had_errors = True
            logger.debug(f"Failed to load configuration '{name}'", exc_info=True)
            continue

        if provider_type == "python":
            conf[key] = output
        elif provider_type == "process":
            if output["status"] != 0:
                had_errors = True
                logger.debug(
                    f"Failed to load configuration dynamically "
                    f"from probe '{name}': {output['stderr']}"
                )
            else:
                conf[key] = output.get("stdout", "").strip()
        elif provider_type == "http":
            conf[key] = output.get("body")

    if had_errors:
        logger.warning(
            "Some of the dynamic configuration failed to be loaded."
            "Please review the log file for understanding what happened."
        )

    return conf