Exemple #1
0
def validate_process_activity(activity: Activity):
    """
    Validate a process activity.

    A process activity requires:

    * a `"path"` key which is an absolute path to an executable the current
      user can call

    In all failing cases, raises :exc:`InvalidActivity`.

    This should be considered as a private function.
    """
    name = activity["name"]
    provider = activity["provider"]

    path = raw_path = provider.get("path")
    if not path:
        raise InvalidActivity("a process activity must have a path")

    path = shutil.which(path)
    if not path:
        raise InvalidActivity(
            "path '{path}' cannot be found, in activity '{name}'".format(
                path=raw_path, name=name))

    if not os.access(path, os.X_OK):
        raise InvalidActivity(
            "no access permission to '{path}', in activity '{name}'".format(
                path=raw_path, name=name))
Exemple #2
0
def check_json_path(tolerance: Tolerance):
    """
    Check the JSON path of a tolerance and raise :exc:`InvalidActivity`
    when the path is missing or invalid.

    See: https://github.com/h2non/jsonpath-ng
    """
    if not HAS_JSONPATH:
        raise InvalidActivity(
            "Install the `jsonpath_ng` package to use a JSON path tolerance: "
            "`pip install chaostoolkit-lib[jsonpath]`.")

    if "path" not in tolerance:
        raise InvalidActivity(
            "hypothesis jsonpath probe tolerance must have a `path` key")

    try:
        path = tolerance.get("path")
        jparse.parse(path)
    except TypeError as t:
        raise InvalidActivity(
            "hypothesis probe tolerance path {} has an invalid type".format(
                path))
    except JsonPathLexerError as e:
        raise InvalidActivity(
            "hypothesis probe tolerance JSON path '{}' is invalid: {}".format(
                str(e)))
def ensure_hypothesis_tolerance_is_valid(tolerance: Tolerance):
    """
    Validate the tolerance of the hypothesis probe and raises
    :exc:`InvalidActivity` if it isn't valid.
    """
    if not isinstance(tolerance, (bool, int, list, str, dict)):
        raise InvalidActivity(
            "hypothesis probe tolerance must either be an integer, "
            "a string, a boolean or a pair of values for boundaries. "
            "It can also be a dictionary which is a probe activity "
            "definition that takes an argument called `value` with "
            "the value of the probe itself to be validated")

    if isinstance(tolerance, dict):
        tolerance_type = tolerance.get("type")

        if tolerance_type == "probe":
            ensure_activity_is_valid(tolerance)
        elif tolerance_type == "regex":
            check_regex_pattern(tolerance)
        elif tolerance_type == "jsonpath":
            check_json_path(tolerance)
        elif tolerance_type == "range":
            check_range(tolerance)
        else:
            raise InvalidActivity(
                "hypothesis probe tolerance type '{}' is unsupported".format(
                    tolerance_type))
Exemple #4
0
def validate_http_activity(activity: Activity):
    """
    Validate a HTTP activity.

    A process activity requires:

    * a `"url"` key which is the address to call

    In addition, you can pass the followings:

    * `"method"` which is the HTTP verb to use (default to `"GET"`)
    * `"headers"` which must be a mapping of string to string

    In all failing cases, raises :exc:`InvalidActivity`.

    This should be considered as a private function.
    """
    provider = activity["provider"]
    url = provider.get("url")
    if not url:
        raise InvalidActivity("a HTTP activity must have a URL")

    headers = provider.get("headers")
    if headers and not type(headers) == dict:
        raise InvalidActivity("a HTTP activities expect headers as a mapping")
Exemple #5
0
def check_range(tolerance: Tolerance):
    """
    Check a value is within a given range. That range may be set to a min and
    max value or a sequence.
    """
    if "range" not in tolerance:
        raise InvalidActivity(
            "hypothesis range probe tolerance must have a `range` key")

    the_range = tolerance["range"]
    if not isinstance(the_range, list):
        raise InvalidActivity(
            "hypothesis range must be a sequence")

    if len(the_range) != 2:
        raise InvalidActivity(
            "hypothesis range sequence must be made of two values")

    if not isinstance(the_range[0], Number):
        raise InvalidActivity(
            "hypothesis range lower boundary must be a number")

    if not isinstance(the_range[1], Number):
        raise InvalidActivity(
            "hypothesis range upper boundary must be a number")
