You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py

363 lines
12 KiB
Python

# -*- 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 (absolute_import, division, print_function)
__metaclass__ = type
import re
from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid
from ansible.module_utils.six import string_types
from ansible.module_utils.common.collections import is_iterable
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
# "Name (!UNKNOWN)" - For the few untraceable authors
author_line = re.compile(r'^\w.*(\(@([\w-]+)\)|!UNKNOWN)(?![\w.])|^Ansible Core Team$|^Michael DeHaan$')
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': Any(*string_types),
},
{
Required('ref'): Any(*string_types),
Required('description'): Any(*string_types),
},
{
Required('name'): Any(*string_types),
Required('link'): Any(*string_types),
Required('description'): Any(*string_types),
},
),
]
)
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 argument_spec_schema():
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': Any(float, *string_types),
'options': Self,
'deprecated_aliases': Any([
{
Required('name'): Any(*string_types),
Required('version'): Any(float, *string_types),
},
]),
}
}
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}),
)
return Schema(schemas)
def ansible_module_kwargs_schema():
schema = {
'argument_spec': argument_spec_schema(),
'bypass_checks': bool,
'no_log': bool,
'check_invalid_arguments': Any(None, bool),
'add_file_common_args': bool,
'supports_check_mode': bool,
}
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))
))
suboption_schema = Schema(
{
Required('description'): Any(list_string_types, *string_types),
'required': bool,
'choices': list,
'aliases': Any(list_string_types),
'version_added': Any(float, *string_types),
'default': json_value,
# Note: Types are strings, not literal bools, such as True or False
'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
# Recursive suboptions
'suboptions': Any(None, *list({str_type: Self} for str_type in string_types)),
},
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 = Schema(
{
Required('description'): Any(list_string_types, *string_types),
'required': bool,
'choices': list,
'aliases': Any(list_string_types),
'version_added': Any(float, *string_types),
'default': json_value,
'suboptions': Any(None, *list_dict_suboption_schema),
# Note: Types are strings, not literal bools, such as True or False
'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
},
extra=PREVENT_EXTRA
)
# This generates list of dicts with keys from string_types and option_schema value
# for example in Python 3: {str: option_schema}
list_dict_option_schema = [{str_type: option_schema} 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
return_contains_schema = Any(
All(
Schema(
{
Required('description'): Any(list_string_types, *string_types),
'returned': Any(*string_types), # only returned on top level
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': Any(float, *string_types),
'sample': json_value,
'example': json_value,
'contains': Any(None, *list({str_type: Self} for str_type in string_types)),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
}
),
Schema(return_contains)
),
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_schema = Any(
All(
Schema(
{
any_string_types: {
Required('description'): Any(list_string_types, *string_types),
Required('returned'): Any(*string_types),
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': Any(float, *string_types),
'sample': json_value,
'example': json_value,
'contains': Any(None, *list_dict_return_contains_schema),
# in case of type='list' elements define type of individual item in list
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
}
}
),
Schema({any_string_types: return_contains})
),
Schema(type(None)),
)
deprecation_schema = Schema(
{
# Only list branches that are deprecated or may have docs stubs in
# Deprecation cycle changed at 2.4 (though not retroactively)
# 2.3 -> removed_in: "2.5" + n for docs stub
# 2.4 -> removed_in: "2.8" + n for docs stub
Required('removed_in'): Any("2.2", "2.3", "2.4", "2.5", "2.6", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13", "2.14"),
Required('why'): Any(*string_types),
Required('alternative'): Any(*string_types),
'removed': Any(True),
},
extra=PREVENT_EXTRA
)
def author(value):
if not is_iterable(value):
value = [value]
for line in value:
m = author_line.search(line)
if not m:
raise Invalid("Invalid author")
def doc_schema(module_name, version_added=True, deprecated_module=False):
if module_name.startswith('_'):
module_name = module_name[1:]
deprecated_module = True
doc_schema_dict = {
Required('module'): module_name,
Required('short_description'): Any(*string_types),
Required('description'): Any(list_string_types, *string_types),
Required('author'): All(Any(None, list_string_types, *string_types), author),
'notes': Any(None, list_string_types),
'seealso': Any(None, seealso_schema),
'requirements': list_string_types,
'todo': Any(None, list_string_types, *string_types),
'options': Any(None, *list_dict_option_schema),
'extends_documentation_fragment': Any(list_string_types, *string_types)
}
if version_added:
doc_schema_dict[Required('version_added')] = Any(float, *string_types)
else:
# Optional
doc_schema_dict['version_added'] = Any(float, *string_types)
if deprecated_module:
deprecation_required_scheme = {
Required('deprecated'): Any(deprecation_schema),
}
doc_schema_dict.update(deprecation_required_scheme)
return Schema(
doc_schema_dict,
extra=PREVENT_EXTRA
)
def metadata_1_0_schema(deprecated):
valid_status = Any('stableinterface', 'preview', 'deprecated', 'removed')
if deprecated:
valid_status = Any('deprecated')
return Schema(
{
Required('status'): [valid_status],
Required('metadata_version'): '1.0',
Required('supported_by'): Any('core', 'community', 'curated')
}
)
def metadata_1_1_schema():
valid_status = Any('stableinterface', 'preview', 'deprecated', 'removed')
return Schema(
{
Required('status'): [valid_status],
Required('metadata_version'): '1.1',
Required('supported_by'): Any('core', 'community', 'certified', 'network')
}
)
# 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)