Add validate action/lookup/filter/test plugin (#13)

* Initial commit

* Add validate lookup/filter/test plugin

*  Update agrspec validation logic
*  Add validate lookup/filter/test plugin

* minor updates

* remve unwanted code

* doc update

* update jsonschema validator plugin

* Add validate sub-plugin configurable option

* fix black failures

* fix ansible-test validate module issue

* fix santiy issues

* more santiy fixes

* remove GA black formatting

* add black

* fix black formatting

* fix black issues

* Add integration test and fix review comments

* add jsonschema to requirments

* fix ci issues

* update GA to install requirments

* fix GA to install requirments

* move ValidateBase to base file

* fix library not found issue in CI

* add unit test
pull/19/head
Ganesh Nalawade 2020-10-29 02:09:20 +05:30 committed by GitHub
parent c20ba34c7d
commit 1807ee7c7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 2930 additions and 370 deletions

View File

@ -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
run: black -l79 --diff --check ansible_collections/ansible/utils

View File

@ -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

15
.yamllint Normal file
View File

@ -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

View File

@ -1,3 +1,3 @@
---
minor_changes:
- Add fact_diff module. Find the difference between text, files or facts
major_changes:
- Added validate module/lookup/filter/test plugin to validate data based on given criteria

View File

View File

@ -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,

View File

@ -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:

107
plugins/action/validate.py Normal file
View File

@ -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

View File

@ -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")

View File

@ -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:

View File

@ -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:

View File

@ -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:

145
plugins/filter/validate.py Normal file
View File

@ -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(<org-name>.<collection-name>.<validator-plugin-name>).
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}

View File

@ -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:

View File

@ -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:

View File

@ -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:

154
plugins/lookup/validate.py Normal file
View File

@ -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(<org-name>.<collection-name>.<validate-plugin-name>).
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", []))

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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 <org-name>.<collection-name>.<validate-plugin>
: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

View File

@ -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
<org-name>.<collection-name>.<validate-plugin-name>.
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
"""

154
plugins/test/validate.py Normal file
View File

@ -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(<org-name>.<collection-name>.<validate-plugin-name>).
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

140
plugins/validate/_base.py Normal file
View File

@ -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)

View File

@ -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)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
ansible
# The follow are 3rd party libs for validate
jsonschema
# /valiate

9
test-requirements.txt Normal file
View File

@ -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

View File

@ -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 }}"
that: "{{ 'unable to convert to list' in result.msg }}"

View File

@ -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

View File

@ -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"
msg: "The regex '+', is not valid"

View File

@ -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

View File

@ -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

View File

@ -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: ''

View File

@ -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: ''

View File

@ -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] }}"

View File

@ -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

View File

@ -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
- "missing required arguments:"
- data
- test

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 }}"

View File

@ -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) }}"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,18 @@
{
"type" : "object",
"patternProperties": {
"^.*": {
"type": "object",
"properties": {
"counters": {
"properties": {
"in_crc_errors": {
"type": "number",
"maximum": 0
}
}
}
}
}
}
}

View File

@ -0,0 +1,13 @@
{
"type" : "object",
"patternProperties": {
"^.*": {
"type": "object",
"properties": {
"enabled": {
"enum": [true]
}
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"type" : "object",
"patternProperties": {
"^.*": {
"type": "object",
"properties": {
"counters": {
"properties": {
"rate": {
"properties": {
"in_rate": {
"type": "number",
"maximum": 0
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"type" : "object",
"patternProperties": {
"^.*": {
"type": "object",
"properties": {
"oper_status": {
"type": "string",
"pattern": "up"
}
}
}
}
}

View File

@ -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"
}
}

View File

@ -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 == []"

View File

@ -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 == []"

View File

@ -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"

View File

@ -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"

View File

@ -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 }}"

View File

@ -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

View File

@ -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,

View File

@ -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"""

View File

@ -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):

View File

@ -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"])

View File

@ -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, [])

View File

@ -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, [])

View File

@ -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)

31
tox.ini Normal file
View File

@ -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/