def check_json_path(tolerance: Tolerance):
    """
    Check the JSON path of a tolerance and raise :exc:`InvalidActivity`
    when the path is missing or invalid.

    See: https://github.com/h2non/jsonpath-ng
    """
    if not HAS_JSONPATH:
        raise InvalidActivity(
            "Install the `jsonpath2` package to use a JSON path tolerance: "
            "`pip install chaostoolkit-lib[jsonpath]`.")

    if "path" not in tolerance:
        raise InvalidActivity(
            "hypothesis jsonpath probe tolerance must have a `path` key")

    try:
        path = tolerance.get("path", "").strip()
        if not path:
            raise InvalidActivity(
                "hypothesis probe tolerance JSON path cannot be empty")
        JSONPath.parse_str(path)
    except ValueError:
        raise InvalidActivity(
            "hypothesis probe tolerance JSON path {} is invalid".format(path))
    except TypeError:
        raise InvalidActivity(
            "hypothesis probe tolerance JSON path {} has an invalid "
            "type".format(path))
Exemple #7
0
def ensure_hypothesis_is_valid(experiment: Experiment):
    """
    Validates that the steady state hypothesis entry has the expected schema
    or raises :exc:`InvalidExperiment` or :exc:`InvalidProbe`.
    """
    hypo = experiment.get("steady-state-hypothesis")
    if hypo is None:
        return

    if not hypo.get("title"):
        raise InvalidExperiment("hypothesis requires a title")

    probes = hypo.get("probes")
    if probes:
        for probe in probes:
            ensure_activity_is_valid(probe)

            if "tolerance" not in probe:
                raise InvalidActivity(
                    "hypothesis probe must have a tolerance entry")

            if not isinstance(probe["tolerance"], (
                    bool, int, list, str, dict)):
                raise InvalidActivity(
                    "hypothesis probe tolerance must either be an integer, "
                    "a string, a boolean or a pair of values for boundaries. "
                    "It can also be a dictionary which is a probe activity "
                    "definition that takes an argument called `value` with "
                    "the value of the probe itself to be validated")

            if isinstance(probe, dict):
                ensure_activity_is_valid(probe)
Exemple #8
0
def validate_python_control(control: Control):
    """
    Verify that a control block matches the specification
    """
    name = control["name"]
    provider = control["provider"]
    mod_name = provider.get("module")
    if not mod_name:
        raise InvalidActivity(f"Control '{name}' must have a module path")

    try:
        importlib.import_module(mod_name)
    except ImportError:
        logger.warning(
            "Could not find Python module '{mod}' "
            "in control '{name}'. Did you install the Python "
            "module? The experiment will carry on running "
            "nonetheless.".format(mod=mod_name, name=name)
        )

    # a control can validate itself too
    # ideally, do it cleanly and raise chaoslib.exceptions.InvalidActivity
    func = load_func(control, "validate_control")
    if not func:
        return

    func(control)
Exemple #9
0
def validate_python_control(control: Control):
    """
    Verify that a control block matches the specification
    """
    name = control["name"]
    provider = control["provider"]
    mod_name = provider.get("module")
    if not mod_name:
        raise InvalidActivity(
            "Control '{}' must have a module path".format(name))

    try:
        importlib.import_module(mod_name)
    except ImportError:
        raise InvalidActivity("could not find Python module '{mod}' "
                              "in control '{name}'".format(
                                  mod=mod_name, name=name))
def random_host(host_list: list, num_target: int):

    new_host_list = host_list[:]

    if num_target <= 0 or num_target > len(new_host_list):
        raise InvalidActivity("Number of target is not correct")

    try:
        nb_remove_target = len(new_host_list) - int(num_target)

        if nb_remove_target > 0:
            for i in range(nb_remove_target):
                rand = randint(0, len(new_host_list) - 1)
                new_host_list.pop(rand)

    except Exception:
        raise InvalidActivity("Could not generate host list")

    return new_host_list
