|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Copyright: (c) 2015, Matt Martz <matt@sivel.net>
|
|
|
|
# Copyright: (c) 2015, Rackspace US, Inc.
|
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
from ansible.module_utils.compat.version import StrictVersion
|
|
|
|
from functools import partial
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid, Exclusive
|
|
|
|
from ansible.constants import DOCUMENTABLE_PLUGINS
|
|
|
|
from ansible.module_utils.six import string_types
|
|
|
|
from ansible.module_utils.common.collections import is_iterable
|
|
|
|
from ansible.module_utils.parsing.convert_bool import boolean
|
|
|
|
from ansible.parsing.quoting import unquote
|
|
|
|
from ansible.utils.version import SemanticVersion
|
|
|
|
from ansible.release import __version__
|
|
|
|
|
|
|
|
from .utils import parse_isodate
|
|
|
|
|
|
|
|
list_string_types = list(string_types)
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
5 years ago
|
|
|
tuple_string_types = tuple(string_types)
|
|
|
|
any_string_types = Any(*string_types)
|
|
|
|
|
|
|
|
# Valid DOCUMENTATION.author lines
|
|
|
|
# Based on Ansibulbot's extract_github_id()
|
|
|
|
# author: First Last (@name) [optional anything]
|
|
|
|
# "Ansible Core Team" - Used by the Bot
|
|
|
|
# "Michael DeHaan" - nop
|
|
|
|
# "OpenStack Ansible SIG" - OpenStack does not use GitHub
|
|
|
|
# "Name (!UNKNOWN)" - For the few untraceable authors
|
|
|
|
author_line = re.compile(r'^\w.*(\(@([\w-]+)\)|!UNKNOWN)(?![\w.])|^Ansible Core Team$|^Michael DeHaan$|^OpenStack Ansible SIG$')
|
|
|
|
|
|
|
|
|
|
|
|
def _add_ansible_error_code(exception, error_code):
|
|
|
|
setattr(exception, 'ansible_error_code', error_code)
|
|
|
|
return exception
|
|
|
|
|
|
|
|
|
|
|
|
def isodate(v, error_code=None):
|
|
|
|
try:
|
|
|
|
parse_isodate(v, allow_date=True)
|
|
|
|
except ValueError as e:
|
|
|
|
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'ansible-invalid-date')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
COLLECTION_NAME_RE = re.compile(r'^\w+(?:\.\w+)+$')
|
|
|
|
FULLY_QUALIFIED_COLLECTION_RESOURCE_RE = re.compile(r'^\w+(?:\.\w+){2,}$')
|
|
|
|
|
|
|
|
|
|
|
|
def collection_name(v, error_code=None):
|
|
|
|
if not isinstance(v, string_types):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Collection name must be a string'), error_code or 'collection-invalid-name')
|
|
|
|
m = COLLECTION_NAME_RE.match(v)
|
|
|
|
if not m:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Collection name must be of format `<namespace>.<name>`'), error_code or 'collection-invalid-name')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def deprecation_versions():
|
|
|
|
"""Create a list of valid version for deprecation entries, current+4"""
|
|
|
|
major, minor = [int(version) for version in __version__.split('.')[0:2]]
|
|
|
|
return Any(*['{0}.{1}'.format(major, minor + increment) for increment in range(0, 5)])
|
|
|
|
|
|
|
|
|
|
|
|
def version(for_collection=False):
|
|
|
|
if for_collection:
|
|
|
|
# We do not accept floats for versions in collections
|
|
|
|
return Any(*string_types)
|
|
|
|
return Any(float, *string_types)
|
|
|
|
|
|
|
|
|
|
|
|
def date(error_code=None):
|
|
|
|
return Any(isodate, error_code=error_code)
|
|
|
|
|
|
|
|
|
|
|
|
_MODULE = re.compile(r"\bM\(([^)]+)\)")
|
|
|
|
_LINK = re.compile(r"\bL\(([^)]+)\)")
|
|
|
|
_URL = re.compile(r"\bU\(([^)]+)\)")
|
|
|
|
_REF = re.compile(r"\bR\(([^)]+)\)")
|
|
|
|
|
|
|
|
|
|
|
|
def _check_module_link(directive, content):
|
|
|
|
if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(content):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Directive "%s" must contain a FQCN' % directive), 'invalid-documentation-markup')
|
|
|
|
|
|
|
|
|
|
|
|
def _check_link(directive, content):
|
|
|
|
if ',' not in content:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
|
|
|
|
idx = content.rindex(',')
|
|
|
|
title = content[:idx]
|
|
|
|
url = content[idx + 1:].lstrip(' ')
|
|
|
|
_check_url(directive, url)
|
|
|
|
|
|
|
|
|
|
|
|
def _check_url(directive, content):
|
|
|
|
try:
|
|
|
|
parsed_url = urlparse(content)
|
|
|
|
if parsed_url.scheme not in ('', 'http', 'https'):
|
|
|
|
raise ValueError('Schema must be HTTP, HTTPS, or not specified')
|
|
|
|
except ValueError as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Directive "%s" must contain an URL' % directive), 'invalid-documentation-markup')
|
|
|
|
|
|
|
|
|
|
|
|
def _check_ref(directive, content):
|
|
|
|
if ',' not in content:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
|
|
|
|
|
|
|
|
|
|
|
|
def doc_string(v):
|
|
|
|
"""Match a documentation string."""
|
|
|
|
if not isinstance(v, string_types):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Must be a string'), 'invalid-documentation')
|
|
|
|
for m in _MODULE.finditer(v):
|
|
|
|
_check_module_link(m.group(0), m.group(1))
|
|
|
|
for m in _LINK.finditer(v):
|
|
|
|
_check_link(m.group(0), m.group(1))
|
|
|
|
for m in _URL.finditer(v):
|
|
|
|
_check_url(m.group(0), m.group(1))
|
|
|
|
for m in _REF.finditer(v):
|
|
|
|
_check_ref(m.group(0), m.group(1))
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def doc_string_or_strings(v):
|
|
|
|
"""Match a documentation string, or list of strings."""
|
|
|
|
if isinstance(v, string_types):
|
|
|
|
return doc_string(v)
|
|
|
|
if isinstance(v, (list, tuple)):
|
|
|
|
return [doc_string(vv) for vv in v]
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Must be a string or list of strings'), 'invalid-documentation')
|
|
|
|
|
|
|
|
|
|
|
|
def is_callable(v):
|
|
|
|
if not callable(v):
|
|
|
|
raise ValueInvalid('not a valid value')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def sequence_of_sequences(min=None, max=None):
|
|
|
|
return All(
|
|
|
|
Any(
|
|
|
|
None,
|
|
|
|
[Any(list, tuple)],
|
|
|
|
tuple([Any(list, tuple)]),
|
|
|
|
),
|
|
|
|
Any(
|
|
|
|
None,
|
|
|
|
[Length(min=min, max=max)],
|
|
|
|
tuple([Length(min=min, max=max)]),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
seealso_schema = Schema(
|
|
|
|
[
|
|
|
|
Any(
|
|
|
|
{
|
|
|
|
Required('module'): Any(*string_types),
|
|
|
|
'description': doc_string,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Required('plugin'): Any(*string_types),
|
|
|
|
Required('plugin_type'): Any(*DOCUMENTABLE_PLUGINS),
|
|
|
|
'description': doc_string,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Required('ref'): Any(*string_types),
|
|
|
|
Required('description'): doc_string,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
Required('link'): Any(*string_types),
|
|
|
|
Required('description'): doc_string,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
argument_spec_types = ['bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw',
|
|
|
|
'sid', 'str']
|
|
|
|
|
|
|
|
|
|
|
|
argument_spec_modifiers = {
|
|
|
|
'mutually_exclusive': sequence_of_sequences(min=2),
|
|
|
|
'required_together': sequence_of_sequences(min=2),
|
|
|
|
'required_one_of': sequence_of_sequences(min=2),
|
|
|
|
'required_if': sequence_of_sequences(min=3, max=4),
|
|
|
|
'required_by': Schema({str: Any(list_string_types, tuple_string_types, *string_types)}),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def no_required_with_default(v):
|
|
|
|
if v.get('default') and v.get('required'):
|
|
|
|
raise Invalid('required=True cannot be supplied with a default')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def elements_with_list(v):
|
|
|
|
if v.get('elements') and v.get('type') != 'list':
|
|
|
|
raise Invalid('type must be list to use elements')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def options_with_apply_defaults(v):
|
|
|
|
if v.get('apply_defaults') and not v.get('options'):
|
|
|
|
raise Invalid('apply_defaults=True requires options to be set')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def check_removal_version(v, version_field, collection_name_field, error_code='invalid-removal-version'):
|
|
|
|
version = v.get(version_field)
|
|
|
|
collection_name = v.get(collection_name_field)
|
|
|
|
if not isinstance(version, string_types) or not isinstance(collection_name, string_types):
|
|
|
|
# If they are not strings, schema validation will have already complained.
|
|
|
|
return v
|
|
|
|
if collection_name == 'ansible.builtin':
|
|
|
|
try:
|
|
|
|
parsed_version = StrictVersion()
|
|
|
|
parsed_version.parse(version)
|
|
|
|
except ValueError as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('%s (%r) is not a valid ansible-core version: %s' % (version_field, version, exc)),
|
|
|
|
error_code=error_code)
|
|
|
|
return v
|
|
|
|
try:
|
|
|
|
parsed_version = SemanticVersion()
|
|
|
|
parsed_version.parse(version)
|
|
|
|
if parsed_version.major != 0 and (parsed_version.minor != 0 or parsed_version.patch != 0):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('%s (%r) must be a major release, not a minor or patch release (see specification at '
|
|
|
|
'https://semver.org/)' % (version_field, version)),
|
|
|
|
error_code='removal-version-must-be-major')
|
|
|
|
except ValueError as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('%s (%r) is not a valid collection version (see specification at https://semver.org/): '
|
|
|
|
'%s' % (version_field, version, exc)),
|
|
|
|
error_code=error_code)
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def option_deprecation(v):
|
|
|
|
if v.get('removed_in_version') or v.get('removed_at_date'):
|
|
|
|
if v.get('removed_in_version') and v.get('removed_at_date'):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Only one of removed_in_version and removed_at_date must be specified'),
|
|
|
|
error_code='deprecation-either-date-or-version')
|
|
|
|
if not v.get('removed_from_collection'):
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('If removed_in_version or removed_at_date is specified, '
|
|
|
|
'removed_from_collection must be specified as well'),
|
|
|
|
error_code='deprecation-collection-missing')
|
|
|
|
check_removal_version(v,
|
|
|
|
version_field='removed_in_version',
|
|
|
|
collection_name_field='removed_from_collection',
|
|
|
|
error_code='invalid-removal-version')
|
|
|
|
return
|
|
|
|
if v.get('removed_from_collection'):
|
|
|
|
raise Invalid('removed_from_collection cannot be specified without either '
|
|
|
|
'removed_in_version or removed_at_date')
|
|
|
|
|
|
|
|
|
|
|
|
def argument_spec_schema(for_collection):
|
|
|
|
any_string_types = Any(*string_types)
|
|
|
|
schema = {
|
|
|
|
any_string_types: {
|
|
|
|
'type': Any(is_callable, *argument_spec_types),
|
|
|
|
'elements': Any(*argument_spec_types),
|
|
|
|
'default': object,
|
|
|
|
'fallback': Any(
|
|
|
|
(is_callable, list_string_types),
|
|
|
|
[is_callable, list_string_types],
|
|
|
|
),
|
|
|
|
'choices': Any([object], (object,)),
|
|
|
|
'required': bool,
|
|
|
|
'no_log': bool,
|
|
|
|
'aliases': Any(list_string_types, tuple(list_string_types)),
|
|
|
|
'apply_defaults': bool,
|
|
|
|
'removed_in_version': version(for_collection),
|
|
|
|
'removed_at_date': date(),
|
|
|
|
'removed_from_collection': collection_name,
|
|
|
|
'options': Self,
|
|
|
|
'deprecated_aliases': Any([All(
|
|
|
|
Any(
|
|
|
|
{
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
Required('date'): date(),
|
|
|
|
Required('collection_name'): collection_name,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
Required('version'): version(for_collection),
|
|
|
|
Required('collection_name'): collection_name,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
partial(check_removal_version,
|
|
|
|
version_field='version',
|
|
|
|
collection_name_field='collection_name',
|
|
|
|
error_code='invalid-removal-version')
|
|
|
|
)]),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
schema[any_string_types].update(argument_spec_modifiers)
|
|
|
|
schemas = All(
|
|
|
|
schema,
|
|
|
|
Schema({any_string_types: no_required_with_default}),
|
|
|
|
Schema({any_string_types: elements_with_list}),
|
|
|
|
Schema({any_string_types: options_with_apply_defaults}),
|
|
|
|
Schema({any_string_types: option_deprecation}),
|
|
|
|
)
|
|
|
|
return Schema(schemas)
|
|
|
|
|
|
|
|
|
|
|
|
def ansible_module_kwargs_schema(module_name, for_collection):
|
|
|
|
schema = {
|
|
|
|
'argument_spec': argument_spec_schema(for_collection),
|
|
|
|
'bypass_checks': bool,
|
|
|
|
'no_log': bool,
|
|
|
|
'check_invalid_arguments': Any(None, bool),
|
|
|
|
'add_file_common_args': bool,
|
|
|
|
'supports_check_mode': bool,
|
|
|
|
}
|
|
|
|
if module_name.endswith(('_info', '_facts')):
|
|
|
|
del schema['supports_check_mode']
|
|
|
|
schema[Required('supports_check_mode')] = True
|
|
|
|
schema.update(argument_spec_modifiers)
|
|
|
|
return Schema(schema)
|
|
|
|
|
|
|
|
|
|
|
|
json_value = Schema(Any(
|
|
|
|
None,
|
|
|
|
int,
|
|
|
|
float,
|
|
|
|
[Self],
|
|
|
|
*(list({str_type: Self} for str_type in string_types) + list(string_types))
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
def version_added(v, error_code='version-added-invalid', accept_historical=False):
|
|
|
|
if 'version_added' in v:
|
|
|
|
version_added = v.get('version_added')
|
|
|
|
if isinstance(version_added, string_types):
|
|
|
|
# If it is not a string, schema validation will have already complained
|
|
|
|
# - or we have a float and we are in ansible/ansible, in which case we're
|
|
|
|
# also happy.
|
|
|
|
if v.get('version_added_collection') == 'ansible.builtin':
|
|
|
|
if version_added == 'historical' and accept_historical:
|
|
|
|
return v
|
|
|
|
try:
|
|
|
|
version = StrictVersion()
|
|
|
|
version.parse(version_added)
|
|
|
|
except ValueError as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('version_added (%r) is not a valid ansible-core version: '
|
|
|
|
'%s' % (version_added, exc)),
|
|
|
|
error_code=error_code)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
version = SemanticVersion()
|
|
|
|
version.parse(version_added)
|
|
|
|
if version.major != 0 and version.patch != 0:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('version_added (%r) must be a major or minor release, '
|
|
|
|
'not a patch release (see specification at '
|
|
|
|
'https://semver.org/)' % (version_added, )),
|
|
|
|
error_code='version-added-must-be-major-or-minor')
|
|
|
|
except ValueError as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('version_added (%r) is not a valid collection version '
|
|
|
|
'(see specification at https://semver.org/): '
|
|
|
|
'%s' % (version_added, exc)),
|
|
|
|
error_code=error_code)
|
|
|
|
elif 'version_added_collection' in v:
|
|
|
|
# Must have been manual intervention, since version_added_collection is only
|
|
|
|
# added automatically when version_added is present
|
|
|
|
raise Invalid('version_added_collection cannot be specified without version_added')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def check_option_elements(v):
|
|
|
|
# Check whether elements is there iff type == 'list'
|
|
|
|
v_type = v.get('type')
|
|
|
|
v_elements = v.get('elements')
|
|
|
|
if v_type == 'list' and v_elements is None:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Argument defines type as list but elements is not defined'),
|
|
|
|
error_code='parameter-list-no-elements') # FIXME: adjust error code?
|
|
|
|
if v_type != 'list' and v_elements is not None:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid('Argument defines parameter elements as %s but it is valid only when value of parameter type is list' % (v_elements, )),
|
|
|
|
error_code='doc-elements-invalid')
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def get_type_checker(v):
|
|
|
|
v_type = v.get('type')
|
|
|
|
if v_type == 'list':
|
|
|
|
elt_checker, elt_name = get_type_checker({'type': v.get('elements')})
|
|
|
|
|
|
|
|
def list_checker(value):
|
|
|
|
if isinstance(value, string_types):
|
|
|
|
value = [unquote(x.strip()) for x in value.split(',')]
|
|
|
|
if not isinstance(value, list):
|
|
|
|
raise ValueError('Value must be a list')
|
|
|
|
if elt_checker:
|
|
|
|
for elt in value:
|
|
|
|
try:
|
|
|
|
elt_checker(elt)
|
|
|
|
except Exception as exc:
|
|
|
|
raise ValueError('Entry %r is not of type %s: %s' % (elt, elt_name, exc))
|
|
|
|
|
|
|
|
return list_checker, ('list of %s' % elt_name) if elt_checker else 'list'
|
|
|
|
|
|
|
|
if v_type in ('boolean', 'bool'):
|
|
|
|
return partial(boolean, strict=False), v_type
|
|
|
|
|
|
|
|
if v_type in ('integer', 'int'):
|
|
|
|
return int, v_type
|
|
|
|
|
|
|
|
if v_type == 'float':
|
|
|
|
return float, v_type
|
|
|
|
|
|
|
|
if v_type == 'none':
|
|
|
|
def none_checker(value):
|
|
|
|
if value not in ('None', None):
|
|
|
|
raise ValueError('Value must be "None" or none')
|
|
|
|
|
|
|
|
return none_checker, v_type
|
|
|
|
|
|
|
|
if v_type in ('str', 'string', 'path', 'tmp', 'temppath', 'tmppath'):
|
|
|
|
def str_checker(value):
|
|
|
|
if not isinstance(value, string_types):
|
|
|
|
raise ValueError('Value must be string')
|
|
|
|
|
|
|
|
return str_checker, v_type
|
|
|
|
|
|
|
|
if v_type in ('pathspec', 'pathlist'):
|
|
|
|
def path_list_checker(value):
|
|
|
|
if not isinstance(value, string_types) and not is_iterable(value):
|
|
|
|
raise ValueError('Value must be string or list of strings')
|
|
|
|
|
|
|
|
return path_list_checker, v_type
|
|
|
|
|
|
|
|
if v_type in ('dict', 'dictionary'):
|
|
|
|
def dict_checker(value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise ValueError('Value must be dictionary')
|
|
|
|
|
|
|
|
return dict_checker, v_type
|
|
|
|
|
|
|
|
return None, 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
def check_option_choices(v):
|
|
|
|
# Check whether choices have the correct type
|
|
|
|
v_choices = v.get('choices')
|
|
|
|
if not is_iterable(v_choices):
|
|
|
|
return v
|
|
|
|
|
|
|
|
if v.get('type') == 'list':
|
|
|
|
# choices for a list type means that every list element must be one of these choices
|
|
|
|
type_checker, type_name = get_type_checker({'type': v.get('elements')})
|
|
|
|
else:
|
|
|
|
type_checker, type_name = get_type_checker(v)
|
|
|
|
if type_checker is None:
|
|
|
|
return v
|
|
|
|
|
|
|
|
for value in v_choices:
|
|
|
|
try:
|
|
|
|
type_checker(value)
|
|
|
|
except Exception as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid(
|
|
|
|
'Argument defines choices as (%r) but this is incompatible with argument type %s: %s' % (value, type_name, exc)),
|
|
|
|
error_code='doc-choices-incompatible-type')
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def check_option_default(v):
|
|
|
|
# Check whether default is only present if required=False, and whether default has correct type
|
|
|
|
v_default = v.get('default')
|
|
|
|
if v.get('required') and v_default is not None:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid(
|
|
|
|
'Argument is marked as required but specifies a default.'
|
|
|
|
' Arguments with a default should not be marked as required'),
|
|
|
|
error_code='no-default-for-required-parameter') # FIXME: adjust error code?
|
|
|
|
|
|
|
|
if v_default is None:
|
|
|
|
return v
|
|
|
|
|
|
|
|
type_checker, type_name = get_type_checker(v)
|
|
|
|
if type_checker is None:
|
|
|
|
return v
|
|
|
|
|
|
|
|
try:
|
|
|
|
type_checker(v_default)
|
|
|
|
except Exception as exc:
|
|
|
|
raise _add_ansible_error_code(
|
|
|
|
Invalid(
|
|
|
|
'Argument defines default as (%r) but this is incompatible with parameter type %s: %s' % (v_default, type_name, exc)),
|
|
|
|
error_code='incompatible-default-type')
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def list_dict_option_schema(for_collection, plugin_type):
|
|
|
|
if plugin_type == 'module':
|
|
|
|
option_types = Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str')
|
|
|
|
element_types = option_types
|
|
|
|
else:
|
|
|
|
option_types = Any(None, 'boolean', 'bool', 'integer', 'int', 'float', 'list', 'dict', 'dictionary', 'none',
|
|
|
|
'path', 'tmp', 'temppath', 'tmppath', 'pathspec', 'pathlist', 'str', 'string', 'raw')
|
|
|
|
element_types = Any(None, 'boolean', 'bool', 'integer', 'int', 'float', 'list', 'dict', 'dictionary', 'path', 'str', 'string', 'raw')
|
|
|
|
|
|
|
|
basic_option_schema = {
|
|
|
|
Required('description'): doc_string_or_strings,
|
|
|
|
'required': bool,
|
|
|
|
'choices': list,
|
|
|
|
'aliases': Any(list_string_types),
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
'default': json_value,
|
|
|
|
# Note: Types are strings, not literal bools, such as True or False
|
|
|
|
'type': option_types,
|
|
|
|
# in case of type='list' elements define type of individual item in list
|
|
|
|
'elements': element_types,
|
|
|
|
}
|
|
|
|
if plugin_type != 'module':
|
|
|
|
basic_option_schema['name'] = Any(*string_types)
|
|
|
|
deprecated_schema = All(
|
|
|
|
Schema(
|
|
|
|
All(
|
|
|
|
{
|
|
|
|
# This definition makes sure everything has the correct types/values
|
|
|
|
'why': doc_string,
|
|
|
|
'alternatives': doc_string,
|
|
|
|
# vod stands for 'version or date'; this is the name of the exclusive group
|
|
|
|
Exclusive('removed_at_date', 'vod'): date(),
|
|
|
|
Exclusive('version', 'vod'): version(for_collection),
|
|
|
|
'collection_name': collection_name,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
# This definition makes sure that everything we require is there
|
|
|
|
Required('why'): Any(*string_types),
|
|
|
|
'alternatives': Any(*string_types),
|
|
|
|
Required(Any('removed_at_date', 'version')): Any(*string_types),
|
|
|
|
Required('collection_name'): Any(*string_types),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
extra=PREVENT_EXTRA
|
|
|
|
),
|
|
|
|
partial(check_removal_version,
|
|
|
|
version_field='version',
|
|
|
|
collection_name_field='collection_name',
|
|
|
|
error_code='invalid-removal-version'),
|
|
|
|
)
|
|
|
|
env_schema = All(
|
|
|
|
Schema({
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
partial(version_added, error_code='option-invalid-version-added')
|
|
|
|
)
|
|
|
|
ini_schema = All(
|
|
|
|
Schema({
|
|
|
|
Required('key'): Any(*string_types),
|
|
|
|
Required('section'): Any(*string_types),
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
partial(version_added, error_code='option-invalid-version-added')
|
|
|
|
)
|
|
|
|
vars_schema = All(
|
|
|
|
Schema({
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
partial(version_added, error_code='option-invalid-version-added')
|
|
|
|
)
|
|
|
|
cli_schema = All(
|
|
|
|
Schema({
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
'option': Any(*string_types),
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
partial(version_added, error_code='option-invalid-version-added')
|
|
|
|
)
|
|
|
|
keyword_schema = All(
|
|
|
|
Schema({
|
|
|
|
Required('name'): Any(*string_types),
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
partial(version_added, error_code='option-invalid-version-added')
|
|
|
|
)
|
|
|
|
basic_option_schema.update({
|
|
|
|
'env': [env_schema],
|
|
|
|
'ini': [ini_schema],
|
|
|
|
'vars': [vars_schema],
|
|
|
|
'cli': [cli_schema],
|
|
|
|
'keyword': [keyword_schema],
|
|
|
|
'deprecated': deprecated_schema,
|
|
|
|
})
|
|
|
|
|
|
|
|
suboption_schema = dict(basic_option_schema)
|
|
|
|
suboption_schema.update({
|
|
|
|
# Recursive suboptions
|
|
|
|
'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)),
|
|
|
|
})
|
|
|
|
suboption_schema = Schema(All(
|
|
|
|
suboption_schema,
|
|
|
|
check_option_elements,
|
|
|
|
check_option_choices,
|
|
|
|
check_option_default,
|
|
|
|
), extra=PREVENT_EXTRA)
|
|
|
|
|
|
|
|
# This generates list of dicts with keys from string_types and suboption_schema value
|
|
|
|
# for example in Python 3: {str: suboption_schema}
|
|
|
|
list_dict_suboption_schema = [{str_type: suboption_schema} for str_type in string_types]
|
|
|
|
|
|
|
|
option_schema = dict(basic_option_schema)
|
|
|
|
option_schema.update({
|
|
|
|
'suboptions': Any(None, *list_dict_suboption_schema),
|
|
|
|
})
|
|
|
|
option_schema = Schema(All(
|
|
|
|
option_schema,
|
|
|
|
check_option_elements,
|
|
|
|
check_option_choices,
|
|
|
|
check_option_default,
|
|
|
|
), extra=PREVENT_EXTRA)
|
|
|
|
|
|
|
|
option_version_added = Schema(
|
|
|
|
All({
|
|
|
|
'suboptions': Any(None, *[{str_type: Self} for str_type in string_types]),
|
|
|
|
}, partial(version_added, error_code='option-invalid-version-added')),
|
|
|
|
extra=ALLOW_EXTRA
|
|
|
|
)
|
|
|
|
|
|
|
|
# This generates list of dicts with keys from string_types and option_schema value
|
|
|
|
# for example in Python 3: {str: option_schema}
|
|
|
|
return [{str_type: All(option_schema, option_version_added)} for str_type in string_types]
|
|
|
|
|
|
|
|
|
|
|
|
def return_contains(v):
|
|
|
|
schema = Schema(
|
|
|
|
{
|
|
|
|
Required('contains'): Any(dict, list, *string_types)
|
|
|
|
},
|
|
|
|
extra=ALLOW_EXTRA
|
|
|
|
)
|
|
|
|
if v.get('type') == 'complex':
|
|
|
|
return schema(v)
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
def return_schema(for_collection, plugin_type='module'):
|
|
|
|
if plugin_type == 'module':
|
|
|
|
return_types = Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'raw', 'str')
|
|
|
|
element_types = Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str')
|
|
|
|
else:
|
|
|
|
return_types = Any(None, 'boolean', 'bool', 'integer', 'int', 'float', 'list', 'dict', 'dictionary', 'path', 'str', 'string', 'raw')
|
|
|
|
element_types = return_types
|
|
|
|
|
|
|
|
basic_return_option_schema = {
|
|
|
|
Required('description'): doc_string_or_strings,
|
|
|
|
'returned': doc_string,
|
|
|
|
'version_added': version(for_collection),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
'sample': json_value,
|
|
|
|
'example': json_value,
|
|
|
|
# in case of type='list' elements define type of individual item in list
|
|
|
|
'elements': element_types,
|
|
|
|
'choices': Any([object], (object,)),
|
|
|
|
}
|
|
|
|
if plugin_type == 'module':
|
|
|
|
# type is only required for modules right now
|
|
|
|
basic_return_option_schema[Required('type')] = return_types
|
|
|
|
else:
|
|
|
|
basic_return_option_schema['type'] = return_types
|
|
|
|
|
|
|
|
inner_return_option_schema = dict(basic_return_option_schema)
|
|
|
|
inner_return_option_schema.update({
|
|
|
|
'contains': Any(None, *list({str_type: Self} for str_type in string_types)),
|
|
|
|
})
|
|
|
|
return_contains_schema = Any(
|
|
|
|
All(
|
|
|
|
Schema(inner_return_option_schema),
|
|
|
|
Schema(return_contains),
|
|
|
|
Schema(partial(version_added, error_code='option-invalid-version-added')),
|
|
|
|
),
|
|
|
|
Schema(type(None)),
|
|
|
|
)
|
|
|
|
|
|
|
|
# This generates list of dicts with keys from string_types and return_contains_schema value
|
|
|
|
# for example in Python 3: {str: return_contains_schema}
|
|
|
|
list_dict_return_contains_schema = [{str_type: return_contains_schema} for str_type in string_types]
|
|
|
|
|
|
|
|
return_option_schema = dict(basic_return_option_schema)
|
|
|
|
return_option_schema.update({
|
|
|
|
'contains': Any(None, *list_dict_return_contains_schema),
|
|
|
|
})
|
|
|
|
if plugin_type == 'module':
|
|
|
|
# 'returned' is required on top-level
|
|
|
|
del return_option_schema['returned']
|
|
|
|
return_option_schema[Required('returned')] = Any(*string_types)
|
|
|
|
return Any(
|
|
|
|
All(
|
|
|
|
Schema(
|
|
|
|
{
|
|
|
|
any_string_types: return_option_schema
|
|
|
|
}
|
|
|
|
),
|
|
|
|
Schema({any_string_types: return_contains}),
|
|
|
|
Schema({any_string_types: partial(version_added, error_code='option-invalid-version-added')}),
|
|
|
|
),
|
|
|
|
Schema(type(None)),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def deprecation_schema(for_collection):
|
|
|
|
main_fields = {
|
|
|
|
Required('why'): doc_string,
|
|
|
|
Required('alternative'): doc_string,
|
|
|
|
Required('removed_from_collection'): collection_name,
|
|
|
|
'removed': Any(True),
|
|
|
|
}
|
|
|
|
|
|
|
|
date_schema = {
|
|
|
|
Required('removed_at_date'): date(),
|
|
|
|
}
|
|
|
|
date_schema.update(main_fields)
|
|
|
|
|
|
|
|
if for_collection:
|
|
|
|
version_schema = {
|
|
|
|
Required('removed_in'): version(for_collection),
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
version_schema = {
|
|
|
|
Required('removed_in'): deprecation_versions(),
|
|
|
|
}
|
|
|
|
version_schema.update(main_fields)
|
|
|
|
|
|
|
|
result = Any(
|
|
|
|
Schema(version_schema, extra=PREVENT_EXTRA),
|
|
|
|
Schema(date_schema, extra=PREVENT_EXTRA),
|
|
|
|
)
|
|
|
|
|
|
|
|
if for_collection:
|
|
|
|
result = All(
|
|
|
|
result,
|
|
|
|
partial(check_removal_version,
|
|
|
|
version_field='removed_in',
|
|
|
|
collection_name_field='removed_from_collection',
|
|
|
|
error_code='invalid-removal-version'))
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def author(value):
|
|
|
|
if value is None:
|
|
|
|
return value # let schema checks handle
|
|
|
|
|
|
|
|
if not is_iterable(value):
|
|
|
|
value = [value]
|
|
|
|
|
|
|
|
for line in value:
|
|
|
|
if not isinstance(line, string_types):
|
|
|
|
continue # let schema checks handle
|
|
|
|
m = author_line.search(line)
|
|
|
|
if not m:
|
|
|
|
raise Invalid("Invalid author")
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def doc_schema(module_name, for_collection=False, deprecated_module=False, plugin_type='module'):
|
|
|
|
|
|
|
|
if module_name.startswith('_') and not for_collection:
|
|
|
|
module_name = module_name[1:]
|
|
|
|
deprecated_module = True
|
|
|
|
if for_collection is False and plugin_type == 'connection' and module_name == 'paramiko_ssh':
|
|
|
|
# The plugin loader has a hard-coded exception: when the builtin connection 'paramiko' is
|
|
|
|
# referenced, it loads 'paramiko_ssh' instead. That's why in this plugin, the name must be
|
|
|
|
# 'paramiko' and not 'paramiko_ssh'.
|
|
|
|
module_name = 'paramiko'
|
|
|
|
doc_schema_dict = {
|
|
|
|
Required('module' if plugin_type == 'module' else 'name'): module_name,
|
|
|
|
Required('short_description'): doc_string,
|
|
|
|
Required('description'): doc_string_or_strings,
|
|
|
|
'notes': Any(None, [doc_string]),
|
|
|
|
'seealso': Any(None, seealso_schema),
|
|
|
|
'requirements': [doc_string],
|
|
|
|
'todo': Any(None, doc_string_or_strings),
|
|
|
|
'options': Any(None, *list_dict_option_schema(for_collection, plugin_type)),
|
|
|
|
'extends_documentation_fragment': Any(list_string_types, *string_types),
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
}
|
|
|
|
if plugin_type == 'module':
|
|
|
|
doc_schema_dict[Required('author')] = All(Any(None, list_string_types, *string_types), author)
|
|
|
|
else:
|
|
|
|
# author is optional for plugins (for now)
|
|
|
|
doc_schema_dict['author'] = All(Any(None, list_string_types, *string_types), author)
|
|
|
|
if plugin_type == 'callback':
|
|
|
|
doc_schema_dict[Required('type')] = Any('aggregate', 'notification', 'stdout')
|
|
|
|
|
|
|
|
if for_collection:
|
|
|
|
# Optional
|
|
|
|
doc_schema_dict['version_added'] = version(for_collection=True)
|
|
|
|
else:
|
|
|
|
doc_schema_dict[Required('version_added')] = version(for_collection=False)
|
|
|
|
|
|
|
|
if deprecated_module:
|
|
|
|
deprecation_required_scheme = {
|
|
|
|
Required('deprecated'): Any(deprecation_schema(for_collection=for_collection)),
|
|
|
|
}
|
|
|
|
|
|
|
|
doc_schema_dict.update(deprecation_required_scheme)
|
|
|
|
|
|
|
|
def add_default_attributes(more=None):
|
|
|
|
schema = {
|
|
|
|
'description': doc_string_or_strings,
|
|
|
|
'details': doc_string_or_strings,
|
|
|
|
'support': any_string_types,
|
|
|
|
'version_added_collection': any_string_types,
|
|
|
|
'version_added': any_string_types,
|
|
|
|
}
|
|
|
|
if more:
|
|
|
|
schema.update(more)
|
|
|
|
return schema
|
|
|
|
|
|
|
|
doc_schema_dict['attributes'] = Schema(
|
|
|
|
All(
|
|
|
|
Schema({
|
|
|
|
any_string_types: {
|
|
|
|
Required('description'): doc_string_or_strings,
|
|
|
|
Required('support'): Any('full', 'partial', 'none', 'N/A'),
|
|
|
|
'details': doc_string_or_strings,
|
|
|
|
'version_added_collection': collection_name,
|
|
|
|
'version_added': version(for_collection=for_collection),
|
|
|
|
},
|
|
|
|
}, extra=ALLOW_EXTRA),
|
|
|
|
partial(version_added, error_code='attribute-invalid-version-added', accept_historical=False),
|
|
|
|
Schema({
|
|
|
|
any_string_types: add_default_attributes(),
|
|
|
|
'action_group': add_default_attributes({
|
|
|
|
Required('membership'): list_string_types,
|
|
|
|
}),
|
|
|
|
'platform': add_default_attributes({
|
|
|
|
Required('platforms'): Any(list_string_types, *string_types)
|
|
|
|
}),
|
|
|
|
}, extra=PREVENT_EXTRA),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return Schema(
|
|
|
|
All(
|
|
|
|
Schema(
|
|
|
|
doc_schema_dict,
|
|
|
|
extra=PREVENT_EXTRA
|
|
|
|
),
|
|
|
|
partial(version_added, error_code='module-invalid-version-added', accept_historical=not for_collection),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Things to add soon
|
|
|
|
####################
|
|
|
|
# 1) Recursively validate `type: complex` fields
|
|
|
|
# This will improve documentation, though require fair amount of module tidyup
|
|
|
|
|
|
|
|
# Possible Future Enhancements
|
|
|
|
##############################
|
|
|
|
|
|
|
|
# 1) Don't allow empty options for choices, aliases, etc
|
|
|
|
# 2) If type: bool ensure choices isn't set - perhaps use Exclusive
|
|
|
|
# 3) both version_added should be quoted floats
|
|
|
|
|
|
|
|
# Tool that takes JSON and generates RETURN skeleton (needs to support complex structures)
|