Update jsonschema validator (#226)
* Update jsonschema validator Update jsonschema validator to support newer drafts. It now supports drafts 2019-09 and 2020-12. The logic for choosing the jsonschema validator class has changed so that the following enhancements are available: - When no draft is explicitly specified we now use use the validator draft that is specified in the "$schema" field of the criteria. This is done by the jsonschema module by default and should support possible future drafts without any changes to this code. - Optionally allow to disable format checks in the code. As format checks are not required by the spec there might be situations where people want to disable them. * Update requirements.txt * Skip all tests which dependas on jsonschema 4.5 * jsonschema: Refactor code to support python 3.6 * Fix jsonschema requirements for python<3.7 * jsonschema: Update code for compatibility * Better documentation and error handling for missing schema specifications * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ashwini Mhatre <amhatre@redhat.com> Co-authored-by: ashwini-mhatre <mashu97@gmail.com>pull/254/head
parent
ed6555526d
commit
c2231a6b20
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
minor_changes:
|
||||||
|
- validate - Add support for JSON Schema draft 2019-09 and 2020-12 as well as automatically choosing the draft
|
||||||
|
from the `$schema` field of the criteria.
|
||||||
|
- validate - Add option `check_format` for the jsonschema engine to disable JSON Schema format checking.
|
|
@ -13,4 +13,4 @@ issues: https://github.com/ansible-collections/ansible.utils/issues
|
||||||
tags: [linux, networking, security, cloud, utilities, data, validation, utils]
|
tags: [linux, networking, security, cloud, utilities, data, validation, utils]
|
||||||
# NOTE(pabelanger): We create an empty version key to keep ansible-galaxy
|
# NOTE(pabelanger): We create an empty version key to keep ansible-galaxy
|
||||||
# happy. We dynamically inject version info based on git information.
|
# happy. We dynamically inject version info based on git information.
|
||||||
version: 2.9.1-dev
|
version: 2.10.0-dev
|
||||||
|
|
|
@ -22,17 +22,29 @@ DOCUMENTATION = """
|
||||||
description:
|
description:
|
||||||
- This option provides the jsonschema specification that should be used
|
- This option provides the jsonschema specification that should be used
|
||||||
for the validating the data. The I(criteria) option in the validate
|
for the validating the data. The I(criteria) option in the validate
|
||||||
plugin should follow the specification as mentioned by this option
|
plugin should follow the specification as mentioned by this option.
|
||||||
default: draft7
|
If this option is not specified, jsonschema will use the best validator
|
||||||
|
for the I($schema) field in the criteria. Specifications 2019-09 and
|
||||||
|
2020-12 are only available from jsonschema version 4.0 onwards.
|
||||||
choices:
|
choices:
|
||||||
- draft3
|
- draft3
|
||||||
- draft4
|
- draft4
|
||||||
- draft6
|
- draft6
|
||||||
- draft7
|
- draft7
|
||||||
|
- 2019-09
|
||||||
|
- 2020-12
|
||||||
env:
|
env:
|
||||||
- name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT
|
- name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT
|
||||||
vars:
|
vars:
|
||||||
- name: ansible_validate_jsonschema_draft
|
- name: ansible_validate_jsonschema_draft
|
||||||
|
check_format:
|
||||||
|
description: If enabled, validate the I(format) specification in the criteria.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_VALIDATE_JSONSCHEMA_CHECK_FORMAT
|
||||||
|
vars:
|
||||||
|
- name: ansible_validate_jsonschema_check_format
|
||||||
notes:
|
notes:
|
||||||
- The value of I(data) option should be either a valid B(JSON) object or a B(JSON) string.
|
- The value of I(data) option should be either a valid B(JSON) object or a B(JSON) string.
|
||||||
- The value of I(criteria) should be B(list) of B(dict) or B(list) of B(strings) and each
|
- The value of I(criteria) should be B(list) of B(dict) or B(list) of B(strings) and each
|
||||||
|
@ -45,11 +57,14 @@ from ansible.errors import AnsibleError
|
||||||
from ansible.module_utils._text import to_text
|
from ansible.module_utils._text import to_text
|
||||||
from ansible.module_utils.basic import missing_required_lib
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
from ansible.module_utils.six import string_types
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
from ansible_collections.ansible.utils.plugins.module_utils.common.utils import to_list
|
from ansible_collections.ansible.utils.plugins.module_utils.common.utils import to_list
|
||||||
from ansible_collections.ansible.utils.plugins.plugin_utils.base.validate import ValidateBase
|
from ansible_collections.ansible.utils.plugins.plugin_utils.base.validate import ValidateBase
|
||||||
|
|
||||||
|
|
||||||
|
display = Display()
|
||||||
|
|
||||||
# PY2 compatibility for JSONDecodeError
|
# PY2 compatibility for JSONDecodeError
|
||||||
try:
|
try:
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
@ -58,6 +73,7 @@ except ImportError:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
import jsonschema.validators
|
||||||
|
|
||||||
HAS_JSONSCHEMA = True
|
HAS_JSONSCHEMA = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -79,6 +95,34 @@ def json_path(absolute_path):
|
||||||
|
|
||||||
|
|
||||||
class Validate(ValidateBase):
|
class Validate(ValidateBase):
|
||||||
|
# All available schema versions with the format_check and validator class names.
|
||||||
|
_JSONSCHEMA_DRAFTS = {
|
||||||
|
"draft3": {
|
||||||
|
"validator_name": "Draft3Validator",
|
||||||
|
"format_checker_name": "draft3_format_checker",
|
||||||
|
},
|
||||||
|
"draft4": {
|
||||||
|
"validator_name": "Draft4Validator",
|
||||||
|
"format_checker_name": "draft4_format_checker",
|
||||||
|
},
|
||||||
|
"draft6": {
|
||||||
|
"validator_name": "Draft6Validator",
|
||||||
|
"format_checker_name": "draft6_format_checker",
|
||||||
|
},
|
||||||
|
"draft7": {
|
||||||
|
"validator_name": "Draft7Validator",
|
||||||
|
"format_checker_name": "draft7_format_checker",
|
||||||
|
},
|
||||||
|
"2019-09": {
|
||||||
|
"validator_name": "Draft201909Validator",
|
||||||
|
"format_checker_name": "draft201909_format_checker",
|
||||||
|
},
|
||||||
|
"2020-12": {
|
||||||
|
"validator_name": "Draft202012Validator",
|
||||||
|
"format_checker_name": "draft202012_format_checker",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_reqs():
|
def _check_reqs():
|
||||||
"""Check the prerequisites are installed for jsonschema
|
"""Check the prerequisites are installed for jsonschema
|
||||||
|
@ -126,6 +170,28 @@ class Validate(ValidateBase):
|
||||||
)
|
)
|
||||||
raise AnsibleError(msg)
|
raise AnsibleError(msg)
|
||||||
|
|
||||||
|
def _check_drafts(self):
|
||||||
|
"""For every possible draft check if our jsonschema version supports it and exchange the class names with
|
||||||
|
the actual classes. If it is not supported the draft is removed from the list.
|
||||||
|
"""
|
||||||
|
for draft in list(self._JSONSCHEMA_DRAFTS.keys()):
|
||||||
|
draft_config = self._JSONSCHEMA_DRAFTS[draft]
|
||||||
|
try:
|
||||||
|
validator_class = getattr(jsonschema, draft_config["validator_name"])
|
||||||
|
except AttributeError:
|
||||||
|
display.vvv(
|
||||||
|
'jsonschema draft "{draft}" not supported in this version'.format(draft=draft),
|
||||||
|
)
|
||||||
|
del self._JSONSCHEMA_DRAFTS[draft]
|
||||||
|
continue
|
||||||
|
draft_config["validator"] = validator_class
|
||||||
|
try:
|
||||||
|
format_checker_class = validator_class.FORMAT_CHECKER
|
||||||
|
except AttributeError:
|
||||||
|
# Older jsonschema version
|
||||||
|
format_checker_class = getattr(jsonschema, draft_config["format_checker_name"])
|
||||||
|
draft_config["format_checker"] = format_checker_class
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
"""Std entry point for a validate execution
|
"""Std entry point for a validate execution
|
||||||
|
|
||||||
|
@ -141,6 +207,7 @@ class Validate(ValidateBase):
|
||||||
"""
|
"""
|
||||||
self._check_reqs()
|
self._check_reqs()
|
||||||
self._check_args()
|
self._check_args()
|
||||||
|
self._check_drafts()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._validate_jsonschema()
|
self._validate_jsonschema()
|
||||||
|
@ -153,30 +220,45 @@ class Validate(ValidateBase):
|
||||||
error_messages = None
|
error_messages = None
|
||||||
|
|
||||||
draft = self._get_sub_plugin_options("draft")
|
draft = self._get_sub_plugin_options("draft")
|
||||||
|
check_format = self._get_sub_plugin_options("check_format")
|
||||||
error_messages = []
|
error_messages = []
|
||||||
|
|
||||||
for criteria in self._criteria:
|
for criteria in self._criteria:
|
||||||
if draft == "draft3":
|
format_checker = None
|
||||||
validator = jsonschema.Draft3Validator(
|
validator_class = None
|
||||||
criteria,
|
if draft is not None:
|
||||||
format_checker=jsonschema.draft3_format_checker,
|
try:
|
||||||
)
|
validator_class = self._JSONSCHEMA_DRAFTS[draft]["validator"]
|
||||||
elif draft == "draft4":
|
except KeyError:
|
||||||
validator = jsonschema.Draft4Validator(
|
display.warning(
|
||||||
criteria,
|
'No validator available for "{draft}", falling back to autodetection. A newer version of jsonschema might support this draft.'.format(
|
||||||
format_checker=jsonschema.draft4_format_checker,
|
draft=draft,
|
||||||
)
|
),
|
||||||
elif draft == "draft6":
|
|
||||||
validator = jsonschema.Draft6Validator(
|
|
||||||
criteria,
|
|
||||||
format_checker=jsonschema.draft6_format_checker,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
validator = jsonschema.Draft7Validator(
|
|
||||||
criteria,
|
|
||||||
format_checker=jsonschema.draft7_format_checker,
|
|
||||||
)
|
)
|
||||||
|
if validator_class is None:
|
||||||
|
# Either no draft was specified or specified draft has no validator class
|
||||||
|
# in installed jsonschema version. Do autodetection instead.
|
||||||
|
validator_class = jsonschema.validators.validator_for(criteria)
|
||||||
|
|
||||||
|
if check_format:
|
||||||
|
try:
|
||||||
|
format_checker = validator_class.FORMAT_CHECKER
|
||||||
|
except AttributeError:
|
||||||
|
# TODO: Remove when Python 3.6 support is dropped.
|
||||||
|
# On jsonschema<4.5, there is no connection between a validator and the correct format checker.
|
||||||
|
# So we iterate through our known list of validators and if one matches the current class
|
||||||
|
# we use the format_checker from that validator.
|
||||||
|
for draft, draft_config in self._JSONSCHEMA_DRAFTS.items():
|
||||||
|
if validator_class == draft_config["validator"]:
|
||||||
|
display.vvv(
|
||||||
|
"Using format_checker for {draft} validator".format(draft=draft),
|
||||||
|
)
|
||||||
|
format_checker = draft_config["format_checker"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
display.warning("jsonschema format checks not available")
|
||||||
|
|
||||||
|
validator = validator_class(criteria, format_checker=format_checker)
|
||||||
validation_errors = sorted(validator.iter_errors(self._data), key=lambda e: e.path)
|
validation_errors = sorted(validator.iter_errors(self._data), key=lambda e: e.path)
|
||||||
|
|
||||||
if validation_errors:
|
if validation_errors:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
jsonschema ; python_version >= '2.7'
|
jsonschema
|
||||||
netaddr
|
netaddr
|
||||||
ttp
|
ttp
|
||||||
textfsm
|
textfsm
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||||
|
|
||||||
- name: invalid engine value
|
- name: invalid engine value
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||||
|
|
||||||
- name: test invalid plugin configuration option, passed as task varaible
|
- name: test invalid plugin configuration option, passed as task varaible
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||||
|
|
||||||
- name: invalid engine value
|
- name: invalid engine value
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
that:
|
that:
|
||||||
- "'errors' not in result"
|
- "'errors' not in result"
|
||||||
- "result['failed'] == true"
|
- "result['failed'] == true"
|
||||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||||
|
|
||||||
- name: invalid engine value
|
- name: invalid engine value
|
||||||
ansible.utils.validate:
|
ansible.utils.validate:
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
- ansible.builtin.assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||||
|
|
||||||
- name: invalid engine value
|
- name: invalid engine value
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
@ -186,7 +186,7 @@ class TestValidate(unittest.TestCase):
|
||||||
|
|
||||||
result = self._plugin.run(task_vars={"ansible_validate_jsonschema_draft": "draft0"})
|
result = self._plugin.run(task_vars={"ansible_validate_jsonschema_draft": "draft0"})
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||||
result["msg"],
|
result["msg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -285,3 +285,15 @@ class TestValidate(unittest.TestCase):
|
||||||
|
|
||||||
result = self._plugin.run(task_vars=None)
|
result = self._plugin.run(task_vars=None)
|
||||||
self.assertIn("Validation errors were found", result["msg"])
|
self.assertIn("Validation errors were found", result["msg"])
|
||||||
|
|
||||||
|
def test_support_for_disabled_format_with_invalid_data(self):
|
||||||
|
"""Check passing valid data as per criteria"""
|
||||||
|
|
||||||
|
self._plugin._task.args = {
|
||||||
|
"engine": "ansible.utils.jsonschema",
|
||||||
|
"data": IN_VALID_DATA,
|
||||||
|
"criteria": CRITERIA_FORMAT_SUPPORT_CHECK,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._plugin.run(task_vars=dict(ansible_validate_jsonschema_check_format=False))
|
||||||
|
self.assertIn("All checks passed", result["msg"])
|
||||||
|
|
|
@ -150,7 +150,7 @@ class TestValidate(unittest.TestCase):
|
||||||
with self.assertRaises(AnsibleFilterError) as error:
|
with self.assertRaises(AnsibleFilterError) as error:
|
||||||
validate(*args, **kwargs)
|
validate(*args, **kwargs)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||||
str(error.exception),
|
str(error.exception),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,7 @@ class TestValidate(unittest.TestCase):
|
||||||
with self.assertRaises(AnsibleError) as error:
|
with self.assertRaises(AnsibleError) as error:
|
||||||
validate(*args, **kwargs)
|
validate(*args, **kwargs)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||||
str(error.exception),
|
str(error.exception),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
jsonschema ; python_version >= '2.7'
|
jsonschema
|
||||||
netaddr
|
netaddr
|
||||||
textfsm
|
textfsm
|
||||||
ttp
|
ttp
|
||||||
|
|
Loading…
Reference in New Issue