def check_regex_pattern(tolerance: Tolerance):
    """
    Check the regex pattern of a tolerance and raise :exc:`InvalidActivity`
    when the pattern is missing or invalid (meaning, cannot be compiled by
    the Python regex engine).
    """
    if "pattern" not in tolerance:
        raise InvalidActivity(
            "hypothesis regex probe tolerance must have a `pattern` key")

    pattern = tolerance["pattern"]
    try:
        re.compile(pattern)
    except TypeError:
        raise InvalidActivity(
            "hypothesis probe tolerance pattern {} has an invalid type".format(
                pattern))
    except re.error as e:
        raise InvalidActivity(
            "hypothesis probe tolerance pattern {} seems invalid: {}".format(
                e.pattern, e.msg))
Exemple #12
0
def validate_python_control(control: Control):
    """
    Verify that a control block matches the specification
    """
    name = control["name"]
    provider = control["provider"]
    mod_name = provider.get("module")
    if not mod_name:
        raise InvalidActivity(
            "Control '{}' must have a module path".format(name))

    try:
        importlib.import_module(mod_name)
    except ImportError:
        logger.warning("Could not find Python module '{mod}' "
                       "in control '{name}'. Did you install the Python "
                       "module? The experiment will carry on running "
                       "nonetheless.".format(mod=mod_name, name=name))
def ensure_hypothesis_is_valid(experiment: Experiment):
    """
    Validates that the steady state hypothesis entry has the expected schema
    or raises :exc:`InvalidExperiment` or :exc:`InvalidActivity`.
    """
    hypo = experiment.get("steady-state-hypothesis")
    if hypo is None:
        return

    if not hypo.get("title"):
        raise InvalidExperiment("hypothesis requires a title")

    probes = hypo.get("probes")
    if probes:
        for probe in probes:
            ensure_activity_is_valid(probe)

            if "tolerance" not in probe:
                raise InvalidActivity("hypothesis probe must have a tolerance entry")

            ensure_hypothesis_tolerance_is_valid(probe["tolerance"])
Exemple #14
0
def ensure_activity_is_valid(activity: Activity):
    """
    Goes through the activity and checks certain of its properties and raise
    :exc:`InvalidActivity` whenever one does not respect the expectations.

    An activity must at least take the following key:

    * `"type"` the kind of activity, one of `"python"`, `"process"` or `"http"`

    Depending on the type, an activity requires a variety of other keys.

    In all failing cases, raises :exc:`InvalidActivity`.
    """
    if not activity:
        raise InvalidActivity("empty activity is no activity")

    # when the activity is just a ref, there is little to validate
    ref = activity.get("ref")
    if ref is not None:
        if not isinstance(ref, str) or ref == '':
            raise InvalidActivity(
                "reference to activity must be non-empty strings")
        return

    activity_type = activity.get("type")
    if not activity_type:
        raise InvalidActivity("an activity must have a type")

    if activity_type not in ("probe", "action"):
        raise InvalidActivity(
            "'{t}' is not a supported activity type".format(t=activity_type))

    if not activity.get("name"):
        raise InvalidActivity("an activity must have a name")

    provider = activity.get("provider")
    if not provider:
        raise InvalidActivity("an activity requires a provider")

    provider_type = provider.get("type")
    if not provider_type:
        raise InvalidActivity("a provider must have a type")

    if provider_type not in ("python", "process", "http"):
        raise InvalidActivity(
            "unknown provider type '{type}'".format(type=provider_type))

    if not activity.get("name"):
        raise InvalidActivity("activity must have a name (cannot be empty)")

    timeout = activity.get("timeout")
    if timeout is not None:
        if not isinstance(timeout, numbers.Number):
            raise InvalidActivity("activity timeout must be a number")

    pauses = activity.get("pauses")
    if pauses is not None:
        before = pauses.get("before")
        if before is not None and not isinstance(before, numbers.Number):
            raise InvalidActivity("activity before pause must be a number")
        after = pauses.get("after")
        if after is not None and not isinstance(after, numbers.Number):
            raise InvalidActivity("activity after pause must be a number")

    if "background" in activity:
        if not isinstance(activity["background"], bool):
            raise InvalidActivity("activity background must be a boolean")

    if provider_type == "python":
        validate_python_activity(activity)
    elif provider_type == "process":
        validate_process_activity(activity)
    elif provider_type == "http":
        validate_http_activity(activity)
