diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index f506ca2..26b4b77 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -95,10 +95,10 @@ jobs: - name: Install ansible-base (${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - # OPTIONAL If your unit test requires Python libraries from other collections - # Install them like this - # - name: Install collection dependencies - # run: ansible-galaxy collection install ansible.netcommon -p . + # OPTIONAL If your unit test requires Python libraries from other collections + # Install them like this + - name: Install collection dependencies + run: ansible-galaxy collection install ansible.utils -p . # Run the unit tests - name: Run unit test @@ -156,10 +156,16 @@ jobs: - name: Install ansible-base (${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check +# - name: Install collection dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt +# working-directory: ./ansible_collections/ansible/utils + # OPTIONAL If your integration test requires Python libraries or modules from other collections # Install them like this - # - name: Install collection dependencies - # run: ansible-galaxy collection install ansible.netcommon -p . + - name: Install collection dependencies + run: ansible-galaxy collection install ansible.utils -p . # Run the integration tests - name: Run integration test @@ -200,4 +206,4 @@ jobs: run: pip install black - name: Run black --check - run: black -l79 --diff --check ansible_collections/ansible/utils \ No newline at end of file + run: black -l79 --diff --check ansible_collections/ansible/utils diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 496bb38..2bde00a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,9 @@ +--- name: Publish on: release: types: - - created + - created jobs: publish: @@ -11,7 +12,7 @@ jobs: steps: - name: Check out code uses: actions/checkout@v2 - + - name: Set up Python uses: actions/setup-python@v2 with: @@ -31,5 +32,5 @@ jobs: # env: # ANSIBLE_GALAXY_API_KEY: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} # run: | - # ansible-galaxy collection build + # ansible-galaxy collection build # ansible-galaxy collection publish *.tar.gz --api-key $ANSIBLE_GALAXY_API_KEY diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..3adaf90 --- /dev/null +++ b/.yamllint @@ -0,0 +1,15 @@ +--- +extends: default + +ignore: | + .tox + changelogs/* + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + line-length: disable diff --git a/changelogs/fragments/add_fact_diff.yaml b/changelogs/fragments/add_fact_diff.yaml index 85fcd42..d91d589 100644 --- a/changelogs/fragments/add_fact_diff.yaml +++ b/changelogs/fragments/add_fact_diff.yaml @@ -1,3 +1,3 @@ --- -minor_changes: - - Add fact_diff module. Find the difference between text, files or facts \ No newline at end of file +major_changes: + - Added validate module/lookup/filter/test plugin to validate data based on given criteria diff --git a/changelogs/fragments/validate.yaml b/changelogs/fragments/validate.yaml new file mode 100644 index 0000000..e69de29 diff --git a/plugins/action/fact_diff.py b/plugins/action/fact_diff.py index 86a141b..444c33b 100644 --- a/plugins/action/fact_diff.py +++ b/plugins/action/fact_diff.py @@ -6,10 +6,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type + import re from importlib import import_module from ansible.plugins.action import ActionBase -from ansible.errors import AnsibleActionFail from ansible.module_utils._text import to_native from ansible_collections.ansible.utils.plugins.modules.fact_diff import ( DOCUMENTATION, diff --git a/plugins/action/update_fact.py b/plugins/action/update_fact.py index bbf108e..353b5a3 100644 --- a/plugins/action/update_fact.py +++ b/plugins/action/update_fact.py @@ -6,18 +6,17 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type + import ast -import json import re from ansible.plugins.action import ActionBase -from ansible.errors import AnsibleActionFail from ansible.module_utils.common._collections_compat import ( MutableMapping, MutableSequence, ) -from ansible.module_utils import basic -from ansible.module_utils._text import to_bytes, to_native + +from ansible.module_utils._text import to_native from jinja2 import Template, TemplateSyntaxError from ansible_collections.ansible.utils.plugins.modules.update_fact import ( DOCUMENTATION, @@ -40,9 +39,7 @@ class ActionModule(ActionBase): def _check_argspec(self): aav = AnsibleArgSpecValidator( - data=self._task.args, - schema=DOCUMENTATION, - name=self._task.action, + data=self._task.args, schema=DOCUMENTATION, name=self._task.action ) valid, errors, self._task.args = aav.validate() if not valid: diff --git a/plugins/action/validate.py b/plugins/action/validate.py new file mode 100644 index 0000000..91ccc83 --- /dev/null +++ b/plugins/action/validate.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The action plugin file for validate +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible.errors import AnsibleActionFail, AnsibleError +from ansible.module_utils._text import to_text +from ansible.plugins.action import ActionBase + +from ansible_collections.ansible.utils.plugins.modules.validate import ( + DOCUMENTATION, +) +from ansible_collections.ansible.utils.plugins.module_utils.validate.base import ( + load_validator, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + +ARGSPEC_CONDITIONALS = {} + + +class ActionModule(ActionBase): + """action module""" + + VALIDATE_CLS_NAME = "Validate" + + def __init__(self, *args, **kwargs): + super(ActionModule, self).__init__(*args, **kwargs) + self._validator_name = None + self._result = {} + + def _debug(self, name, msg): + """Output text using ansible's display + + :param msg: The message + :type msg: str + """ + msg = "<{phost}> {name} {msg}".format( + phost=self._playhost, name=name, msg=msg + ) + self._display.vvvv(msg) + + def run(self, tmp=None, task_vars=None): + """The std execution entry pt for an action plugin + + :param tmp: no longer used + :type tmp: none + :param task_vars: The vars provided when the task is run + :type task_vars: dict + :return: The results from the parser + :rtype: dict + """ + valid, argspec_result, updated_params = check_argspec( + DOCUMENTATION, + "validate module", + schema_conditionals=ARGSPEC_CONDITIONALS, + **self._task.args + ) + if not valid: + return argspec_result + + self._task_vars = task_vars + self._playhost = task_vars.get("inventory_hostname") if task_vars else None + + self._validator_engine, validator_result = load_validator( + engine=updated_params["engine"], + data=updated_params["data"], + criteria=updated_params["criteria"], + plugin_vars=task_vars, + ) + if validator_result.get("failed"): + return validator_result + + try: + result = self._validator_engine.validate() + except AnsibleError as exc: + raise AnsibleActionFail( + to_text(exc, errors="surrogate_then_replace") + ) + except Exception as exc: + raise AnsibleActionFail( + "Unhandled exception from validator '{validator}'. Error: {err}".format( + validator=self._validator_engine, + err=to_text(exc, errors="surrogate_then_replace"), + ) + ) + + if result.get("errors"): + self._result["errors"] = result["errors"] + self._result.update({"failed": True}) + if "msg" in result: + self._result["msg"] = ( + "Validation errors were found.\n" + result["msg"] + ) + else: + self._result["msg"] = "Validation errors were found." + else: + self._result["msg"] = "all checks passed" + return self._result diff --git a/plugins/fact_diff/native.py b/plugins/fact_diff/native.py index d841892..ea9c070 100644 --- a/plugins/fact_diff/native.py +++ b/plugins/fact_diff/native.py @@ -36,14 +36,18 @@ class FactDiff(FactDiffBase): self._debug("'after' is a string, splitting lines") self._after = self._after.splitlines() self._before = [ - l - for l in self._before - if not any(regex.match(str(l)) for regex in self._skip_lines) + line + for line in self._before + if not any( + regex.match(str(line)) for regex in self._skip_lines + ) ] self._after = [ - l - for l in self._after - if not any(regex.match(str(l)) for regex in self._skip_lines) + line + for line in self._after + if not any( + regex.match(str(line)) for regex in self._skip_lines + ) ] if isinstance(self._before, list): self._debug("'before' is a list, joining with \n") diff --git a/plugins/filter/get_path.py b/plugins/filter/get_path.py index 6eb8e70..6fd05a4 100644 --- a/plugins/filter/get_path.py +++ b/plugins/filter/get_path.py @@ -167,9 +167,7 @@ def _get_path(*args, **kwargs): data.update(kwargs) environment = data.pop("environment") aav = AnsibleArgSpecValidator( - data=data, - schema=DOCUMENTATION, - name="get_path", + data=data, schema=DOCUMENTATION, name="get_path" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/filter/index_of.py b/plugins/filter/index_of.py index 0a7aeee..b8bc93b 100644 --- a/plugins/filter/index_of.py +++ b/plugins/filter/index_of.py @@ -321,9 +321,7 @@ def _index_of(*args, **kwargs): data.update(kwargs) environment = data.pop("environment") aav = AnsibleArgSpecValidator( - data=data, - schema=DOCUMENTATION, - name="index_of", + data=data, schema=DOCUMENTATION, name="index_of" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/filter/to_paths.py b/plugins/filter/to_paths.py index a266b05..06cd667 100644 --- a/plugins/filter/to_paths.py +++ b/plugins/filter/to_paths.py @@ -132,9 +132,7 @@ def _to_paths(*args, **kwargs): data = dict(zip(keys, args)) data.update(kwargs) aav = AnsibleArgSpecValidator( - data=data, - schema=DOCUMENTATION, - name="to_paths", + data=data, schema=DOCUMENTATION, name="to_paths" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/filter/validate.py b/plugins/filter/validate.py new file mode 100644 index 0000000..1974d96 --- /dev/null +++ b/plugins/filter/validate.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + filter: validate + author: Ganesh Nalawade (@ganeshrn) + version_added: "1.0.0" + short_description: Validate data with provided criteria + description: + - Validate C(data) with provided C(criteria) based on the validation C(engine). + options: + data: + type: raw + description: + - A data that will be validated against C(criteria). + - This option represents the value that is passed to filter plugin in pipe format. + For example I(config_data|ansible.utils.validate()), in this case I(config_data) + represents this option. + - For the type of C(data) that represents this value refer documentation of individual validator plugins. + required: True + criteria: + type: raw + description: + - The criteria used for validation of value that represents C(data) options. + - This option represents the first argument passed in the filter plugin + For example I(config_data|ansible.utils.validate(config_criteria)), in + this case the value of I(config_criteria) represents this option. + - For the type of C(criteria) that represents this value refer documentation of individual validator plugins. + required: True + engine: + type: str + description: + - The name of the validator plugin to use. + - This option can be passed in lookup plugin as a key, value pair + For example I(config_data|ansible.utils.validate(config_criteria, engine='ansible.utils.jsonschema')), in + this case the value I(ansible.utils.jsonschema) represents the engine to be use for data valdiation. + If the value is not provided the default value that is I(ansible.uitls.jsonschema) will be used. + - The value should be in fully qualified collection name format that is + I(..). + default: ansible.utils.jsonschema + notes: + - For the type of options C(data) and C(criteria) refer the individual C(validate) plugin + documentation that is represented in the value of C(engine) option. + - For additional plugin configuration options refer the individual C(validate) plugin + documentation that is represented by the value of C(engine) option. + - The plugin configuration option can be either passed as I(key=value) pairs within filter plugin + or environment variables. + - The precedence of the C(validate) plugin configurable option is the variable passed within filter plugin + as I(key=value) pairs followed by the environment variables. +""" + +EXAMPLES = r""" +- name: set facts for data and criteria + set_fact: + data: "{{ lookup('file', './validate/data/show_interfaces_iosxr.json')}}" + criteria: "{{ lookup('file', './validate/criteria/jsonschema/show_interfaces_iosxr.json')}}" + +- name: validate data in json format using jsonschema with by passing plugin configuration variable as key/value pairs + ansible.builtin.set_fact: + data_validilty: "{{ data|ansible.utils.validate(criteria, engine='ansible.utils.jsonschema', draft='draft7') }}" +""" + +RETURN = """ + _raw: + description: + - If data is valid returns empty list + - If data is invalid returns list of errors in data +""" + +from ansible.errors import AnsibleError, AnsibleFilterError +from ansible.module_utils._text import to_text +from ansible_collections.ansible.utils.plugins.module_utils.validate.base import ( + load_validator, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( + to_list, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + + +ARGSPEC_CONDITIONALS = {} + + +def validate(*args, **kwargs): + if len(args) < 2: + raise AnsibleFilterError( + "Missing either 'data' or 'criteria' value in filter input," + " refer 'ansible.utils.validate' filter plugin documentation for details" + ) + + params = {"data": args[0], "criteria": args[1]} + if kwargs.get("engine"): + params.update({"engine": kwargs["engine"]}) + + valid, argspec_result, updated_params = check_argspec( + DOCUMENTATION, + "validate filter", + schema_conditionals=ARGSPEC_CONDITIONALS, + **params + ) + if not valid: + raise AnsibleFilterError( + "{argspec_result} with errors: {argspec_errors}".format( + argspec_result=argspec_result.get("msg"), + argspec_errors=argspec_result.get("errors"), + ) + ) + + validator_engine, validator_result = load_validator( + engine=updated_params["engine"], + data=updated_params["data"], + criteria=updated_params["criteria"], + kwargs=kwargs, + ) + if validator_result.get("failed"): + raise AnsibleFilterError( + "validate lookup plugin failed with errors: {msg}".format( + msg=validator_result.get("msg") + ) + ) + + try: + result = validator_engine.validate() + except AnsibleError as exc: + raise AnsibleFilterError(to_text(exc, errors="surrogate_then_replace")) + except Exception as exc: + raise AnsibleFilterError( + "Unhandled exception from validator '{validator}'. Error: {err}".format( + validator=updated_params["engine"], + err=to_text(exc, errors="surrogate_then_replace"), + ) + ) + + return to_list(result.get("errors", [])) + + +class FilterModule(object): + """ index_of """ + + def filters(self): + """a mapping of filter names to functions""" + return {"validate": validate} diff --git a/plugins/lookup/get_path.py b/plugins/lookup/get_path.py index caa576f..0f6ba91 100644 --- a/plugins/lookup/get_path.py +++ b/plugins/lookup/get_path.py @@ -171,9 +171,7 @@ class LookupModule(LookupBase): terms = dict(zip(keys, terms)) terms.update(kwargs) aav = AnsibleArgSpecValidator( - data=terms, - schema=DOCUMENTATION, - name="get_path", + data=terms, schema=DOCUMENTATION, name="get_path" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/lookup/index_of.py b/plugins/lookup/index_of.py index 0441239..c4bb4a7 100644 --- a/plugins/lookup/index_of.py +++ b/plugins/lookup/index_of.py @@ -338,9 +338,7 @@ class LookupModule(LookupBase): terms = dict(zip(keys, terms)) terms.update(kwargs) aav = AnsibleArgSpecValidator( - data=terms, - schema=DOCUMENTATION, - name="index_of", + data=terms, schema=DOCUMENTATION, name="index_of" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/lookup/to_paths.py b/plugins/lookup/to_paths.py index 3e62712..ad791ea 100644 --- a/plugins/lookup/to_paths.py +++ b/plugins/lookup/to_paths.py @@ -142,9 +142,7 @@ class LookupModule(LookupBase): terms = dict(zip(keys, terms)) terms.update(kwargs) aav = AnsibleArgSpecValidator( - data=terms, - schema=DOCUMENTATION, - name="to_paths", + data=terms, schema=DOCUMENTATION, name="to_paths" ) valid, errors, updated_data = aav.validate() if not valid: diff --git a/plugins/lookup/validate.py b/plugins/lookup/validate.py new file mode 100644 index 0000000..f60948a --- /dev/null +++ b/plugins/lookup/validate.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + lookup: validate + author: Ganesh Nalawade (@ganeshrn) + version_added: "1.0.0" + short_description: Validate data with provided criteria + description: + - Validate C(data) with provided C(criteria) based on the validation C(engine). + options: + data: + type: raw + description: + - A data that will be validated against C(criteria). + - This option represents the value that is passed to lookup plugin as first argument. + For example I(lookup(config_data, config_criteria, engine='ansible.utils.jsonschema')), + in this case I(config_data) represents this option. + - For the type of C(data) that represents this value refer documentation of individual validate plugins. + required: True + criteria: + type: raw + description: + - The criteria used for validation of value that represents C(data) options. + - This option represents the second argument passed in the lookup plugin + For example I(lookup(config_data, config_criteria, engine='ansible.utils.jsonschema')), + in this case the value of I(config_criteria) represents this option. + - For the type of C(criteria) that represents this value refer documentation of individual + validate plugins. + required: True + engine: + type: str + description: + - The name of the validate plugin to use. + - This option can be passed in lookup plugin as a key, value pair + For example I(lookup(config_data, config_criteria, engine='ansible.utils.jsonschema')), in + this case the value I(ansible.utils.jsonschema) represents the engine to be use for data valdiation. + If the value is not provided the default value that is I(ansible.uitls.jsonschema) will be used. + - The value should be in fully qualified collection name format that is + I(..). + default: ansible.utils.jsonschema + notes: + - For the type of options C(data) and C(criteria) refer the individual C(validate) plugin + documentation that is represented in the value of C(engine) option. + - For additional plugin configuration options refer the individual C(validate) plugin + documentation that is represented by the value of C(engine) option. + - The plugin configuration option can be either passed as I(key=value) pairs within lookup plugin + or task or environment variables. + - The precedence the C(validate) plugin configurable option is the variable passed within lookup plugin + as I(key=value) pairs followed by task variables followed by environment variables. +""" + +EXAMPLES = r""" +- name: set facts for data and criteria + set_fact: + data: "{{ lookup('file', './validate/data/show_interfaces_iosxr.json')}}" + criteria: "{{ lookup('file', './validate/criteria/jsonschema/show_interfaces_iosxr.json')}}" + +- name: validate data in json format using jsonschema with lookup plugin by passing plugin configuration variable as key/value pairs + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup(data, criteria, engine='ansible.utils.jsonschema', draft='draft7') }}" + +- name: validate data in json format using jsonschema with lookup plugin by passing plugin configuration variable as task variable + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', data, criteria, engine='ansible.utils.jsonschema', draft='draft7') }}" + vars: + ansible_validate_jsonschema_draft: draft3 +""" + +RETURN = """ + _raw: + description: + - If data is valid returns empty list + - If data is invalid returns list of errors in data +""" + +from ansible.errors import AnsibleError, AnsibleLookupError +from ansible.module_utils._text import to_text +from ansible.plugins.lookup import LookupBase +from ansible_collections.ansible.utils.plugins.module_utils.validate.base import ( + load_validator, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( + to_list, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + + +ARGSPEC_CONDITIONALS = {} + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + if len(terms) < 2: + raise AnsibleLookupError( + "missing either 'data' or 'criteria' value in lookup input," + " refer ansible.utils.validate lookup plugin documentation for details" + ) + + params = {"data": terms[0], "criteria": terms[1]} + if kwargs.get("engine"): + params.update({"engine": kwargs["engine"]}) + + valid, argspec_result, updated_params = check_argspec( + DOCUMENTATION, + "validate lookup", + schema_conditionals=ARGSPEC_CONDITIONALS, + **params + ) + if not valid: + raise AnsibleLookupError( + "{argspec_result} with errors: {argspec_errors}".format( + argspec_result=argspec_result.get("msg"), + argspec_errors=argspec_result.get("errors"), + ) + ) + + validator_engine, validator_result = load_validator( + engine=updated_params["engine"], + data=updated_params["data"], + criteria=updated_params["criteria"], + plugin_vars=variables, + kwargs=kwargs, + ) + if validator_result.get("failed"): + raise AnsibleLookupError( + "validate lookup plugin failed with errors: {validator_result}".format( + validator_result=validator_result.get("msg") + ) + ) + + try: + result = validator_engine.validate() + except AnsibleError as exc: + raise AnsibleLookupError( + to_text(exc, errors="surrogate_then_replace") + ) + except Exception as exc: + raise AnsibleLookupError( + "Unhandled exception from validator '{validator}'. Error: {err}".format( + validator=updated_params["engine"], + err=to_text(exc, errors="surrogate_then_replace"), + ) + ) + + return to_list(result.get("errors", [])) diff --git a/plugins/module_utils/common/argspec_validate.py b/plugins/module_utils/common/argspec_validate.py index cc60f71..b71e3fd 100644 --- a/plugins/module_utils/common/argspec_validate.py +++ b/plugins/module_utils/common/argspec_validate.py @@ -23,14 +23,12 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import json import re from ansible.module_utils.basic import AnsibleModule from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( dict_merge, ) -from ansible.module_utils.six import iteritems, string_types -from ansible.module_utils._text import to_bytes +from ansible.module_utils.six import iteritems try: import yaml @@ -48,7 +46,9 @@ except ImportError: # ansible-base 2.11 should expose argspec validation outside of the # ansiblemodule class try: - from ansible.module_utils.somefile import FutureBaseArgspecValidator + from ansible.module_utils.somefile import ( # noqa: F401 + FutureBaseArgspecValidator, + ) HAS_ANSIBLE_ARG_SPEC_VALIDATOR = True except ImportError: @@ -107,9 +107,7 @@ class MonkeyModule(AnsibleModule): """ if self.name: msg = re.sub( - r"\(basic\.pyc?\)", - "'{name}'".format(name=self.name), - msg, + r"\(basic\.pyc?\)", "'{name}'".format(name=self.name), msg ) self._valid = False self._errors = msg @@ -243,3 +241,28 @@ class AnsibleArgSpecValidator: return self._validate() else: return self._validate() + + +def check_argspec( + schema, name, schema_format="doc", schema_conditionals=None, **args +): + if schema_conditionals is None: + schema_conditionals = {} + + aav = AnsibleArgSpecValidator( + data=args, + schema=schema, + schema_format=schema_format, + schema_conditionals=schema_conditionals, + name=name, + ) + result = {} + valid, errors, updated_params = aav.validate() + if not valid: + result["errors"] = errors + result["failed"] = True + result["msg"] = "argspec validation failed for {name} plugin".format( + name=name + ) + + return valid, result, updated_params diff --git a/plugins/module_utils/common/index_of.py b/plugins/module_utils/common/index_of.py index 3d2858c..331e2e1 100644 --- a/plugins/module_utils/common/index_of.py +++ b/plugins/module_utils/common/index_of.py @@ -12,7 +12,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import json -from ansible.module_utils.common.collections import is_sequence + from ansible.module_utils.six import string_types, integer_types from ansible.module_utils._text import to_native diff --git a/plugins/module_utils/common/utils.py b/plugins/module_utils/common/utils.py index a69dabe..681bf77 100644 --- a/plugins/module_utils/common/utils.py +++ b/plugins/module_utils/common/utils.py @@ -8,10 +8,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from itertools import chain from ansible.module_utils.common._collections_compat import Mapping -from ansible.module_utils.six import iteritems, string_types +from ansible.module_utils.six import iteritems def sort_list(val): @@ -102,3 +101,12 @@ def dict_merge(base, other): combined[key] = other.get(key) return combined + + +def to_list(val): + if isinstance(val, (list, tuple, set)): + return list(val) + elif val is not None: + return [val] + else: + return list() diff --git a/plugins/module_utils/validate/base.py b/plugins/module_utils/validate/base.py new file mode 100644 index 0000000..b80b53b --- /dev/null +++ b/plugins/module_utils/validate/base.py @@ -0,0 +1,66 @@ +""" +The base class for validator +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +try: + from importlib import import_module +except ImportError: + pass + +from ansible.module_utils._text import to_native + + +def load_validator( + engine, data, criteria, plugin_vars=None, cls_name="Validate", kwargs=None +): + """ + Load the validate plugin from engine name + :param engine: Name of the validate engine in format .. + :param vars: Variables for validate plugins. The variable information for each validate plugins can + be referred in individual plugin documentation. + :param cls_name: Base class name for validate plugin. Defaults to ``Validate``. + :param kwargs: The base name of the class for validate plugin + :return: + """ + result = {} + if plugin_vars is None: + plugin_vars = {} + + if kwargs is None: + kwargs = {} + + if len(engine.split(".")) != 3: + result["failed"] = True + result[ + "msg" + ] = "Parser name should be provided as a full name including collection" + return None, result + + cref = dict(zip(["corg", "cname", "plugin"], engine.split("."))) + validatorlib = ( + "ansible_collections.{corg}.{cname}.plugins.validate.{plugin}".format( + **cref + ) + ) + + try: + validatorcls = getattr(import_module(validatorlib), cls_name) + validator = validatorcls( + data=data, + criteria=criteria, + engine=engine, + plugin_vars=plugin_vars, + kwargs=kwargs, + ) + return validator, result + except Exception as exc: + result["failed"] = True + result[ + "msg" + ] = "For engine '{engine}' error loading the corresponding validate plugin: {err}".format( + engine=engine, err=to_native(exc) + ) + return None, result diff --git a/plugins/modules/validate.py b/plugins/modules/validate.py new file mode 100644 index 0000000..f3e1fc7 --- /dev/null +++ b/plugins/modules/validate.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +module: validate +author: +- Bradley Thornton (@cidrblock) +- Ganesh Nalawade (@ganeshrn) +short_description: Validate data with provided criteria +description: +- Validate data with provided criteria based on the validation engine. +version_added: 1.0.0 +options: + data: + type: raw + description: + - A data that will be validated against C(criteria). For the type of data refer + documentation of individual validate plugins + required: True + engine: + type: str + description: + - The name of the validate plugin to use. The engine value should follow + the fully qualified collection name format that is + ... + default: ansible.utils.jsonschema + criteria: + type: raw + description: + - The criteria used for validation of C(data). For the type of criteria refer + documentation of individual validate plugins. + required: True +notes: +- For the type of options C(data) and C(criteria) refer the individual C(validate) plugin + documentation that is represented in the value of C(engine) option. +- For additional plugin configuration options refer the individual C(validate) plugin + documentation that is represented by the value of C(engine) option. +- The plugin configuration option can be either passed as task or environment variables. +- The precedence the C(validate) plugin configurable option is task variables followed + by the environment variables. +""" + +EXAMPLES = r""" +- name: set facts for data and criteria + set_fact: + data: "{{ lookup('file', './validate/data/show_interfaces_iosxr.json')}}" + criteria: "{{ lookup('file', './validate/criteria/jsonschema/show_interfaces_iosxr.json')}}" + +- name: validate data in with jsonschema engine (by passing task vars as configurable plugin options) + ansible.utils.validate: + data: "{{ data }}" + criteria: "{{ criteria }}" + engine: ansible.utils.jsonschema + vars: + ansible_jsonschema_draft: draft7 +""" + +RETURN = r""" +msg: + description: + - The msg indicates if the C(data) is valid as per the C(criteria). + - In case data is valid return success message I(all checks passed) + - In case data is invalid return error message I(Validation errors were found) + along with more information on error is available. + returned: always + type: str +errors: + description: The list of errors in C(data) based on the C(criteria). + returned: when C(data) value is invalid + type: list +""" diff --git a/plugins/test/validate.py b/plugins/test/validate.py new file mode 100644 index 0000000..8ff73af --- /dev/null +++ b/plugins/test/validate.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +DOCUMENTATION = """ + test: validate + author: Ganesh Nalawade (@ganeshrn) + version_added: "1.0.0" + short_description: Validate data with provided criteria + description: + - Validate C(data) with provided C(criteria) based on the validation C(engine). + options: + data: + type: raw + description: + - A data that will be validated against C(criteria). + - This option represents the value that is passed to test plugin as check. + For example I(config_data is ansible.utils.validate(criteria=criteria), in this case I(config_data) + represents this option. + - For the type of C(data) that represents this value refer documentation of individual validate plugins. + required: True + criteria: + type: raw + description: + - The criteria used for validation of value that represents C(data) options. + - This option is passed to the test plugin as key, value pair + For example I(config_data is ansible.utils.validate(criteria=criteria)), in + this case the value of I(criteria) key represents this criteria for data validation. + - For the type of C(criteria) that represents this value refer documentation of individual validate plugins. + required: True + engine: + type: str + description: + - The name of the validate plugin to use. + - This option can be passed in test plugin as a key, value pair + For example I(config_data is ansible.utils.validate(engine='ansible.utils.jsonschema', criteria=criteria)), in + this case the value of I(engine) key represents the engine to be use for data valdiation. + If the value is not provided the default value that is I(ansible.uitls.jsonschema) will be used. + - The value should be in fully qualified collection name format that is + I(..). + default: ansible.utils.jsonschema + notes: + - For the type of options C(data) and C(criteria) refer the individual C(validate) plugin + documentation that is represented in the value of C(engine) option. + - For additional plugin configuration options refer the individual C(validate) plugin + documentation that is represented by the value of C(engine) option. + - The plugin configuration option can be either passed as I(key=value) pairs within test plugin + or set as environment variables. + - The precedence the C(validate) plugin configurable option is the variable passed within test plugin + as I(key=value) pairs followed by task variables followed by environment variables. +""" + +EXAMPLES = r""" +- name: set facts for data and criteria + set_fact: + data: "{{ lookup('file', './validate/data/show_interfaces_iosxr.json')}}" + criteria: "{{ lookup('file', './validate/criteria/jsonschema/show_interfaces_iosxr.json')}}" + +- name: validate data in json format using jsonschema with test plugin + ansible.builtin.set_fact: + is_data_valid: "{{ data is ansible.utils.validate(engine='ansible.utils.jsonschema', criteria=criteria, draft='draft7') }}" +""" + +RETURN = """ + _raw: + description: + - If data is valid return C(true) + - If data is invalid return C(false) +""" + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_text +from ansible_collections.ansible.utils.plugins.module_utils.validate.base import ( + load_validator, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( + to_list, +) +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + +ARGSPEC_CONDITIONALS = {} + + +def validate(*args, **kwargs): + if not len(args): + raise AnsibleError( + "Missing either 'data' value in test plugin input," + "refer ansible.utils.validate test plugin documentation for details" + ) + + params = {"data": args[0]} + + for item in ["engine", "criteria"]: + if kwargs.get(item): + params.update({item: kwargs[item]}) + + valid, argspec_result, updated_params = check_argspec( + DOCUMENTATION, + "validate test", + schema_conditionals=ARGSPEC_CONDITIONALS, + **params + ) + if not valid: + raise AnsibleError( + "{argspec_result} with errors: {argspec_errors}".format( + argspec_result=argspec_result.get("msg"), + argspec_errors=argspec_result.get("errors"), + ) + ) + + validator_engine, validator_result = load_validator( + engine=updated_params["engine"], + data=updated_params["data"], + criteria=updated_params["criteria"], + kwargs=kwargs, + ) + if validator_result.get("failed"): + raise AnsibleError( + "validate lookup plugin failed with errors: %s" + % validator_result.get("msg") + ) + + try: + result = validator_engine.validate() + except AnsibleError as exc: + raise AnsibleError(to_text(exc, errors="surrogate_then_replace")) + except Exception as exc: + raise AnsibleError( + "Unhandled exception from validator '{validator}'. Error: {err}".format( + validator=updated_params["engine"], + err=to_text(exc, errors="surrogate_then_replace"), + ) + ) + + errors = to_list(result.get("errors", [])) + if len(errors): + return False + + return True + + +class TestModule(object): + """data validation test""" + + test_map = {"validate": validate} + + def tests(self): + return self.test_map diff --git a/plugins/validate/_base.py b/plugins/validate/_base.py new file mode 100644 index 0000000..f52ad1a --- /dev/null +++ b/plugins/validate/_base.py @@ -0,0 +1,140 @@ +""" +The base class for validator +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import os + +from importlib import import_module + +from ansible.errors import AnsibleError +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_text + +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + +from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( + to_list, +) + +try: + import yaml + + try: + from yaml import CSafeLoader as SafeLoader + except ImportError: + from yaml import SafeLoader + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ValidateBase(object): + """The base class for data validators + Provides a _debug function to normalize debug output + """ + + def __init__(self, data, criteria, engine, plugin_vars=None, kwargs=None): + self._data = data + self._criteria = criteria + self._engine = engine + self._plugin_vars = plugin_vars if plugin_vars is not None else {} + self._result = {} + self._kwargs = kwargs if kwargs is not None else {} + self._sub_plugin_options = {} + + cref = dict(zip(["corg", "cname", "plugin"], engine.split("."))) + validatorlib = "ansible_collections.{corg}.{cname}.plugins.validate.{plugin}".format( + **cref + ) + + validatordoc = getattr(import_module(validatorlib), "DOCUMENTATION") + if validatordoc: + self._set_sub_plugin_options(validatordoc) + + def _set_sub_plugin_options(self, doc): + params = {} + try: + argspec_obj = yaml.load(doc, SafeLoader) + except Exception as exc: + raise AnsibleError( + "Error '{err}' while reading validate plugin {engine} documentation: '{argspec}'".format( + err=to_text(exc, errors="surrogate_or_strict"), + engine=self._engine, + argspec=doc, + ) + ) + options = argspec_obj.get("options", {}) + + if not options: + return None + + for option_name, option_value in iteritems(options): + + option_var_name_list = option_value.get("vars", []) + option_env_name_list = option_value.get("env", []) + + # check if plugin configuration option passed as kwargs + # valid for lookup, filter, test plugins or pass through + # variables if supported by the module. + if option_name in self._kwargs: + params[option_name] = self._kwargs[option_name] + continue + + # check if plugin configuration option passed in task vars eg. + # vars: + # - name: ansible_validate_jsonschema_draft + # - name: ansible_validate_jsonschema_draft_type + if option_var_name_list and (option_name not in params): + for var_name_entry in to_list(option_var_name_list): + if not isinstance(var_name_entry, dict): + raise AnsibleError( + "invalid type '{var_name_type}' for the value of '{var_name_entry}' option," + " should to be type dict".format( + var_name_type=type(var_name_entry), + var_name_entry=var_name_entry, + ) + ) + var_name = var_name_entry.get("name") + if var_name and var_name in self._plugin_vars: + params[option_name] = self._plugin_vars[var_name] + break + + # check if plugin configuration option as passed as enviornment eg. + # env: + # - name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT + if option_env_name_list and (option_name not in params): + for env_name_entry in to_list(option_env_name_list): + if not isinstance(env_name_entry, dict): + raise AnsibleError( + "invalid type '{env_name_entry_type}' for the value of '{env_name_entry}' option," + " should to be type dict".format( + env_name_entry_type=type(env_name_entry), + env_name_entry=env_name_entry, + ) + ) + env_name = env_name_entry.get("name") + if env_name in os.environ: + params[option_name] = os.environ[env_name] + break + + valid, argspec_result, updated_params = check_argspec( + yaml.dump(argspec_obj), self._engine, **params + ) + if not valid: + raise AnsibleError( + "{argspec_result} with errors: {argspec_errors}".format( + argspec_result=argspec_result.get("msg"), + argspec_errors=argspec_result.get("errors"), + ) + ) + + if updated_params: + self._sub_plugin_options = updated_params + + def _get_sub_plugin_options(self, name): + return self._sub_plugin_options.get(name) diff --git a/plugins/validate/jsonschema.py b/plugins/validate/jsonschema.py new file mode 100644 index 0000000..458037b --- /dev/null +++ b/plugins/validate/jsonschema.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + author: Ganesh Nalawade (@ganeshrn) + validate: jsonschema + short_description: Define configurable options for jsonschema validate plugin + description: + - This plugin documentation provides the configurable options that can be passed + to the validate plugins when I(ansible.utils.json) is used as a value for + engine option. + version_added: 1.0.0 + options: + draft: + description: + - This option provides the jsonschema specification that should be used + for the validating the data. The C(criteria) option in the C(validate) + plugin should follow the specifiaction as mentined by this option + default: draft7 + choices: + - draft3 + - draft4 + - draft6 + - draft7 + env: + - name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT + vars: + - name: ansible_validate_jsonschema_draft + notes: + - The value of C(data) option should be either of type I(dict) or I(strings) which should be + a valid I(dict) when read in python. + - The value of C(criteria) should be I(list) of I(dict) or I(list) of I(strings) and each + I(string) within the I(list) entry should be a valid I(dict) when read in python. +""" + +import json + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import missing_required_lib +from ansible.errors import AnsibleError +from ansible.module_utils.six import string_types + +from ansible_collections.ansible.utils.plugins.validate._base import ValidateBase + +from ansible_collections.ansible.utils.plugins.module_utils.common.utils import ( + to_list, +) + +try: + import jsonschema + + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + + +def to_path(fpath): + return ".".join(str(index) for index in fpath) + + +def json_path(absolute_path): + path = "$" + for elem in absolute_path: + if isinstance(elem, int): + path += "[" + str(elem) + "]" + else: + path += "." + elem + return path + + +class Validate(ValidateBase): + @staticmethod + def _check_reqs(): + """Check the prerequisites are installed for jsonschema + + :return None: In case all prerequisites are satisfised + """ + if not HAS_JSONSCHEMA: + raise AnsibleError(missing_required_lib("jsonschema")) + + def _check_args(self): + """Ensure specific args are set + + :return: None: In case all arguments passed are valid + """ + try: + if isinstance(self._data, dict): + self._data = json.loads(json.dumps(self._data)) + elif isinstance(self._data, string_types): + self._data = json.loads(self._data) + else: + msg = "Expected value of 'data' option is either dict or str, received type '{data_type}'".format( + data_type=type(self._data) + ) + raise AnsibleError(msg) + + except (TypeError, json.decoder.JSONDecodeError) as exe: + msg = ( + "'data' option value is invalid, value should of type dict or str format of dict." + " Failed to read with error '{err}'".format( + err=to_text(exe, errors="surrogate_then_replace") + ) + ) + raise AnsibleError(msg) + + try: + criteria = [] + for item in to_list(self._criteria): + if isinstance(item, dict): + criteria.append(json.loads(json.dumps(item))) + elif isinstance(self._criteria, string_types): + criteria.append(json.loads(item)) + else: + msg = "Expected value of 'criteria' option is either list of dict/str or dict or str, received type '{criteria_type}'".format( + criteria_type=type(criteria) + ) + raise AnsibleError(msg) + + self._criteria = criteria + except (TypeError, json.decoder.JSONDecodeError) as exe: + msg = ( + "'criteria' option value is invalid, value should of type dict or str format of dict." + " Failed to read with error '{err}'".format( + err=to_text(exe, errors="surrogate_then_replace") + ) + ) + raise AnsibleError(msg) + + def validate(self): + """Std entry point for a validate execution + + :return: Errors or parsed text as structured data + :rtype: dict + + :example: + + The parse function of a parser should return a dict: + {"errors": [a list of errors]} + or + {"parsed": obj} + """ + self._check_reqs() + self._check_args() + + try: + self._validate_jsonschema() + except Exception as exc: + return {"errors": to_text(exc, errors="surrogate_then_replace")} + + return self._result + + def _validate_jsonschema(self): + error_messages = None + + draft = self._get_sub_plugin_options("draft") + error_messages = [] + + for criteria in self._criteria: + if draft == "draft3": + validator = jsonschema.Draft3Validator(criteria) + elif draft == "draft4": + validator = jsonschema.Draft4Validator(criteria) + elif draft == "draft6": + validator = jsonschema.Draft6Validator(criteria) + else: + validator = jsonschema.Draft7Validator(criteria) + + validation_errors = sorted( + validator.iter_errors(self._data), key=lambda e: e.path + ) + + if validation_errors: + if "errors" not in self._result: + self._result["errors"] = [] + + for validation_error in validation_errors: + if isinstance( + validation_error, jsonschema.ValidationError + ): + error = { + "message": validation_error.message, + "data_path": to_path( + validation_error.absolute_path + ), + "json_path": json_path( + validation_error.absolute_path + ), + "schema_path": to_path( + validation_error.relative_schema_path + ), + "relative_schema": validation_error.schema, + "expected": validation_error.validator_value, + "validator": validation_error.validator, + "found": validation_error.instance, + } + self._result["errors"].append(error) + error_message = ( + "At '{schema_path}' {message}. ".format( + schema_path=error["schema_path"], + message=error["message"], + ) + ) + error_messages.append(error_message) + if error_messages: + if "msg" not in self._result: + self._result["msg"] = "\n".join(error_messages) + else: + self._result["msg"] += "\n".join(error_messages) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7eafef4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +ansible +# The follow are 3rd party libs for validate +jsonschema +# /valiate diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..309ed02 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,9 @@ +black==19.3b0 ; python_version > '3.5' +coverage==4.5.4 +flake8 +mock ; python_version < '3.5' +pytest-xdist +yamllint +# The follow are 3rd party libs for valiate +jsonschema +# /valiate diff --git a/tests/integration/targets/fact_diff/tasks/include/argspec.yaml b/tests/integration/targets/fact_diff/tasks/include/argspec.yaml index 2ab89e7..6b72b21 100644 --- a/tests/integration/targets/fact_diff/tasks/include/argspec.yaml +++ b/tests/integration/targets/fact_diff/tasks/include/argspec.yaml @@ -1,14 +1,15 @@ -- name: Check argspec validation +--- +- name: Check argspec validation ansible.utils.fact_diff: - ignore_errors: True + ignore_errors: true register: result - assert: that: "{{ string in result.msg }}" loop: - - "missing required arguments:" - - before - - after + - "missing required arguments:" + - before + - after loop_control: loop_var: string @@ -19,9 +20,9 @@ plugin: vars: skip_lines: - a_dict: False - ignore_errors: True + a_dict: false + ignore_errors: true register: result - assert: - that: "{{ 'unable to convert to list' in result.msg }}" \ No newline at end of file + that: "{{ 'unable to convert to list' in result.msg }}" diff --git a/tests/integration/targets/fact_diff/tasks/include/examples.yaml b/tests/integration/targets/fact_diff/tasks/include/examples.yaml index 8351ebf..2dc7938 100644 --- a/tests/integration/targets/fact_diff/tasks/include/examples.yaml +++ b/tests/integration/targets/fact_diff/tasks/include/examples.yaml @@ -1,18 +1,19 @@ +--- - set_fact: before: a: b: c: d: - - 0 - - 1 + - 0 + - 1 after: a: b: c: d: - - 2 - - 3 + - 2 + - 3 - name: Show the difference in json format ansible.utils.fact_diff: @@ -111,7 +112,7 @@ # ansible.utils.fact_diff: # before: "{{ pre.response|ansible.utils.to_paths }}" # after: "{{ post.response|ansible.utils.to_paths }}" - + # TASK [ansible.utils.fact_diff] ********************************************* # --- before # +++ after diff --git a/tests/integration/targets/fact_diff/tasks/include/simple.yaml b/tests/integration/targets/fact_diff/tasks/include/simple.yaml index d9d62f2..22ed19f 100644 --- a/tests/integration/targets/fact_diff/tasks/include/simple.yaml +++ b/tests/integration/targets/fact_diff/tasks/include/simple.yaml @@ -1,3 +1,4 @@ +--- - name: Check for graceful fail of invalid regex ansible.utils.fact_diff: before: [0, 1, 2] @@ -5,11 +6,11 @@ plugin: vars: skip_lines: - - '+' - ignore_errors: True + - '+' + ignore_errors: true register: result - assert: that: "{{ msg in result.msg }}" vars: - msg: "The regex '+', is not valid" \ No newline at end of file + msg: "The regex '+', is not valid" diff --git a/tests/integration/targets/fact_diff/tasks/main.yaml b/tests/integration/targets/fact_diff/tasks/main.yaml index c9cd63e..4274d75 100644 --- a/tests/integration/targets/fact_diff/tasks/main.yaml +++ b/tests/integration/targets/fact_diff/tasks/main.yaml @@ -1,9 +1,10 @@ +--- - name: Recursively find all test files find: file_type: file paths: "{{ role_path }}/tasks/include" - recurse: yes - use_regex: yes + recurse: true + use_regex: true patterns: - '^(?!_).+$' register: found diff --git a/tests/integration/targets/get_path/tasks/include/argspec.yaml b/tests/integration/targets/get_path/tasks/include/argspec.yaml index 8a6609e..8aec6c5 100644 --- a/tests/integration/targets/get_path/tasks/include/argspec.yaml +++ b/tests/integration/targets/get_path/tasks/include/argspec.yaml @@ -1,15 +1,16 @@ +--- - set_fact: a: b: c: d: - - 0 - - 1 - + - 0 + - 1 + - name: Check argspec validation with filter set_fact: _result: "{{ a|ansible.utils.get_path() }}" - ignore_errors: True + ignore_errors: true register: result - assert: @@ -20,12 +21,12 @@ - name: Check argspec validation with lookup set_fact: _result: "{{ lookup('ansible.utils.get_path') }}" - ignore_errors: True + ignore_errors: true register: result - assert: that: "{{ item in result.msg }}" loop: - - "missing required arguments:" - - path - - var + - "missing required arguments:" + - path + - var diff --git a/tests/integration/targets/get_path/tasks/include/examples_filter.yaml b/tests/integration/targets/get_path/tasks/include/examples_filter.yaml index 03c8ad4..a084a92 100644 --- a/tests/integration/targets/get_path/tasks/include/examples_filter.yaml +++ b/tests/integration/targets/get_path/tasks/include/examples_filter.yaml @@ -1,13 +1,14 @@ +--- - ansible.builtin.set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 e: - - True - - False + - true + - false - name: Retrieve a value deep inside a using a path ansible.builtin.set_fact: @@ -16,7 +17,7 @@ path: b.c.d[0] # TASK [Retrieve a value deep inside a using a path] ****************** -# ok: [localhost] => changed=false +# ok: [localhost] => changed=false # ansible_facts: # value: '0' @@ -93,9 +94,9 @@ # TASK [Get the description of several interfaces] ****************** -# ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => changed=false # msg: Configured by ansible -# ok: [nxos101] => (item=by_name['Ethernet1/2'].description|upper) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/2'].description|upper) => changed=false # msg: CONFIGURED BY ANSIBLE -# ok: [nxos101] => (item=by_name['Ethernet1/3'].description|default('')) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/3'].description|default('')) => changed=false # msg: '' diff --git a/tests/integration/targets/get_path/tasks/include/examples_lookup.yaml b/tests/integration/targets/get_path/tasks/include/examples_lookup.yaml index ce4d262..7bd651f 100644 --- a/tests/integration/targets/get_path/tasks/include/examples_lookup.yaml +++ b/tests/integration/targets/get_path/tasks/include/examples_lookup.yaml @@ -1,13 +1,14 @@ +--- - ansible.builtin.set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 e: - - True - - False + - true + - false - name: Retrieve a value deep inside a using a path ansible.builtin.set_fact: @@ -16,7 +17,7 @@ path: b.c.d[0] # TASK [Retrieve a value deep inside a using a path] ****************** -# ok: [localhost] => changed=false +# ok: [localhost] => changed=false # ansible_facts: # value: '0' @@ -93,9 +94,9 @@ # TASK [Get the description of several interfaces] ****************** -# ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => changed=false # msg: Configured by ansible -# ok: [nxos101] => (item=by_name['Ethernet1/2'].description|upper) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/2'].description|upper) => changed=false # msg: CONFIGURED BY ANSIBLE -# ok: [nxos101] => (item=by_name['Ethernet1/3'].description|default('')) => changed=false +# ok: [nxos101] => (item=by_name['Ethernet1/3'].description|default('')) => changed=false # msg: '' diff --git a/tests/integration/targets/get_path/tasks/include/simple.yaml b/tests/integration/targets/get_path/tasks/include/simple.yaml index 41043ff..d69bbbc 100644 --- a/tests/integration/targets/get_path/tasks/include/simple.yaml +++ b/tests/integration/targets/get_path/tasks/include/simple.yaml @@ -1,58 +1,59 @@ +--- - set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 - name: Simple test filter and lookup assert: that: "{{ item.result == item.expected }}" loop: - - result: "{{ vars|ansible.utils.get_path('a') }}" - expected: "{{ a }}" - - result: "{{ a|ansible.utils.get_path('b') }}" - expected: "{{ a.b }}" - - result: "{{ a|ansible.utils.get_path('b.c') }}" - expected: "{{ a.b.c }}" - - result: "{{ a|ansible.utils.get_path('b.c.d') }}" - expected: "{{ a.b.c.d }}" - - result: "{{ a|ansible.utils.get_path('b.c.d[0]') }}" - expected: "{{ a.b.c.d[0] }}" - - result: "{{ a|ansible.utils.get_path('b.c.d[1]') }}" - expected: "{{ a.b.c.d[1] }}" - - result: "{{ a|ansible.utils.get_path('b[\"c\"]') }}" - expected: "{{ a.b.c }}" - - result: "{{ lookup('ansible.utils.get_path', vars, 'a') }}" - expected: "{{ a }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b') }}" - expected: "{{ a.b }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b.c') }}" - expected: "{{ a.b.c }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d') }}" - expected: "{{ a.b.c.d }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d[0]') }}" - expected: "{{ a.b.c.d[0] }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d[1]') }}" - expected: "{{ a.b.c.d[1] }}" - - result: "{{ lookup('ansible.utils.get_path', a, 'b[\"c\"]') }}" - expected: "{{ a.b.c }}" + - result: "{{ vars|ansible.utils.get_path('a') }}" + expected: "{{ a }}" + - result: "{{ a|ansible.utils.get_path('b') }}" + expected: "{{ a.b }}" + - result: "{{ a|ansible.utils.get_path('b.c') }}" + expected: "{{ a.b.c }}" + - result: "{{ a|ansible.utils.get_path('b.c.d') }}" + expected: "{{ a.b.c.d }}" + - result: "{{ a|ansible.utils.get_path('b.c.d[0]') }}" + expected: "{{ a.b.c.d[0] }}" + - result: "{{ a|ansible.utils.get_path('b.c.d[1]') }}" + expected: "{{ a.b.c.d[1] }}" + - result: "{{ a|ansible.utils.get_path('b[\"c\"]') }}" + expected: "{{ a.b.c }}" + - result: "{{ lookup('ansible.utils.get_path', vars, 'a') }}" + expected: "{{ a }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b') }}" + expected: "{{ a.b }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b.c') }}" + expected: "{{ a.b.c }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d') }}" + expected: "{{ a.b.c.d }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d[0]') }}" + expected: "{{ a.b.c.d[0] }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b.c.d[1]') }}" + expected: "{{ a.b.c.d[1] }}" + - result: "{{ lookup('ansible.utils.get_path', a, 'b[\"c\"]') }}" + expected: "{{ a.b.c }}" - set_fact: a: b: c: d: - - 0 + - 0 - name: Simple test filter and lookup w/ wantlist assert: that: "{{ item.result == item.expected }}" loop: - - result: "{{ vars|ansible.utils.get_path('a.b.c.d[0]', wantlist=True) }}" - expected: - - "{{ a.b.c.d[0] }}" - - result: "{{ lookup('ansible.utils.get_path', vars, 'a.b.c.d[0]', wantlist=True) }}" - expected: - - "{{ a.b.c.d[0] }}" + - result: "{{ vars|ansible.utils.get_path('a.b.c.d[0]', wantlist=True) }}" + expected: + - "{{ a.b.c.d[0] }}" + - result: "{{ lookup('ansible.utils.get_path', vars, 'a.b.c.d[0]', wantlist=True) }}" + expected: + - "{{ a.b.c.d[0] }}" diff --git a/tests/integration/targets/get_path/tasks/main.yaml b/tests/integration/targets/get_path/tasks/main.yaml index c9cd63e..4274d75 100644 --- a/tests/integration/targets/get_path/tasks/main.yaml +++ b/tests/integration/targets/get_path/tasks/main.yaml @@ -1,9 +1,10 @@ +--- - name: Recursively find all test files find: file_type: file paths: "{{ role_path }}/tasks/include" - recurse: yes - use_regex: yes + recurse: true + use_regex: true patterns: - '^(?!_).+$' register: found diff --git a/tests/integration/targets/index_of/tasks/include/argspec.yaml b/tests/integration/targets/index_of/tasks/include/argspec.yaml index 939a559..9d72369 100644 --- a/tests/integration/targets/index_of/tasks/include/argspec.yaml +++ b/tests/integration/targets/index_of/tasks/include/argspec.yaml @@ -1,19 +1,20 @@ +--- - set_fact: complex: a: b: c: d: - - e0: 0 - e1: ansible - e2: True - - e0: 1 - e1: redhat + - e0: 0 + e1: ansible + e2: true + - e0: 1 + e1: redhat - name: Check argspec validation with filter (not a list) set_fact: _result: "{{ complex|ansible.utils.index_of() }}" - ignore_errors: True + ignore_errors: true register: result - assert: @@ -24,7 +25,7 @@ - name: Check argspec validation with filter (missing params) set_fact: _result: "{{ complex.a.b.c.d|ansible.utils.index_of() }}" - ignore_errors: True + ignore_errors: true register: result - assert: @@ -35,7 +36,7 @@ - name: Check argspec validation with lookup (not a list) set_fact: _result: "{{ lookup('ansible.utils.index_of', complex) }}" - ignore_errors: True + ignore_errors: true register: result - assert: @@ -46,12 +47,12 @@ - name: Check argspec validation with lookup (missing params) set_fact: _result: "{{ lookup('ansible.utils.index_of') }}" - ignore_errors: True + ignore_errors: true register: result - assert: that: "{{ item in result.msg }}" loop: - - "missing required arguments:" - - data - - test \ No newline at end of file + - "missing required arguments:" + - data + - test diff --git a/tests/integration/targets/index_of/tasks/include/examples_filter.yaml b/tests/integration/targets/index_of/tasks/include/examples_filter.yaml index 776b409..9c7a5f1 100644 --- a/tests/integration/targets/index_of/tasks/include/examples_filter.yaml +++ b/tests/integration/targets/index_of/tasks/include/examples_filter.yaml @@ -1,17 +1,17 @@ +--- #### Simple examples - - set_fact: data: - - 1 - - 2 - - 3 + - 1 + - 2 + - 3 - name: Find the index of 2 set_fact: indices: "{{ data|ansible.utils.index_of('eq', 2) }}" # TASK [Find the index of 2] ************************************************* -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: '1' @@ -20,7 +20,7 @@ indices: "{{ data|ansible.utils.index_of('eq', 2, wantlist=True) }}" # TASK [Find the index of 2, ensure list is returned] ************************ -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: # - 1 @@ -32,7 +32,7 @@ value: 3 # TASK [Find the index of 3 using the long format] *************************** -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: # - 2 @@ -46,9 +46,9 @@ value: 1 # TASK [Find numbers great than 1, using loop] ******************************* -# ok: [sw01] => (item=1) => +# ok: [sw01] => (item=1) => # msg: 2 is > than 1 -# ok: [sw01] => (item=2) => +# ok: [sw01] => (item=2) => # msg: 3 is > than 1 @@ -56,21 +56,21 @@ - set_fact: data: - - name: sw01.example.lan - type: switch - - name: rtr01.example.lan - type: router - - name: fw01.example.corp - type: firewall - - name: fw02.example.corp - type: firewall + - name: sw01.example.lan + type: switch + - name: rtr01.example.lan + type: router + - name: fw01.example.corp + type: firewall + - name: fw02.example.corp + type: firewall - name: Find the index of all firewalls using the type key set_fact: firewalls: "{{ data|ansible.utils.index_of('eq', 'firewall', 'type') }}" # TASK [Find the index of all firewalls using the type key] ****************** -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # firewalls: # - 2 @@ -84,9 +84,9 @@ device_type: firewall # TASK [Find the index of all firewalls, use in a loop, as a filter] ********* -# ok: [nxos101] => (item=2) => +# ok: [nxos101] => (item=2) => # msg: The type of firewall at index 2 has name fw01.example.corp. -# ok: [nxos101] => (item=3) => +# ok: [nxos101] => (item=3) => # msg: The type of firewall at index 3 has name fw02.example.corp. - name: Find the index of all devices with a .corp name @@ -94,12 +94,12 @@ msg: "The device named {{ data[item].name }} is a {{ data[item].type }}" loop: "{{ data|ansible.utils.index_of('regex', expression, 'name') }}" vars: - expression: '\.corp$' # ends with .corp + expression: '\.corp$' # ends with .corp # TASK [Find the index of all devices with a .corp name] ********************* -# ok: [nxos101] => (item=2) => +# ok: [nxos101] => (item=2) => # msg: The device named fw01.example.corp is a firewall -# ok: [nxos101] => (item=3) => +# ok: [nxos101] => (item=3) => # msg: The device named fw02.example.corp is a firewall @@ -110,8 +110,8 @@ # state: gathered # register: current_l3 -# TASK [Retrieve the current L3 interface configuration] ********************* -# ok: [sw01] => changed=false +# TASK [Retrieve the current L3 interface configuration] ********************* +# ok: [sw01] => changed=false # gathered: # - name: Ethernet1/1 # - name: Ethernet1/2 @@ -122,7 +122,7 @@ # name: mgmt0 # - name: Find the indices interfaces with a 192.168.101.xx ip address -# set_fact: +# set_fact: # found: "{{ found + entry }}" # with_indexed_items: "{{ current_l3.gathered }}" # vars: @@ -135,7 +135,7 @@ # when: address # TASK [debug] *************************************************************** -# ok: [sw01] => +# ok: [sw01] => # found: # - address_idxs: # - 0 @@ -150,7 +150,7 @@ # address: "{{ interface.ipv4[item.1].address }}" # TASK [Show all interfaces and their address] ******************************* -# ok: [nxos101] => (item=[{'interface_idx': '128', 'address_idxs': [0]}, 0]) => +# ok: [nxos101] => (item=[{'interface_idx': '128', 'address_idxs': [0]}, 0]) => # msg: mgmt0 has ip 192.168.101.14/24 @@ -162,8 +162,8 @@ interface: - config: description: configured by Ansible - 1 - enabled: True - loopback-mode: False + enabled: true + loopback-mode: false mtu: 1024 name: loopback0000 type: eth @@ -172,18 +172,18 @@ subinterface: - config: description: subinterface configured by Ansible - 1 - enabled: True + enabled: true index: 5 index: 5 - config: description: subinterface configured by Ansible - 2 - enabled: False + enabled: false index: 2 index: 2 - config: description: configured by Ansible - 2 - enabled: False - loopback-mode: False + enabled: false + loopback-mode: false mtu: 2048 name: loopback1111 type: virt @@ -192,12 +192,12 @@ subinterface: - config: description: subinterface configured by Ansible - 3 - enabled: True + enabled: true index: 10 index: 10 - config: description: subinterface configured by Ansible - 4 - enabled: False + enabled: false index: 3 index: 3 @@ -222,5 +222,5 @@ ansible.utils.index_of('eq', sub_index, 'index') }} # TASK [Find the description of loopback111, subinterface index 10] ************ -# ok: [sw01] => +# ok: [sw01] => # msg: subinterface configured by Ansible - 3 diff --git a/tests/integration/targets/index_of/tasks/include/examples_lookup.yaml b/tests/integration/targets/index_of/tasks/include/examples_lookup.yaml index e2a52e2..548aa54 100644 --- a/tests/integration/targets/index_of/tasks/include/examples_lookup.yaml +++ b/tests/integration/targets/index_of/tasks/include/examples_lookup.yaml @@ -1,17 +1,17 @@ +--- #### Simple examples - - set_fact: data: - - 1 - - 2 - - 3 + - 1 + - 2 + - 3 - name: Find the index of 2 set_fact: indices: "{{ lookup('ansible.utils.index_of', data, 'eq', 2) }}" # TASK [Find the index of 2] ************************************************* -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: '1' @@ -20,7 +20,7 @@ indices: "{{ lookup('ansible.utils.index_of', data, 'eq', 2, wantlist=True) }}" # TASK [Find the index of 2, ensure list is returned] ************************ -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: # - 1 @@ -32,7 +32,7 @@ value: 3 # TASK [Find the index of 3 using the long format] *************************** -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # indices: # - 2 @@ -46,9 +46,9 @@ value: 1 # TASK [Find numbers great than 1, using loop] ******************************* -# ok: [sw01] => (item=1) => +# ok: [sw01] => (item=1) => # msg: 2 is > than 1 -# ok: [sw01] => (item=2) => +# ok: [sw01] => (item=2) => # msg: 3 is > than 1 - name: Find numbers greater than 1, using with @@ -62,9 +62,9 @@ value: 1 # TASK [Find numbers greater than 1, using with] ***************************** -# ok: [nxos101] => (item=1) => +# ok: [nxos101] => (item=1) => # msg: 2 is > than 1 -# ok: [nxos101] => (item=2) => +# ok: [nxos101] => (item=2) => # msg: 3 is > than 1 @@ -72,21 +72,21 @@ - set_fact: data: - - name: sw01.example.lan - type: switch - - name: rtr01.example.lan - type: router - - name: fw01.example.corp - type: firewall - - name: fw02.example.corp - type: firewall + - name: sw01.example.lan + type: switch + - name: rtr01.example.lan + type: router + - name: fw01.example.corp + type: firewall + - name: fw02.example.corp + type: firewall - name: Find the index of all firewalls using the type key set_fact: firewalls: "{{ lookup('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}" # TASK [Find the index of all firewalls using the type key] ****************** -# ok: [nxos101] => changed=false +# ok: [nxos101] => changed=false # ansible_facts: # firewalls: # - 2 @@ -100,9 +100,9 @@ device_type: firewall # TASK [Find the index of all firewalls, use in a loop, as a filter] ********* -# ok: [nxos101] => (item=2) => +# ok: [nxos101] => (item=2) => # msg: The type of firewall at index 2 has name fw01.example.corp. -# ok: [nxos101] => (item=3) => +# ok: [nxos101] => (item=3) => # msg: The type of firewall at index 3 has name fw02.example.corp. - name: Find the index of all devices with a .corp name @@ -110,12 +110,12 @@ msg: "The device named {{ data[item].name }} is a {{ data[item].type }}" loop: "{{ lookup('ansible.utils.index_of', data, 'regex', expression, 'name') }}" vars: - expression: '\.corp$' # ends with .corp + expression: '\.corp$' # ends with .corp # TASK [Find the index of all devices with a .corp name] ********************* -# ok: [nxos101] => (item=2) => +# ok: [nxos101] => (item=2) => # msg: The device named fw01.example.corp is a firewall -# ok: [nxos101] => (item=3) => +# ok: [nxos101] => (item=3) => # msg: The device named fw02.example.corp is a firewall @@ -126,8 +126,8 @@ # state: gathered # register: current_l3 -# TASK [Retrieve the current L3 interface configuration] ********************* -# ok: [sw01] => changed=false +# TASK [Retrieve the current L3 interface configuration] ********************* +# ok: [sw01] => changed=false # gathered: # - name: Ethernet1/1 # - name: Ethernet1/2 @@ -138,7 +138,7 @@ # name: mgmt0 # - name: Find the indices interfaces with a 192.168.101.xx ip address -# set_fact: +# set_fact: # found: "{{ found + entry }}" # with_indexed_items: "{{ current_l3.gathered }}" # vars: @@ -151,7 +151,7 @@ # when: address # TASK [debug] *************************************************************** -# ok: [sw01] => +# ok: [sw01] => # found: # - address_idxs: # - 0 @@ -166,7 +166,7 @@ # address: "{{ interface.ipv4[item.1].address }}" # TASK [Show all interfaces and their address] ******************************* -# ok: [nxos101] => (item=[{'interface_idx': '128', 'address_idxs': [0]}, 0]) => +# ok: [nxos101] => (item=[{'interface_idx': '128', 'address_idxs': [0]}, 0]) => # msg: mgmt0 has ip 192.168.101.14/24 @@ -178,8 +178,8 @@ interface: - config: description: configured by Ansible - 1 - enabled: True - loopback-mode: False + enabled: true + loopback-mode: false mtu: 1024 name: loopback0000 type: eth @@ -188,18 +188,18 @@ subinterface: - config: description: subinterface configured by Ansible - 1 - enabled: True + enabled: true index: 5 index: 5 - config: description: subinterface configured by Ansible - 2 - enabled: False + enabled: false index: 2 index: 2 - config: description: configured by Ansible - 2 - enabled: False - loopback-mode: False + enabled: false + loopback-mode: false mtu: 2048 name: loopback1111 type: virt @@ -208,12 +208,12 @@ subinterface: - config: description: subinterface configured by Ansible - 3 - enabled: True + enabled: true index: 10 index: 10 - config: description: subinterface configured by Ansible - 4 - enabled: False + enabled: false index: 3 index: 3 @@ -230,8 +230,8 @@ sub_index: 10 # retrieve the index in each nested list int_idx: | - {{ lookup('ansible.utils.index_of', - data.interfaces.interface, + {{ lookup('ansible.utils.index_of', + data.interfaces.interface, 'eq', int_name, 'name') }} subint_idx: | {{ lookup('ansible.utils.index_of', @@ -239,5 +239,5 @@ 'eq', sub_index, 'index') }} # TASK [Find the description of loopback111, subinterface index 10] ************ -# ok: [sw01] => +# ok: [sw01] => # msg: subinterface configured by Ansible - 3 diff --git a/tests/integration/targets/index_of/tasks/include/simple.yaml b/tests/integration/targets/index_of/tasks/include/simple.yaml index 6b7708b..f6a81ac 100644 --- a/tests/integration/targets/index_of/tasks/include/simple.yaml +++ b/tests/integration/targets/index_of/tasks/include/simple.yaml @@ -1,9 +1,10 @@ +--- - set_fact: complex: a: - - True - - True - - False + - true + - true + - false - 5 b: @@ -13,9 +14,9 @@ b2: 4 c: c1: - - a - - b - - c + - a + - b + - c d: - Abcd - abcd @@ -26,35 +27,35 @@ assert: that: "{{ item.test == item.result }}" loop: - - test: "{{ complex.a|ansible.utils.index_of('eq', True) }}" - result: [0, 1] - - test: "{{ lookup('ansible.utils.index_of', complex.a, 'eq', True) }}" - result: [0, 1] - - test: "{{ complex.a|ansible.utils.index_of('in', [True, False]) }}" - result: [0, 1, 2] - - test: "{{ lookup('ansible.utils.index_of', complex.a, 'in', [True, False]) }}" - result: [0, 1, 2] - # These are commented out due to jinja < 2.11 w/ 2.9, 'integer' not avaialable - # can be enabled at a later date - # - test: "{{ complex.a|ansible.utils.index_of('integer') }}" - # result: "3" - # - test: "{{ lookup('ansible.utils.index_of', complex.a, 'integer') }}" - # result: "3" + - test: "{{ complex.a|ansible.utils.index_of('eq', True) }}" + result: [0, 1] + - test: "{{ lookup('ansible.utils.index_of', complex.a, 'eq', True) }}" + result: [0, 1] + - test: "{{ complex.a|ansible.utils.index_of('in', [True, False]) }}" + result: [0, 1, 2] + - test: "{{ lookup('ansible.utils.index_of', complex.a, 'in', [True, False]) }}" + result: [0, 1, 2] + # These are commented out due to jinja < 2.11 w/ 2.9, 'integer' not avaialable + # can be enabled at a later date + # - test: "{{ complex.a|ansible.utils.index_of('integer') }}" + # result: "3" + # - test: "{{ lookup('ansible.utils.index_of', complex.a, 'integer') }}" + # result: "3" - - test: "{{ complex.b|ansible.utils.index_of('==', 1, 'b1') }}" - result: "0" - - test: "{{ lookup('ansible.utils.index_of', complex.b, '==', 1, 'b1') }}" - result: "0" - - - test: "{{ complex.c.c1|ansible.utils.index_of('!=', 'c') }}" - result: [0, 1] - - test: "{{ lookup('ansible.utils.index_of', complex.c.c1, '!=', 'c') }}" - result: [0, 1] + - test: "{{ complex.b|ansible.utils.index_of('==', 1, 'b1') }}" + result: "0" + - test: "{{ lookup('ansible.utils.index_of', complex.b, '==', 1, 'b1') }}" + result: "0" - - test: "{{ complex.d|ansible.utils.index_of('match', '.*d$') }}" - result: [0, 1] - - test: "{{ lookup('ansible.utils.index_of', complex.d, 'match', '.*d$') }}" - result: [0, 1] + - test: "{{ complex.c.c1|ansible.utils.index_of('!=', 'c') }}" + result: [0, 1] + - test: "{{ lookup('ansible.utils.index_of', complex.c.c1, '!=', 'c') }}" + result: [0, 1] + + - test: "{{ complex.d|ansible.utils.index_of('match', '.*d$') }}" + result: [0, 1] + - test: "{{ lookup('ansible.utils.index_of', complex.d, 'match', '.*d$') }}" + result: [0, 1] - set_fact: @@ -63,43 +64,43 @@ b: c: d: - - e0: 0 - e1: ansible - e2: True - - e0: 1 - e1: redhat + - e0: 0 + e1: ansible + e2: true + - e0: 1 + e1: redhat - name: Find index in list of dictionaries assert: that: "{{ item.test == item.result }}" loop: - - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1') }}" - result: "0" - - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1') }}" - result: "0" - - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1', wantlist=True) }}" - result: [0] - - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1', wantlist=True) }}" - result: [0] + - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1') }}" + result: "0" + - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1') }}" + result: "0" + - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1', wantlist=True) }}" + result: [0] + - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1', wantlist=True) }}" + result: [0] - name: Test a missing key in the list of dictionaries assert: that: "{{ item.test == item.result }}" loop: - - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2') }}" - result: "0" - - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2') }}" - result: "0" + - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2') }}" + result: "0" + - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2') }}" + result: "0" - name: Test a missing key in the list of dictionaries, fail on missing assert: that: "{{ item.test == item.result }}" loop: - - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2', fail_on_missing=True) }}" - result: "0" - - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2', fail_on_missing=True) }}" - result: "0" - ignore_errors: True + - test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2', fail_on_missing=True) }}" + result: "0" + - test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2', fail_on_missing=True) }}" + result: "0" + ignore_errors: true register: result - name: Ensure the previous test failed diff --git a/tests/integration/targets/index_of/tasks/main.yaml b/tests/integration/targets/index_of/tasks/main.yaml index c9cd63e..4274d75 100644 --- a/tests/integration/targets/index_of/tasks/main.yaml +++ b/tests/integration/targets/index_of/tasks/main.yaml @@ -1,9 +1,10 @@ +--- - name: Recursively find all test files find: file_type: file paths: "{{ role_path }}/tasks/include" - recurse: yes - use_regex: yes + recurse: true + use_regex: true patterns: - '^(?!_).+$' register: found diff --git a/tests/integration/targets/to_paths/tasks/include/argspec.yaml b/tests/integration/targets/to_paths/tasks/include/argspec.yaml index cf5d1e3..619ba44 100644 --- a/tests/integration/targets/to_paths/tasks/include/argspec.yaml +++ b/tests/integration/targets/to_paths/tasks/include/argspec.yaml @@ -1,14 +1,15 @@ +--- - set_fact: a: b: c: d: - - 0 + - 0 - name: Check argspec validation with lookup set_fact: _result: "{{ a|ansible.utils.to_paths(wantlist=5) }}" - ignore_errors: True + ignore_errors: true register: result - debug: @@ -22,7 +23,7 @@ - name: Check argspec validation with lookup set_fact: _result: "{{ lookup('ansible.utils.to_paths') }}" - ignore_errors: True + ignore_errors: true register: result - debug: diff --git a/tests/integration/targets/to_paths/tasks/include/examples_filter.yaml b/tests/integration/targets/to_paths/tasks/include/examples_filter.yaml index 9e45019..f5911d0 100644 --- a/tests/integration/targets/to_paths/tasks/include/examples_filter.yaml +++ b/tests/integration/targets/to_paths/tasks/include/examples_filter.yaml @@ -1,15 +1,15 @@ +--- #### Simple examples - - ansible.builtin.set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 e: - - True - - False + - true + - false - ansible.builtin.set_fact: paths: "{{ a|ansible.utils.to_paths }}" diff --git a/tests/integration/targets/to_paths/tasks/include/examples_lookup.yaml b/tests/integration/targets/to_paths/tasks/include/examples_lookup.yaml index 0701977..adf27a4 100644 --- a/tests/integration/targets/to_paths/tasks/include/examples_lookup.yaml +++ b/tests/integration/targets/to_paths/tasks/include/examples_lookup.yaml @@ -1,15 +1,15 @@ +--- #### Simple examples - - ansible.builtin.set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 e: - - True - - False + - true + - false - ansible.builtin.set_fact: paths: "{{ lookup('ansible.utils.to_paths', a) }}" diff --git a/tests/integration/targets/to_paths/tasks/include/simple.yaml b/tests/integration/targets/to_paths/tasks/include/simple.yaml index 0c81f87..ef255f3 100644 --- a/tests/integration/targets/to_paths/tasks/include/simple.yaml +++ b/tests/integration/targets/to_paths/tasks/include/simple.yaml @@ -1,52 +1,53 @@ +--- - set_fact: a: b: c: d: - - 0 - - 1 + - 0 + - 1 - name: Test filter and lookup plugin, simple and prepend assert: that: "{{ item.result == item.expected }}" loop: - - result: "{{ a|ansible.utils.to_paths }}" - expected: - b.c.d[0]: 0 - b.c.d[1]: 1 - - result: "{{ lookup('ansible.utils.to_paths', a) }}" - expected: - b.c.d[0]: 0 - b.c.d[1]: 1 - - result: "{{ a|ansible.utils.to_paths(prepend='a') }}" - expected: - a.b.c.d[0]: 0 - a.b.c.d[1]: 1 - - result: "{{ lookup('ansible.utils.to_paths', a, prepend='a') }}" - expected: - a.b.c.d[0]: 0 - a.b.c.d[1]: 1 + - result: "{{ a|ansible.utils.to_paths }}" + expected: + b.c.d[0]: 0 + b.c.d[1]: 1 + - result: "{{ lookup('ansible.utils.to_paths', a) }}" + expected: + b.c.d[0]: 0 + b.c.d[1]: 1 + - result: "{{ a|ansible.utils.to_paths(prepend='a') }}" + expected: + a.b.c.d[0]: 0 + a.b.c.d[1]: 1 + - result: "{{ lookup('ansible.utils.to_paths', a, prepend='a') }}" + expected: + a.b.c.d[0]: 0 + a.b.c.d[1]: 1 - set_fact: a: b: c: d: - - 0 - + - 0 + - name: Test filter and lookup plugin, wantlist and prepend assert: that: "{{ item.result == item.expected }}" loop: - - result: "{{ a|ansible.utils.to_paths(wantlist=True) }}" - expected: - - b.c.d[0]: 0 - - result: "{{ lookup('ansible.utils.to_paths', a, wantlist=True) }}" - expected: - - b.c.d[0]: 0 - - result: "{{ a|ansible.utils.to_paths(wantlist=True, prepend='a') }}" - expected: - - a.b.c.d[0]: 0 - - result: "{{ lookup('ansible.utils.to_paths', a, wantlist=True, prepend='a') }}" - expected: - - a.b.c.d[0]: 0 + - result: "{{ a|ansible.utils.to_paths(wantlist=True) }}" + expected: + - b.c.d[0]: 0 + - result: "{{ lookup('ansible.utils.to_paths', a, wantlist=True) }}" + expected: + - b.c.d[0]: 0 + - result: "{{ a|ansible.utils.to_paths(wantlist=True, prepend='a') }}" + expected: + - a.b.c.d[0]: 0 + - result: "{{ lookup('ansible.utils.to_paths', a, wantlist=True, prepend='a') }}" + expected: + - a.b.c.d[0]: 0 diff --git a/tests/integration/targets/to_paths/tasks/main.yaml b/tests/integration/targets/to_paths/tasks/main.yaml index c9cd63e..4274d75 100644 --- a/tests/integration/targets/to_paths/tasks/main.yaml +++ b/tests/integration/targets/to_paths/tasks/main.yaml @@ -1,9 +1,10 @@ +--- - name: Recursively find all test files find: file_type: file paths: "{{ role_path }}/tasks/include" - recurse: yes - use_regex: yes + recurse: true + use_regex: true patterns: - '^(?!_).+$' register: found diff --git a/tests/integration/targets/update_fact/tasks/main.yaml b/tests/integration/targets/update_fact/tasks/main.yaml index c5aa320..de0a876 100644 --- a/tests/integration/targets/update_fact/tasks/main.yaml +++ b/tests/integration/targets/update_fact/tasks/main.yaml @@ -1,18 +1,19 @@ +--- - name: Set a fact set_fact: a: b: c: - - 1 - - 2 + - 1 + - 2 - name: Update the fact ansible.utils.update_fact: updates: - - path: a.b.c.0 - value: 10 - - path: "a['b']['c'][1]" - value: 20 + - path: a.b.c.0 + value: 10 + - path: "a['b']['c'][1]" + value: 20 register: updated - assert: @@ -22,19 +23,19 @@ a: b: c: - - 10 - - 20 + - 10 + - 20 - name: Update the fact ansible.utils.update_fact: updates: - - path: a - value: - x: - y: - z: - - 100 - - True + - path: a + value: + x: + y: + z: + - 100 + - true register: updated - assert: @@ -45,14 +46,14 @@ x: y: z: - - 100 - - True + - 100 + - true - name: Update the fact ansible.utils.update_fact: updates: - - path: "a.b.c[{{ index }}]" - value: 20 + - path: "a.b.c[{{ index }}]" + value: 20 vars: index: "{{ a.b.c|ansible.utils.index_of('eq', 2) }}" register: updated @@ -64,5 +65,5 @@ a: b: c: - - 1 - - 20 + - 1 + - 20 diff --git a/tests/integration/targets/validate/files/criteria/crc_error_check.json b/tests/integration/targets/validate/files/criteria/crc_error_check.json new file mode 100644 index 0000000..66c43ca --- /dev/null +++ b/tests/integration/targets/validate/files/criteria/crc_error_check.json @@ -0,0 +1,18 @@ +{ + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "in_crc_errors": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/integration/targets/validate/files/criteria/enabled_check.json b/tests/integration/targets/validate/files/criteria/enabled_check.json new file mode 100644 index 0000000..dde7c05 --- /dev/null +++ b/tests/integration/targets/validate/files/criteria/enabled_check.json @@ -0,0 +1,13 @@ +{ + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "enabled": { + "enum": [true] + } + } + } + } +} \ No newline at end of file diff --git a/tests/integration/targets/validate/files/criteria/in_rate_check.json b/tests/integration/targets/validate/files/criteria/in_rate_check.json new file mode 100644 index 0000000..94614ea --- /dev/null +++ b/tests/integration/targets/validate/files/criteria/in_rate_check.json @@ -0,0 +1,22 @@ +{ + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "rate": { + "properties": { + "in_rate": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/integration/targets/validate/files/criteria/oper_status_up.json b/tests/integration/targets/validate/files/criteria/oper_status_up.json new file mode 100644 index 0000000..2f5803a --- /dev/null +++ b/tests/integration/targets/validate/files/criteria/oper_status_up.json @@ -0,0 +1,14 @@ +{ + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "oper_status": { + "type": "string", + "pattern": "up" + } + } + } + } +} \ No newline at end of file diff --git a/tests/integration/targets/validate/files/data/show_interface.json b/tests/integration/targets/validate/files/data/show_interface.json new file mode 100644 index 0000000..ed93c20 --- /dev/null +++ b/tests/integration/targets/validate/files/data/show_interface.json @@ -0,0 +1,38 @@ +{ + "GigabitEthernet0/0/0/0": { + "auto_negotiate": false, + "counters": { + "in_crc_errors": 0, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "configured using Ansible", + "duplex_mode": "full", + "enabled": true, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "down", + "type": "GigabitEthernet" + }, + "GigabitEthernet0/0/0/1": { + "auto_negotiate": false, + "counters": { + "in_crc_errors": 10, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "# interface is configures with Ansible", + "duplex_mode": "full", + "enabled": false, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "up", + "type": "GigabitEthernet" + } +} diff --git a/tests/integration/targets/validate/tasks/include/filter.yaml b/tests/integration/targets/validate/tasks/include/filter.yaml new file mode 100644 index 0000000..d6a2eb1 --- /dev/null +++ b/tests/integration/targets/validate/tasks/include/filter.yaml @@ -0,0 +1,88 @@ +--- +- name: validate data in json format using jsonschema (invalid data) + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces|ansible.utils.validate([oper_status, enable_check, crc_error_check], engine='ansible.utils.jsonschema') }}" + +- assert: + that: + - "data_criteria_checks[0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "data_criteria_checks[1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "data_criteria_checks[2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + +- name: validate data in json format using jsonschema (valid data) + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces|ansible.utils.validate(in_rate_check) }}" + +- assert: + that: + - "data_criteria_checks == []" + +- name: test invalid plugin configuration option, passed within filter plugin + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces|ansible.utils.validate(in_rate_check, draft='draft0') }}" + ignore_errors: true + register: result + +- assert: + that: + - "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg" + +- name: invalid engine value + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces|ansible.utils.validate(in_rate_check, engine='ansible.utils.sample') }}" + ignore_errors: true + register: result + +- assert: + that: + - "'errors' not in result" + - "result['failed'] == true" + - "'For engine \\'ansible.utils.sample\\' error loading the corresponding validate plugin' in result.msg" + +- name: invalid data value + ansible.builtin.set_fact: + data_criteria_checks: "{{ 'invalid data'|ansible.utils.validate(in_rate_check, engine='ansible.utils.jsonschema') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'data\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: invalid criteria value + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces|ansible.utils.validate('invalid criteria', engine='ansible.utils.jsonschema') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'criteria\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: read data and criteria from file + set_fact: + data: "{{ lookup('file', 'data/show_interface.json') }}" + oper_status_up_criteria: "{{ lookup('file', 'criteria/oper_status_up.json') }}" + enabled_check_criteria: "{{ lookup('file', 'criteria/enabled_check.json') }}" + crc_error_check_criteria: "{{ lookup('file', 'criteria/crc_error_check.json') }}" + in_rate_check_criteria: "{{ lookup('file', 'criteria/in_rate_check.json') }}" + +- name: validate data using jsonschema engine (invalid data read from file) + ansible.builtin.set_fact: + data_criteria_checks: "{{ data|ansible.utils.validate([oper_status_up_criteria, enabled_check_criteria, crc_error_check_criteria]) }}" + +- assert: + that: + - "data_criteria_checks[0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "data_criteria_checks[1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "data_criteria_checks[2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + +- name: validate data using jsonschema engine (valid data read from file) + ansible.builtin.set_fact: + data_criteria_checks: "{{ data|ansible.utils.validate(in_rate_check_criteria) }}" + +- assert: + that: + - "data_criteria_checks == []" diff --git a/tests/integration/targets/validate/tasks/include/lookup.yaml b/tests/integration/targets/validate/tasks/include/lookup.yaml new file mode 100644 index 0000000..6e389a4 --- /dev/null +++ b/tests/integration/targets/validate/tasks/include/lookup.yaml @@ -0,0 +1,106 @@ +--- +- name: validate data in json format using jsonschema (invalid data) + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, [oper_status, enable_check, crc_error_check], engine='ansible.utils.jsonschema') }}" + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "data_criteria_checks[0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "data_criteria_checks[1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "data_criteria_checks[2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + +- name: validate data in json format using jsonschema (invalid data) + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, in_rate_check) }}" + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "data_criteria_checks == []" + +- name: test invalid plugin configuration option, passed within lookup plugin + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, in_rate_check, draft='draft0') }}" + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg" + +- name: test invalid plugin configuration option, passed as task varaible + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, in_rate_check) }}" + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft0 + +- assert: + that: + - "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg" + +- name: invalid engine value + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, in_rate_check, engine='ansible.utils.sample') }}" + ignore_errors: true + register: result + +- assert: + that: + - "'errors' not in result" + - "result['failed'] == true" + - "'For engine \\'ansible.utils.sample\\' error loading the corresponding validate plugin' in result.msg" + +- name: invalid data value + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', 'invalid data', in_rate_check, engine='ansible.utils.jsonschema') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'data\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: invalid criteria value + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', show_interfaces, 'invalid criteria', engine='ansible.utils.jsonschema') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'criteria\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: read data and criteria from file + set_fact: + data: "{{ lookup('file', 'data/show_interface.json') }}" + oper_status_up_criteria: "{{ lookup('file', 'criteria/oper_status_up.json') }}" + enabled_check_criteria: "{{ lookup('file', 'criteria/enabled_check.json') }}" + crc_error_check_criteria: "{{ lookup('file', 'criteria/crc_error_check.json') }}" + in_rate_check_criteria: "{{ lookup('file', 'criteria/in_rate_check.json') }}" + +- name: validate data using jsonschema engine (invalid data read from file) + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', data, [oper_status_up_criteria, enabled_check_criteria, crc_error_check_criteria], engine='ansible.utils.jsonschema') }}" + +- assert: + that: + - "data_criteria_checks[0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "data_criteria_checks[1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "data_criteria_checks[2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + +- name: validate data using jsonschema engine (valid data read from file) + ansible.builtin.set_fact: + data_criteria_checks: "{{ lookup('ansible.utils.validate', data, in_rate_check_criteria, engine='ansible.utils.jsonschema') }}" + +- assert: + that: + - "data_criteria_checks == []" diff --git a/tests/integration/targets/validate/tasks/include/module.yaml b/tests/integration/targets/validate/tasks/include/module.yaml new file mode 100644 index 0000000..3ffdbb0 --- /dev/null +++ b/tests/integration/targets/validate/tasks/include/module.yaml @@ -0,0 +1,134 @@ +--- +- name: validate data using jsonschema engine (invalid data) + ansible.utils.validate: + data: "{{ show_interfaces }}" + criteria: + - "{{ oper_status }}" + - "{{ enable_check }}" + - "{{ crc_error_check }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "'errors' in result" + - "result['errors'][0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "result['errors'][1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "result['errors'][2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + - "'Validation errors were found' in result.msg" + - "'patternProperties.^.*.properties.oper_status.pattern' in result.msg" + - "'patternProperties.^.*.properties.enabled.enum' in result.msg" + - "'patternProperties.^.*.properties.counters.properties.in_crc_errors.maximum' in result.msg" + +- name: validate data using jsonschema engine (valid data) + ansible.utils.validate: + data: "{{ show_interfaces }}" + criteria: "{{ in_rate_check }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "'errors' not in result" + - "'all checks passed' in result.msg" + +- name: test invalid plugin configuration option + ansible.utils.validate: + data: "{{ show_interfaces }}" + criteria: "{{ in_rate_check }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft0 + +- assert: + that: + - "'errors' not in result" + - "result['failed'] == true" + - "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg" + +- name: invalid engine value + ansible.utils.validate: + data: "{{ show_interfaces }}" + criteria: "{{ in_rate_check }}" + engine: ansible.utils.sample + ignore_errors: true + register: result + +- assert: + that: + - "'errors' not in result" + - "result['failed'] == true" + - "'For engine \\'ansible.utils.sample\\' error loading the corresponding validate plugin' in result.msg" + +- name: invalid data value + ansible.utils.validate: + data: "sample" + criteria: "{{ in_rate_check }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'data\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: invalid criteria value + ansible.utils.validate: + data: "{{ show_interfaces }}" + criteria: "sample}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'criteria\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: validate data using jsonschema engine (invalid data read from file) + ansible.utils.validate: + data: "{{ lookup('file', 'data/show_interface.json') }}" + criteria: + - "{{ lookup('file', 'criteria/oper_status_up.json') }}" + - "{{ lookup('file', 'criteria/enabled_check.json') }}" + - "{{ lookup('file', 'criteria/crc_error_check.json') }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "'errors' in result" + - "result['errors'][0].data_path == 'GigabitEthernet0/0/0/0.oper_status'" + - "result['errors'][1].data_path == 'GigabitEthernet0/0/0/1.enabled'" + - "result['errors'][2].data_path == 'GigabitEthernet0/0/0/1.counters.in_crc_errors'" + - "'Validation errors were found' in result.msg" + - "'patternProperties.^.*.properties.oper_status.pattern' in result.msg" + - "'patternProperties.^.*.properties.enabled.enum' in result.msg" + - "'patternProperties.^.*.properties.counters.properties.in_crc_errors.maximum' in result.msg" + +- name: validate data using jsonschema engine (valid data read from file) + ansible.utils.validate: + data: "{{ lookup('file', 'data/show_interface.json') }}" + criteria: "{{ lookup('file', 'criteria/in_rate_check.json') }}" + engine: ansible.utils.jsonschema + ignore_errors: true + register: result + vars: + ansible_validate_jsonschema_draft: draft7 + +- assert: + that: + - "'errors' not in result" + - "'all checks passed' in result.msg" diff --git a/tests/integration/targets/validate/tasks/include/test.yaml b/tests/integration/targets/validate/tasks/include/test.yaml new file mode 100644 index 0000000..9826252 --- /dev/null +++ b/tests/integration/targets/validate/tasks/include/test.yaml @@ -0,0 +1,89 @@ +--- +- name: validate data in json format using jsonschema (invalid data) + ansible.builtin.set_fact: + is_data_valid: "{{ show_interfaces is ansible.utils.validate(engine='ansible.utils.jsonschema', criteria=[oper_status, enable_check, crc_error_check], draft='draft7') }}" + +- assert: + that: + - "is_data_valid == false" + +- name: validate data in json format using jsonschema (valid data) + ansible.builtin.set_fact: + is_data_valid: "{{ show_interfaces is ansible.utils.validate(criteria=in_rate_check) }}" + +- assert: + that: + - "is_data_valid == true" + +- name: test invalid plugin configuration option, passed within filter plugin + ansible.builtin.set_fact: + is_data_valid: "{{ show_interfaces is ansible.utils.validate(criteria=in_rate_check, draft='draft0') }}" + ignore_errors: true + register: result + +- assert: + that: + - "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg" + +- name: invalid engine value + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces is ansible.utils.validate(criteria=in_rate_check, engine='ansible.utils.sample') }}" + ignore_errors: true + register: result + +- assert: + that: + - "'errors' not in result" + - "result['failed'] == true" + - "'For engine \\'ansible.utils.sample\\' error loading the corresponding validate plugin' in result.msg" + +- name: invalid data value + ansible.builtin.set_fact: + data_criteria_checks: "{{ 'invalid data' is ansible.utils.validate(criteria=in_rate_check) }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'data\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: invalid criteria value + ansible.builtin.set_fact: + data_criteria_checks: "{{ show_interfaces is ansible.utils.validate(criteria='invalid data') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result['failed'] == true" + - "'\\'criteria\\' option value is invalid, value should of type dict or str format of dict' in result.msg" + +- name: read data and criteria from file + set_fact: + data: "{{ lookup('file', 'data/show_interface.json') }}" + oper_status_up_criteria: "{{ lookup('file', 'criteria/oper_status_up.json') }}" + enabled_check_criteria: "{{ lookup('file', 'criteria/enabled_check.json') }}" + crc_error_check_criteria: "{{ lookup('file', 'criteria/crc_error_check.json') }}" + in_rate_check_criteria: "{{ lookup('file', 'criteria/in_rate_check.json') }}" + +- name: validate data using jsonschema engine (invalid data read from file) + ansible.builtin.set_fact: + is_data_valid: "{{ show_interfaces is ansible.utils.validate(engine='ansible.utils.jsonschema', criteria=[oper_status_up_criteria, enabled_check_criteria, crc_error_check_criteria], draft='draft7') }}" + +- assert: + that: + - "is_data_valid == false" + +- name: validate data using jsonschema engine (invalid data read from file) + ansible.builtin.set_fact: + is_data_valid: "{{ show_interfaces is ansible.utils.validate(criteria=in_rate_check_criteria) }}" + + +- name: validate data using jsonschema engine (valid data read from file) + ansible.builtin.set_fact: + data_criteria_checks: "{{ data|ansible.utils.validate(in_rate_check_criteria) }}" + +- assert: + that: + - "is_data_valid == true" diff --git a/tests/integration/targets/validate/tasks/main.yaml b/tests/integration/targets/validate/tasks/main.yaml new file mode 100644 index 0000000..4274d75 --- /dev/null +++ b/tests/integration/targets/validate/tasks/main.yaml @@ -0,0 +1,13 @@ +--- +- name: Recursively find all test files + find: + file_type: file + paths: "{{ role_path }}/tasks/include" + recurse: true + use_regex: true + patterns: + - '^(?!_).+$' + register: found + +- include: "{{ item.path }}" + loop: "{{ found.files }}" diff --git a/tests/integration/targets/validate/vars/main.yaml b/tests/integration/targets/validate/vars/main.yaml new file mode 100644 index 0000000..58f8e30 --- /dev/null +++ b/tests/integration/targets/validate/vars/main.yaml @@ -0,0 +1,78 @@ +--- +show_interfaces: + GigabitEthernet0/0/0/0: + auto_negotiate: false + counters: + in_crc_errors: 0 + in_errors: 0 + rate: + in_rate: 0 + out_rate: 0 + description: configured using Ansible + duplex_mode: full + enabled: true + line_protocol: up + mtu: 1514 + oper_status: down + type: GigabitEthernet + GigabitEthernet0/0/0/1: + auto_negotiate: false + counters: + in_crc_errors: 10 + in_errors: 0 + rate: + in_rate: 0 + out_rate: 0 + description: '# interface is configures with Ansible' + duplex_mode: full + enabled: false + line_protocol: up + mtu: 1514 + oper_status: up + type: GigabitEthernet + +oper_status: + type: object + patternProperties: + ^.*: + type: object + properties: + oper_status: + type: string + pattern: up + +enable_check: + type: object + patternProperties: + ^.*: + type: object + properties: + enabled: + enum: + - true + +crc_error_check: + type: object + patternProperties: + ^.*: + type: object + properties: + counters: + properties: + in_crc_errors: + type: number + maximum: 0 + +in_rate_check: + type: object + patternProperties: + ^.*: + type: object + properties: + counters: + properties: + rate: + properties: + in_rate: + type: number + maximum: 0 diff --git a/tests/unit/module_utils/test_get_path.py b/tests/unit/module_utils/test_get_path.py index 89964d0..a2a203d 100644 --- a/tests/unit/module_utils/test_get_path.py +++ b/tests/unit/module_utils/test_get_path.py @@ -8,9 +8,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import json -import heapq -import os import unittest from ansible_collections.ansible.utils.plugins.module_utils.common.get_path import ( get_path, diff --git a/tests/unit/plugins/action/test_fact_diff.py b/tests/unit/plugins/action/test_fact_diff.py index 66c9f98..f9807d3 100644 --- a/tests/unit/plugins/action/test_fact_diff.py +++ b/tests/unit/plugins/action/test_fact_diff.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import copy import re import unittest from mock import MagicMock @@ -43,10 +42,7 @@ class TestUpdate_Fact(unittest.TestCase): self._plugin._task.args = {"before": True} result = self._plugin.run(task_vars=self._task_vars) self.assertTrue(result["failed"]) - self.assertIn( - "missing required arguments: after", - result["msg"], - ) + self.assertIn("missing required arguments: after", result["msg"]) def test_same(self): """Ensure two equal string don't create a diff""" @@ -151,9 +147,15 @@ class TestUpdate_Fact(unittest.TestCase): self._plugin._task.args = {"before": before, "after": after} result = self._plugin.run(task_vars=self._task_vars) self.assertTrue(result["changed"]) - mlines = [l for l in result["diff_lines"] if re.match(r"^-\s+3$", l)] + mlines = [ + line for line in result["diff_lines"] if re.match(r"^-\s+3$", line) + ] self.assertEqual(1, len(mlines)) - mlines = [l for l in result["diff_lines"] if re.match(r"^\+\s+4$", l)] + mlines = [ + line + for line in result["diff_lines"] + if re.match(r"^\+\s+4$", line) + ] self.assertEqual(1, len(mlines)) def test_invalid_diff_engine_not_collection(self): @@ -179,10 +181,7 @@ class TestUpdate_Fact(unittest.TestCase): } result = self._plugin.run(task_vars=self._task_vars) self.assertTrue(result["failed"]) - self.assertIn( - "Error loading plugin 'a.b.c'", - result["msg"], - ) + self.assertIn("Error loading plugin 'a.b.c'", result["msg"]) def test_invalid_regex(self): """Check with invalid regex""" @@ -195,10 +194,7 @@ class TestUpdate_Fact(unittest.TestCase): } result = self._plugin.run(task_vars=self._task_vars) self.assertTrue(result["failed"]) - self.assertIn( - "The regex '+', is not valid", - result["msg"], - ) + self.assertIn("The regex '+', is not valid", result["msg"]) def test_fail_plugin(self): """Simulate a diff plugin failure""" diff --git a/tests/unit/plugins/action/test_update_fact.py b/tests/unit/plugins/action/test_update_fact.py index 20cfd90..1abdc42 100644 --- a/tests/unit/plugins/action/test_update_fact.py +++ b/tests/unit/plugins/action/test_update_fact.py @@ -97,8 +97,7 @@ class TestUpdate_Fact(unittest.TestCase): with self.assertRaises(Exception) as error: self._plugin.run(task_vars=None) self.assertIn( - "missing required arguments: updates", - str(error.exception), + "missing required arguments: updates", str(error.exception) ) def test_argspec_none(self): diff --git a/tests/unit/plugins/action/test_validate.py b/tests/unit/plugins/action/test_validate.py new file mode 100644 index 0000000..1e25d30 --- /dev/null +++ b/tests/unit/plugins/action/test_validate.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import copy +import unittest +from jinja2 import Template, TemplateSyntaxError +from mock import MagicMock +from ansible.playbook.task import Task +from ansible.template import Templar +from ansible.errors import AnsibleActionFail + +from ansible_collections.ansible.utils.plugins.action.validate import ( + ActionModule, +) + +DATA = { + "GigabitEthernet0/0/0/0": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 0, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "configured using Ansible", + "duplex_mode": "full", + "enabled": True, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "down", + "type": "GigabitEthernet" + }, + "GigabitEthernet0/0/0/1": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 10, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "# interface is configures with Ansible", + "duplex_mode": "full", + "enabled": False, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "up", + "type": "GigabitEthernet" + } +} + +CRITERIA_CRC_ERROR_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "in_crc_errors": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } +} + +CRITERIA_ENABLED_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "enabled": { + "enum": [True] + } + } + } + } +} + +CRITERIA_OPER_STATUS_UP_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "oper_status": { + "type": "string", + "pattern": "up" + } + } + } + } +} + +CRITERIA_IN_RATE_CHECK = { + "type": "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "rate": { + "properties": { + "in_rate": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } + } + } +} + + +class TestValidate(unittest.TestCase): + def setUp(self): + task = MagicMock(Task) + play_context = MagicMock() + play_context.check_mode = False + connection = MagicMock() + fake_loader = {} + templar = Templar(loader=fake_loader) + self._plugin = ActionModule( + task=task, + connection=connection, + play_context=play_context, + loader=fake_loader, + templar=templar, + shared_loader_obj=None, + ) + self._plugin._task.action = "validate" + + def test_invalid_argspec(self): + """Check passing invalid argspec""" + + # missing required arguments + self._plugin._task.args = {"engine": "ansible.utils.jsonschema"} + result = self._plugin.run(task_vars=None) + self.assertIn("missing required arguments: criteria", result["errors"]) + + # invalid engine option value + self._plugin._task.args = { + "engine": "ansible.utils.sample", + "data": DATA, + "criteria": CRITERIA_OPER_STATUS_UP_CHECK + } + result = self._plugin.run(task_vars=None) + self.assertIn("For engine 'ansible.utils.sample' error loading the corresponding validate plugin", result["msg"]) + + # invalid data option value + self._plugin._task.args = { + "engine": "ansible.utils.jsonschema", + "data": "invalid data", + "criteria": CRITERIA_OPER_STATUS_UP_CHECK + } + + with self.assertRaises(AnsibleActionFail) as error: + self._plugin.run(task_vars=None) + self.assertIn( + "'data' option value is invalid, value should of type dict or str format of dict", str(error.exception) + ) + + # invalid criteria option value + self._plugin._task.args = { + "engine": "ansible.utils.jsonschema", + "data": DATA, + "criteria": "invalid criteria" + } + + with self.assertRaises(AnsibleActionFail) as error: + self._plugin.run(task_vars=None) + self.assertIn( + "'criteria' option value is invalid, value should of type dict or str format of dict", str(error.exception) + ) + + def test_invalid_validate_plugin_config_options(self): + """Check passing invalid validate plugin options""" + + self._plugin._task.args = { + "engine": "ansible.utils.jsonschema", + "data": DATA, + "criteria": CRITERIA_IN_RATE_CHECK + } + + result = self._plugin.run(task_vars={'ansible_validate_jsonschema_draft': 'draft0'}) + self.assertIn("value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0", result["msg"]) + + def test_invalid_data(self): + """Check passing invalid data as per criteria""" + + self._plugin._task.args = { + "engine": "ansible.utils.jsonschema", + "data": DATA, + "criteria": [CRITERIA_CRC_ERROR_CHECK, CRITERIA_ENABLED_CHECK, CRITERIA_OPER_STATUS_UP_CHECK] + } + + result = self._plugin.run(task_vars=None) + self.assertIn("patternProperties.^.*.properties.counters.properties.in_crc_errors.maximum", result["msg"]) + self.assertIn("patternProperties.^.*.properties.enabled.enum", result["msg"]) + self.assertIn("'patternProperties.^.*.properties.oper_status.pattern", result["msg"]) + + def test_valid_data(self): + """Check passing valid data as per criteria""" + + self._plugin._task.args = { + "engine": "ansible.utils.jsonschema", + "data": DATA, + "criteria": CRITERIA_IN_RATE_CHECK + } + + result = self._plugin.run(task_vars=None) + self.assertIn("all checks passed", result["msg"]) diff --git a/tests/unit/plugins/filter/test_validate.py b/tests/unit/plugins/filter/test_validate.py new file mode 100644 index 0000000..4adc65b --- /dev/null +++ b/tests/unit/plugins/filter/test_validate.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.filter.validate import validate + +DATA = { + "GigabitEthernet0/0/0/0": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 0, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "configured using Ansible", + "duplex_mode": "full", + "enabled": True, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "down", + "type": "GigabitEthernet" + }, + "GigabitEthernet0/0/0/1": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 10, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "# interface is configures with Ansible", + "duplex_mode": "full", + "enabled": False, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "up", + "type": "GigabitEthernet" + } +} + +CRITERIA_CRC_ERROR_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "in_crc_errors": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } +} + +CRITERIA_ENABLED_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "enabled": { + "enum": [True] + } + } + } + } +} + +CRITERIA_OPER_STATUS_UP_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "oper_status": { + "type": "string", + "pattern": "up" + } + } + } + } +} + +CRITERIA_IN_RATE_CHECK = { + "type": "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "rate": { + "properties": { + "in_rate": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } + } + } +} + + +class TestValidate(unittest.TestCase): + def setUp(self): + pass + + def test_invalid_argspec(self): + """Check passing invalid argspec""" + + # missing required arguments + args = [DATA] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + validate(*args, **kwargs) + self.assertIn( + "Missing either 'data' or 'criteria' value in filter input, refer 'ansible.utils.validate' filter", str(error.exception) + ) + + # missing required arguments + with self.assertRaises(AnsibleFilterError) as error: + validate([DATA], {}) + self.assertIn( + "Expected value of 'data' option is either dict or str, received type", str(error.exception) + ) + + args = [DATA, [CRITERIA_IN_RATE_CHECK]] + kwargs = {'engine': 'ansible.utils.sample'} + with self.assertRaises(AnsibleFilterError) as error: + validate(*args, **kwargs) + self.assertIn( + "For engine 'ansible.utils.sample' error loading", str(error.exception) + ) + + args = ["invalid data", [CRITERIA_IN_RATE_CHECK]] + kwargs = {'engine': 'ansible.utils.jsonschema'} + with self.assertRaises(AnsibleFilterError) as error: + validate(*args, **kwargs) + self.assertIn( + "'data' option value is invalid", str(error.exception) + ) + + args = [DATA, "invalid criteria"] + kwargs = {'engine': 'ansible.utils.jsonschema'} + with self.assertRaises(AnsibleFilterError) as error: + validate(*args, **kwargs) + self.assertIn( + "'criteria' option value is invalid", str(error.exception) + ) + + def test_invalid_validate_plugin_config_options(self): + """Check passing invalid validate plugin options""" + + args = [DATA, [CRITERIA_CRC_ERROR_CHECK, CRITERIA_ENABLED_CHECK, CRITERIA_OPER_STATUS_UP_CHECK]] + kwargs = {'engine': 'ansible.utils.jsonschema', 'draft': 'draft0'} + with self.assertRaises(AnsibleFilterError) as error: + validate(*args, **kwargs) + self.assertIn( + "value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0", str(error.exception) + ) + + def test_valid_data(self): + """Check passing valid data as per criteria""" + + args = [DATA, CRITERIA_IN_RATE_CHECK] + kwargs = {'engine': 'ansible.utils.jsonschema'} + result = validate(*args, **kwargs) + self.assertEqual(result, []) diff --git a/tests/unit/plugins/lookup/test_validate.py b/tests/unit/plugins/lookup/test_validate.py new file mode 100644 index 0000000..e051c3b --- /dev/null +++ b/tests/unit/plugins/lookup/test_validate.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible.errors import AnsibleLookupError +from ansible_collections.ansible.utils.plugins.lookup.validate import LookupModule + +DATA = { + "GigabitEthernet0/0/0/0": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 0, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "configured using Ansible", + "duplex_mode": "full", + "enabled": True, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "down", + "type": "GigabitEthernet" + }, + "GigabitEthernet0/0/0/1": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 10, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "# interface is configures with Ansible", + "duplex_mode": "full", + "enabled": False, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "up", + "type": "GigabitEthernet" + } +} + +CRITERIA_CRC_ERROR_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "in_crc_errors": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } +} + +CRITERIA_ENABLED_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "enabled": { + "enum": [True] + } + } + } + } +} + +CRITERIA_OPER_STATUS_UP_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "oper_status": { + "type": "string", + "pattern": "up" + } + } + } + } +} + +CRITERIA_IN_RATE_CHECK = { + "type": "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "rate": { + "properties": { + "in_rate": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } + } + } +} + + +class TestValidate(unittest.TestCase): + def setUp(self): + self._lp = LookupModule() + + def test_invalid_argspec(self): + """Check passing invalid argspec""" + + # missing required arguments + with self.assertRaises(AnsibleLookupError) as error: + self._lp.run([DATA], {}) + self.assertIn( + "missing either 'data' or 'criteria' value", str(error.exception) + ) + + terms = [DATA, [CRITERIA_IN_RATE_CHECK]] + kwargs = {'engine': 'ansible.utils.sample'} + variables = {} + with self.assertRaises(AnsibleLookupError) as error: + self._lp.run(terms, variables, **kwargs) + self.assertIn( + "For engine 'ansible.utils.sample' error loading", str(error.exception) + ) + + terms = ["invalid data", [CRITERIA_IN_RATE_CHECK]] + kwargs = {'engine': 'ansible.utils.jsonschema'} + variables = {} + with self.assertRaises(AnsibleLookupError) as error: + self._lp.run(terms, variables, **kwargs) + self.assertIn( + "'data' option value is invalid", str(error.exception) + ) + + terms = [DATA, "invalid criteria"] + kwargs = {'engine': 'ansible.utils.jsonschema'} + variables = {} + with self.assertRaises(AnsibleLookupError) as error: + self._lp.run(terms, variables, **kwargs) + self.assertIn( + "'criteria' option value is invalid", str(error.exception) + ) + + def test_invalid_validate_plugin_config_options(self): + """Check passing invalid validate plugin options""" + + terms = [DATA, [CRITERIA_CRC_ERROR_CHECK, CRITERIA_ENABLED_CHECK, CRITERIA_OPER_STATUS_UP_CHECK]] + kwargs = {'engine': 'ansible.utils.jsonschema'} + variables = {} + result = self._lp.run(terms, variables, **kwargs) + self.assertIn("GigabitEthernet0/0/0/1.counters.in_crc_errors", result[0]['data_path']) + self.assertIn("GigabitEthernet0/0/0/1.enabled", result[1]['data_path']) + self.assertIn("GigabitEthernet0/0/0/0.oper_status", result[2]['data_path']) + + def test_valid_data(self): + """Check passing valid data as per criteria""" + + terms = [DATA, CRITERIA_IN_RATE_CHECK] + kwargs = {'engine': 'ansible.utils.jsonschema'} + variables = {} + result = self._lp.run(terms, variables, **kwargs) + self.assertEqual(result, []) + diff --git a/tests/unit/plugins/test/test_validate.py b/tests/unit/plugins/test/test_validate.py new file mode 100644 index 0000000..0459cc0 --- /dev/null +++ b/tests/unit/plugins/test/test_validate.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible.errors import AnsibleError +from ansible_collections.ansible.utils.plugins.test.validate import validate + +DATA = { + "GigabitEthernet0/0/0/0": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 0, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "configured using Ansible", + "duplex_mode": "full", + "enabled": True, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "down", + "type": "GigabitEthernet" + }, + "GigabitEthernet0/0/0/1": { + "auto_negotiate": False, + "counters": { + "in_crc_errors": 10, + "in_errors": 0, + "rate": { + "in_rate": 0, + "out_rate": 0 + } + }, + "description": "# interface is configures with Ansible", + "duplex_mode": "full", + "enabled": False, + "line_protocol": "up", + "mtu": 1514, + "oper_status": "up", + "type": "GigabitEthernet" + } +} + +CRITERIA_CRC_ERROR_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "in_crc_errors": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } +} + +CRITERIA_ENABLED_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "enabled": { + "enum": [True] + } + } + } + } +} + +CRITERIA_OPER_STATUS_UP_CHECK = { + "type" : "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "oper_status": { + "type": "string", + "pattern": "up" + } + } + } + } +} + +CRITERIA_IN_RATE_CHECK = { + "type": "object", + "patternProperties": { + "^.*": { + "type": "object", + "properties": { + "counters": { + "properties": { + "rate": { + "properties": { + "in_rate": { + "type": "number", + "maximum": 0 + } + } + } + } + } + } + } + } +} + + +class TestValidate(unittest.TestCase): + def setUp(self): + pass + + def test_invalid_argspec(self): + """Check passing invalid argspec""" + + # missing required arguments + args = [DATA] + kwargs = {} + with self.assertRaises(AnsibleError) as error: + validate(*args, **kwargs) + self.assertIn( + "missing required arguments: criteria", str(error.exception) + ) + + kwargs = {'criteria': CRITERIA_IN_RATE_CHECK, 'engine': 'ansible.utils.sample'} + with self.assertRaises(AnsibleError) as error: + validate(*args, **kwargs) + self.assertIn( + "For engine 'ansible.utils.sample' error loading", str(error.exception) + ) + + args = ["invalid data"] + kwargs = {'criteria': [CRITERIA_IN_RATE_CHECK], 'engine': 'ansible.utils.jsonschema'} + with self.assertRaises(AnsibleError) as error: + validate(*args, **kwargs) + self.assertIn( + "'data' option value is invalid", str(error.exception) + ) + + args = [DATA] + kwargs = {'criteria': 'invalid criteria', 'engine': 'ansible.utils.jsonschema'} + with self.assertRaises(AnsibleError) as error: + validate(*args, **kwargs) + self.assertIn( + "'criteria' option value is invalid", str(error.exception) + ) + + def test_invalid_validate_plugin_config_options(self): + """Check passing invalid validate plugin options""" + args = [DATA] + kwargs = {'criteria': 'invalid criteria', 'engine': 'ansible.utils.jsonschema', 'draft': 'draft0'} + + with self.assertRaises(AnsibleError) as error: + validate(*args, **kwargs) + self.assertIn( + "value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0", str(error.exception) + ) + + def test_invalid_data(self): + """Check passing invalid data as per criteria""" + args = [DATA] + kwargs = {'criteria': [CRITERIA_ENABLED_CHECK, CRITERIA_OPER_STATUS_UP_CHECK, CRITERIA_CRC_ERROR_CHECK], 'engine': 'ansible.utils.jsonschema'} + result = validate(*args, **kwargs) + self.assertEqual(result, False) + + def test_valid_data(self): + """Check passing valid data as per criteria""" + args = [DATA] + kwargs = {'criteria': CRITERIA_IN_RATE_CHECK, 'engine': 'ansible.utils.jsonschema'} + result = validate(*args, **kwargs) + self.assertEqual(result, True) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a95eb6f --- /dev/null +++ b/tox.ini @@ -0,0 +1,31 @@ +[tox] +minversion = 1.4.2 +envlist = linters +skipsdist = True + +[testenv] +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +[testenv:black] +install_command = pip install {opts} {packages} +commands = + black -v -l79 {toxinidir} + +[testenv:linters] +install_command = pip install {opts} {packages} +commands = + black -v -l79 --check {toxinidir} + flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125,E402,W503 +max-line-length = 160 +builtins = _ +exclude = .git,.tox,tests/unit/compat/