mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
add custom module type validation (#27183)
* Module argument_spec now accepts a callable for the type argument, which is passed through and called with the value when appropriate. On validation/conversion failure, the name of the callable (or its type as a fallback) is used in the error message. * adds basic smoke tests for custom callable validator functionality
This commit is contained in:
parent
53ebe8d441
commit
3f1ec6b862
3 changed files with 67 additions and 11 deletions
|
@ -33,6 +33,7 @@ Ansible Changes By Release
|
||||||
- Also added an ansible-config CLI to allow for listing config options and dumping current config (including origin)
|
- Also added an ansible-config CLI to allow for listing config options and dumping current config (including origin)
|
||||||
- TODO: build upon this to add many features detailed in ansible-config proposal https://github.com/ansible/proposals/issues/35
|
- TODO: build upon this to add many features detailed in ansible-config proposal https://github.com/ansible/proposals/issues/35
|
||||||
* Windows modules now support the use of multiple shared module_utils files in the form of Powershell modules (.psm1), via `#Requires -Module Ansible.ModuleUtils.Whatever.psm1`
|
* Windows modules now support the use of multiple shared module_utils files in the form of Powershell modules (.psm1), via `#Requires -Module Ansible.ModuleUtils.Whatever.psm1`
|
||||||
|
* Python module argument_spec now supports custom validation logic by accepting a callable as the `type` argument.
|
||||||
|
|
||||||
### Deprecations
|
### Deprecations
|
||||||
* The behaviour when specifying `--tags` (or `--skip-tags`) multiple times on the command line
|
* The behaviour when specifying `--tags` (or `--skip-tags`) multiple times on the command line
|
||||||
|
|
|
@ -1874,22 +1874,28 @@ class AnsibleModule(object):
|
||||||
wanted = v.get('type', None)
|
wanted = v.get('type', None)
|
||||||
if k not in param:
|
if k not in param:
|
||||||
continue
|
continue
|
||||||
if wanted is None:
|
|
||||||
# Mostly we want to default to str.
|
|
||||||
# For values set to None explicitly, return None instead as
|
|
||||||
# that allows a user to unset a parameter
|
|
||||||
if param[k] is None:
|
|
||||||
continue
|
|
||||||
wanted = 'str'
|
|
||||||
|
|
||||||
value = param[k]
|
value = param[k]
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
if not callable(wanted):
|
||||||
type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted]
|
if wanted is None:
|
||||||
except KeyError:
|
# Mostly we want to default to str.
|
||||||
self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
|
# For values set to None explicitly, return None instead as
|
||||||
|
# that allows a user to unset a parameter
|
||||||
|
if param[k] is None:
|
||||||
|
continue
|
||||||
|
wanted = 'str'
|
||||||
|
try:
|
||||||
|
type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted]
|
||||||
|
except KeyError:
|
||||||
|
self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
|
||||||
|
else:
|
||||||
|
# set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock)
|
||||||
|
type_checker = wanted
|
||||||
|
wanted = getattr(wanted, '__name__', to_native(type(wanted)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
param[k] = type_checker(value)
|
param[k] = type_checker(value)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
|
|
49
test/units/module_utils/basic/test_argument_spec.py
Normal file
49
test/units/module_utils/basic/test_argument_spec.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ansible.compat.tests import unittest
|
||||||
|
from ansible.compat.tests.mock import MagicMock
|
||||||
|
from units.mock.procenv import swap_stdin_and_argv, swap_stdout
|
||||||
|
from ansible.module_utils import basic
|
||||||
|
|
||||||
|
|
||||||
|
class TestCallableTypeValidation(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
args = json.dumps(dict(ANSIBLE_MODULE_ARGS=dict(arg="42")))
|
||||||
|
self.stdin_swap_ctx = swap_stdin_and_argv(stdin_data=args)
|
||||||
|
self.stdin_swap_ctx.__enter__()
|
||||||
|
|
||||||
|
# since we can't use context managers and "with" without overriding run(), call them directly
|
||||||
|
self.stdout_swap_ctx = swap_stdout()
|
||||||
|
self.fake_stream = self.stdout_swap_ctx.__enter__()
|
||||||
|
|
||||||
|
basic._ANSIBLE_ARGS = None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# since we can't use context managers and "with" without overriding run(), call them directly to clean up
|
||||||
|
self.stdin_swap_ctx.__exit__(None, None, None)
|
||||||
|
self.stdout_swap_ctx.__exit__(None, None, None)
|
||||||
|
|
||||||
|
def test_validate_success(self):
|
||||||
|
mock_validator = MagicMock(return_value=42)
|
||||||
|
m = basic.AnsibleModule(argument_spec=dict(
|
||||||
|
arg=dict(type=mock_validator)
|
||||||
|
))
|
||||||
|
|
||||||
|
self.assertTrue(mock_validator.called)
|
||||||
|
self.assertEqual(m.params['arg'], 42)
|
||||||
|
self.assertEqual(type(m.params['arg']), int)
|
||||||
|
|
||||||
|
def test_validate_fail(self):
|
||||||
|
mock_validator = MagicMock(side_effect=TypeError("bad conversion"))
|
||||||
|
with self.assertRaises(SystemExit) as ecm:
|
||||||
|
m = basic.AnsibleModule(argument_spec=dict(
|
||||||
|
arg=dict(type=mock_validator)
|
||||||
|
))
|
||||||
|
|
||||||
|
self.assertIn("bad conversion", json.loads(self.fake_stream.getvalue())['msg'])
|
Loading…
Reference in a new issue