def chaosansible_run(
    host_list: list = ("localhost"),
    configuration: Configuration = None,
    facts: bool = False,
    become: bool = False,
    run_once: bool = False,
    ansible: dict = {},
    num_target: str = "all",
    secrets: Secrets = None,
):

    """
    Run a task through ansible and eventually gather facts from host
    """

    # Check for correct inputs
    if ansible:
        if ansible.get("module") is None:
            raise InvalidActivity("No ansible module defined")

        if ansible.get("args") is None:
            raise InvalidActivity("No ansible module args defined")

    configuration = configuration or {}

    # Ansible configuration elements
    module_path = configuration.get("ansible_module_path")
    become_user = configuration.get("ansible_become_user")
    ssh_key_path = configuration.get("ansible_ssh_private_key")
    ansible_user = configuration.get("ansible_user")
    become_ask_pass = configuration.get("become_ask_pass")
    ssh_extra_args = configuration.get("ansible_ssh_extra_args")

    context.CLIARGS = ImmutableDict(
        connection="smart",
        verbosity=0,
        module_path=module_path,
        forks=10,
        become=become,
        become_method="sudo",
        become_user=become_user,
        check=False,
        diff=False,
        private_key_file=ssh_key_path,
        remote_user=ansible_user,
        ssh_extra_args=ssh_extra_args,
    )

    # Update host_list regarding the number of desired target.
    # Need to generate a new host-list because after being update
    # and will be used later
    if num_target != "all":
        new_host_list = random_host(host_list, int(num_target))
    else:
        new_host_list = host_list[:]

    # Create an inventory
    sources = ",".join(new_host_list)
    if len(new_host_list) == 1:
        sources += ","

    loader = DataLoader()
    inventory = InventoryManager(loader=loader, sources=sources)

    # Instantiate callback for storing results
    results_callback = ResultsCollectorJSONCallback()

    variable_manager = VariableManager(loader=loader, inventory=inventory)
    if become_ask_pass:
        passwords = dict(become_pass=become_ask_pass)
    else:
        passwords = None

    # Ansible taskmanager
    tqm = TaskQueueManager(
        inventory=inventory,
        variable_manager=variable_manager,
        loader=loader,
        passwords=passwords,
        stdout_callback=results_callback,
        run_additional_callbacks=False,
    )

    # Ansible playbook
    play_source = dict(
        name="Ansible Play",
        hosts=new_host_list,
        gather_facts=facts,
        tasks=[
            dict(
                name="facts",
                action=dict(module="debug", args=dict(var="ansible_facts")),
            ),
        ],
    )

    # In cas we only want to gather facts
    if ansible:
        module = ansible.get("module")
        args = ansible.get("args")
        play_source["tasks"].append(
            dict(
                name="task",
                run_once=run_once,
                action=dict(module=module, args=args),
                register="shell_out",
            )
        )

    # Create an ansible playbook
    play = Play().load(play_source,
                       variable_manager=variable_manager,
                       loader=loader)

    # Run it
    try:
        result = tqm.run(play)
    finally:
        tqm.cleanup()
        if loader:
            loader.cleanup_all_tmp_files()

    # Remove ansible tmpdir
    shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)

    if len(results_callback.host_failed) > 0:
        print("Ansible error(s): ")
        for error in results_callback.host_failed:
            print(results_callback.host_failed[error].__dict__)

        raise FailedActivity("Failed to run ansible task")

    elif len(results_callback.host_unreachable) > 0:
        print("Unreachable host(s): ")
        for error in results_callback.host_unreachable:
            print(error)

        raise FailedActivity("At least one target is down")

    else:
        results = {}

        for host, result in results_callback.host_ok.items():
            results[host] = result

        return json.dumps(results)
Exemple #16
0
def validate_python_activity(activity: Activity):  # noqa: C901
    """
    Validate a Python activity.

    A Python activity requires:

    * a `"module"` key which is an absolute Python dotted path for a Python
      module this process can import
    * a `func"` key which is the name of a function in that module

    The `"arguments"` activity key must match the function's signature.

    In all failing cases, raises :exc:`InvalidActivity`.

    This should be considered as a private function.
    """
    name = activity["name"]
    provider = activity["provider"]
    mod_name = provider.get("module")
    if not mod_name:
        raise InvalidActivity("a Python activity must have a module path")

    func = provider.get("func")
    if not func:
        raise InvalidActivity("a Python activity must have a function name")

    try:
        mod = importlib.import_module(mod_name)
    except ImportError:
        raise InvalidActivity("could not find Python module '{mod}' "
                              "in activity '{name}'".format(mod=mod_name,
                                                            name=name))

    found_func = False
    arguments = provider.get("arguments", {})
    candidates = set(inspect.getmembers(mod, inspect.isfunction)).union(
        inspect.getmembers(mod, inspect.isbuiltin))
    for (name, cb) in candidates:
        if name == func:
            found_func = True

            # let's try to bind the activity's arguments with the function
            # signature see if they match
            sig = inspect.signature(cb)
            try:
                # config and secrets are provided through specific parameters
                # to an activity that needs them. However, they are declared
                # out of band of the `arguments` mapping. Here, we simply
                # ensure the signature of the activity is valid by injecting
                # fake `configuration` and `secrets` arguments into the mapping
                args = arguments.copy()

                if "secrets" in sig.parameters:
                    args["secrets"] = None

                if "configuration" in sig.parameters:
                    args["configuration"] = None

                sig.bind(**args)
            except TypeError as x:
                # I dislike this sort of lookup but not sure we can
                # differentiate them otherwise
                msg = str(x)
                if "missing" in msg:
                    arg = msg.rsplit(":", 1)[1].strip()
                    raise InvalidActivity(
                        "required argument {arg} is missing from "
                        "activity '{name}'".format(arg=arg, name=name))
                elif "unexpected" in msg:
                    arg = msg.rsplit(" ", 1)[1].strip()
                    raise InvalidActivity(
                        "argument {arg} is not part of the "
                        "function signature in activity '{name}'".format(
                            arg=arg, name=name))
                else:
                    # another error? let's fail fast
                    raise
            break

    if not found_func:
        raise InvalidActivity(
            "'{mod}' does not expose '{func}' in activity '{name}'".format(
                mod=mod_name, func=func, name=name))
Exemple #17
0
def validate_control(control: Control) -> None:
    if "should-not-be-here" in control:
        raise InvalidActivity("invalid key on control")
Exemple #18
0
def ensure_experiment_is_valid(experiment: Experiment):
    """
    A chaos experiment consists of a method made of activities to carry
    sequentially.

    There are two kinds of activities:

    * probe: detecting the state of a resource in your system or external to it
      There are two kinds of probes: `steady` and `close`
    * action: an operation to apply against your system

    Usually, an experiment is made of a set of `steady` probes that ensure the
    system is sound to carry further the experiment. Then, an action before
    another set of of  ̀close` probes to sense the state of the system
    post-action.

    This function raises :exc:`InvalidExperiment`, :exc:`InvalidProbe` or
    :exc:`InvalidAction` depending on where it fails.
    """
    logger.info("Validating the experiment's syntax")

    if not experiment:
        raise InvalidExperiment("an empty experiment is not an experiment")

    if not experiment.get("title"):
        raise InvalidExperiment("experiment requires a title")

    if not experiment.get("description"):
        raise InvalidExperiment("experiment requires a description")

    tags = experiment.get("tags")
    if tags:
        if list(filter(lambda t: t == '' or not isinstance(t, str), tags)):
            raise InvalidExperiment(
                "experiment tags must be a non-empty string")

    validate_extensions(experiment)

    config = load_configuration(experiment.get("configuration", {}))
    load_secrets(experiment.get("secrets", {}), config)

    ensure_hypothesis_is_valid(experiment)

    method = experiment.get("method")
    if not method:
        raise InvalidExperiment("an experiment requires a method with "
                                "at least one activity")

    for activity in method:
        ensure_activity_is_valid(activity)

        # let's see if a ref is indeed found in the experiment
        ref = activity.get("ref")
        if ref and not lookup_activity(ref):
            raise InvalidActivity("referenced activity '{r}' could not be "
                                  "found in the experiment".format(r=ref))

    rollbacks = experiment.get("rollbacks", [])
    for activity in rollbacks:
        ensure_activity_is_valid(activity)

    warn_about_deprecated_features(experiment)

    validate_controls(experiment)

    logger.info("Experiment looks valid")