Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
{
"all_targets": true,
"prefixes": [
"lib/ansible/modules/",
"lib/ansible/plugins/action/",
"plugins/modules/",
"plugins/action/"
],
"extensions": [
".py"
],
"output": "path-message"
}

View File

@@ -0,0 +1,67 @@
"""Test to verify action plugins have an associated module to provide documentation."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
def main():
"""Main entry point."""
paths = sys.argv[1:] or sys.stdin.read().splitlines()
module_names = set()
module_prefixes = {
'lib/ansible/modules/': True,
'plugins/modules/': False,
}
action_prefixes = {
'lib/ansible/plugins/action/': True,
'plugins/action/': False,
}
for path in paths:
full_name = get_full_name(path, module_prefixes)
if full_name:
module_names.add(full_name)
for path in paths:
full_name = get_full_name(path, action_prefixes)
if full_name and full_name not in module_names:
print('%s: action plugin has no matching module to provide documentation' % path)
def get_full_name(path, prefixes):
"""Return the full name of the plugin at the given path by matching against the given path prefixes, or None if no match is found."""
for prefix, flat in prefixes.items():
if path.startswith(prefix):
relative_path = os.path.relpath(path, prefix)
if flat:
full_name = os.path.basename(relative_path)
else:
full_name = relative_path
full_name = os.path.splitext(full_name)[0]
name = os.path.basename(full_name)
if name == '__init__':
return None
if name.startswith('_'):
name = name[1:]
full_name = os.path.join(os.path.dirname(full_name), name).replace(os.path.sep, '.')
return full_name
return None
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,8 @@
{
"intercept": true,
"prefixes": [
"changelogs/config.yaml",
"changelogs/fragments/"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,55 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
import subprocess
def main():
paths = sys.argv[1:] or sys.stdin.read().splitlines()
allowed_extensions = ('.yml', '.yaml')
config_path = 'changelogs/config.yaml'
# config must be detected independent of the file list since the file list only contains files under test (changed)
has_config = os.path.exists(config_path)
paths_to_check = []
for path in paths:
if path == config_path:
continue
if path.startswith('changelogs/fragments/.'):
if path in ('changelogs/fragments/.keep', 'changelogs/fragments/.gitkeep'):
continue
print('%s:%d:%d: file must not be a dotfile' % (path, 0, 0))
continue
ext = os.path.splitext(path)[1]
if ext not in allowed_extensions:
print('%s:%d:%d: extension must be one of: %s' % (path, 0, 0, ', '.join(allowed_extensions)))
paths_to_check.append(path)
if not has_config:
print('changelogs/config.yaml:0:0: config file does not exist')
return
if not paths_to_check:
return
cmd = [sys.executable, '-m', 'antsibull_changelog', 'lint'] + paths_to_check
# The sphinx module is a soft dependency for rstcheck, which is used by the changelog linter.
# If sphinx is found it will be loaded by rstcheck, which can affect the results of the test.
# To maintain consistency across environments, loading of sphinx is blocked, since any version (or no version) of sphinx may be present.
env = os.environ.copy()
env.update(PYTHONPATH='%s:%s' % (os.path.join(os.path.dirname(__file__), 'changelog'), env['PYTHONPATH']))
subprocess.call(cmd, env=env) # ignore the return code, rely on the output instead
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,5 @@
"""Block the sphinx module from being loaded."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
raise ImportError('The sphinx module has been prevented from loading to maintain consistent test results.')

View File

@@ -0,0 +1,14 @@
{
"prefixes": [
"lib/ansible/modules/",
"lib/ansible/module_utils/",
"plugins/modules/",
"plugins/module_utils/",
"test/units/",
"tests/unit/"
],
"files": [
"__init__.py"
],
"output": "path-message"
}

View File

@@ -0,0 +1,15 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
if os.path.getsize(path) > 0:
print('%s: empty __init__.py required' % path)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"py2_compat": true,
"output": "path-message"
}

View File

@@ -0,0 +1,45 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import ast
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'rb') as path_fd:
lines = path_fd.read().splitlines()
missing = True
if not lines:
# Files are allowed to be empty of everything including boilerplate
missing = False
for text in lines:
if text in (b'from __future__ import (absolute_import, division, print_function)',
b'from __future__ import absolute_import, division, print_function'):
missing = False
break
if missing:
with open(path) as file:
contents = file.read()
# noinspection PyBroadException
try:
node = ast.parse(contents)
# files consisting of only assignments have no need for future import boilerplate
# the only exception would be division during assignment, but we'll overlook that for simplicity
# the most likely case is that of a documentation only python file
if all(isinstance(statement, ast.Assign) for statement in node.body):
missing = False
except Exception: # pylint: disable=broad-except
pass # the compile sanity test will report this error
if missing:
print('%s: missing: from __future__ import (absolute_import, division, print_function)' % path)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,4 @@
{
"text": true,
"output": "path-message"
}

View File

@@ -0,0 +1,17 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'rb') as path_fd:
contents = path_fd.read()
if b'\r' in contents:
print('%s: use "\\n" for line endings instead of "\\r\\n"' % path)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"py2_compat": true,
"output": "path-message"
}

View File

@@ -0,0 +1,43 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import ast
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'rb') as path_fd:
lines = path_fd.read().splitlines()
missing = True
if not lines:
# Files are allowed to be empty of everything including boilerplate
missing = False
for text in lines:
if text == b'__metaclass__ = type':
missing = False
break
if missing:
with open(path) as file:
contents = file.read()
# noinspection PyBroadException
try:
node = ast.parse(contents)
# files consisting of only assignments have no need for metaclass boilerplate
# the most likely case is that of a documentation only python file
if all(isinstance(statement, ast.Assign) for statement in node.body):
missing = False
except Exception: # pylint: disable=broad-except
pass # the compile sanity test will report this error
if missing:
print('%s: missing: __metaclass__ = type' % path)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,10 @@
{
"extensions": [
".py"
],
"prefixes": [
"lib/ansible/",
"plugins/"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,23 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
ASSERT_RE = re.compile(r'^\s*assert[^a-z0-9_:]')
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as file:
for i, line in enumerate(file.readlines()):
matches = ASSERT_RE.findall(line)
if matches:
lineno = i + 1
colno = line.index('assert') + 1
print('%s:%d:%d: raise AssertionError instead of: %s' % (path, lineno, colno, matches[0][colno - 1:]))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'(isinstance.*basestring)', text)
if match:
print('%s:%d:%d: do not use `isinstance(s, basestring)`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'(?<! six)\.(iteritems)', text)
if match:
print('%s:%d:%d: use `dict.items` or `ansible.module_utils.six.iteritems` instead of `dict.iteritems`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'\.(iterkeys)', text)
if match:
print('%s:%d:%d: use `dict.keys` or `for key in dict:` instead of `dict.iterkeys`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'(?<! six)\.(itervalues)', text)
if match:
print('%s:%d:%d: use `dict.values` or `ansible.module_utils.six.itervalues` instead of `dict.itervalues`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,27 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
basic_allow_once = True
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'([^a-zA-Z0-9_]get_exception[^a-zA-Z0-9_])', text)
if match:
if path == 'lib/ansible/module_utils/basic.py' and basic_allow_once:
# basic.py is allowed to import get_exception for backwards compatibility but should not call it anywhere
basic_allow_once = False
continue
print('%s:%d:%d: do not use `get_exception`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,5 @@
{
"include_directories": true,
"include_symlinks": true,
"output": "path-message"
}

View File

@@ -0,0 +1,80 @@
# a script to check for illegal filenames on various Operating Systems. The
# main rules are derived from restrictions on Windows
# https://msdn.microsoft.com/en-us/library/aa365247#naming_conventions
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import struct
import sys
from ansible.module_utils.basic import to_bytes
ILLEGAL_CHARS = [
b'<',
b'>',
b':',
b'"',
b'/',
b'\\',
b'|',
b'?',
b'*'
] + [struct.pack("b", i) for i in range(32)]
ILLEGAL_NAMES = [
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
]
ILLEGAL_END_CHARS = [
'.',
' ',
]
def check_path(path, is_dir=False):
type_name = 'directory' if is_dir else 'file'
file_name = os.path.basename(path.rstrip(os.path.sep))
name = os.path.splitext(file_name)[0]
if name.upper() in ILLEGAL_NAMES:
print("%s: illegal %s name %s" % (path, type_name, name.upper()))
if file_name[-1] in ILLEGAL_END_CHARS:
print("%s: illegal %s name end-char '%s'" % (path, type_name, file_name[-1]))
bfile = to_bytes(file_name, encoding='utf-8')
for char in ILLEGAL_CHARS:
if char in bfile:
bpath = to_bytes(path, encoding='utf-8')
print("%s: illegal char '%s' in %s name" % (bpath, char, type_name))
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
check_path(path, is_dir=path.endswith(os.path.sep))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,10 @@
{
"extensions": [
".py"
],
"prefixes": [
"lib/ansible/",
"plugins/"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import sys
MAIN_DISPLAY_IMPORT = 'from __main__ import display'
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as file:
for i, line in enumerate(file.readlines()):
if MAIN_DISPLAY_IMPORT in line:
lineno = i + 1
colno = line.index(MAIN_DISPLAY_IMPORT) + 1
print('%s:%d:%d: Display is a singleton, just import and instantiate' % (path, lineno, colno))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,5 @@
{
"text": true,
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'rb') as path_fd:
for line, text in enumerate(path_fd.readlines()):
try:
text = text.decode('utf-8')
except UnicodeDecodeError as ex:
print('%s:%d:%d: UnicodeDecodeError: %s' % (path, line + 1, ex.start + 1, ex))
continue
match = re.search(u'([‘’“”])', text)
if match:
print('%s:%d:%d: use ASCII quotes `\'` and `"` instead of Unicode quotes' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'(unicode_literals)', text)
if match:
print('%s:%d:%d: do not use `unicode_literals`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
{
"extensions": [
".py"
],
"ignore_self": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'^(?:[^#]*?)(urlopen)', text)
if match:
print('%s:%d:%d: use `ansible.module_utils.urls.open_url` instead of `urlopen`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,11 @@
{
"prefixes": [
"lib/ansible/config/ansible_builtin_runtime.yml",
"meta/routing.yml",
"meta/runtime.yml"
],
"extensions": [
".yml"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,278 @@
"""Schema validation of ansible-core's ansible_builtin_runtime.yml and collection's meta/runtime.yml"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import datetime
import os
import re
import sys
from functools import partial
import yaml
from voluptuous import All, Any, MultipleInvalid, PREVENT_EXTRA
from voluptuous import Required, Schema, Invalid
from voluptuous.humanize import humanize_error
from ansible.module_utils.compat.version import StrictVersion, LooseVersion
from ansible.module_utils.six import string_types
from ansible.utils.version import SemanticVersion
def isodate(value, check_deprecation_date=False, is_tombstone=False):
"""Validate a datetime.date or ISO 8601 date string."""
# datetime.date objects come from YAML dates, these are ok
if isinstance(value, datetime.date):
removal_date = value
else:
# make sure we have a string
msg = 'Expected ISO 8601 date string (YYYY-MM-DD), or YAML date'
if not isinstance(value, string_types):
raise Invalid(msg)
# From Python 3.7 in, there is datetime.date.fromisoformat(). For older versions,
# we have to do things manually.
if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', value):
raise Invalid(msg)
try:
removal_date = datetime.datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
raise Invalid(msg)
# Make sure date is correct
today = datetime.date.today()
if is_tombstone:
# For a tombstone, the removal date must be in the past
if today < removal_date:
raise Invalid(
'The tombstone removal_date (%s) must not be after today (%s)' % (removal_date, today))
else:
# For a deprecation, the removal date must be in the future. Only test this if
# check_deprecation_date is truish, to avoid checks to suddenly start to fail.
if check_deprecation_date and today > removal_date:
raise Invalid(
'The deprecation removal_date (%s) must be after today (%s)' % (removal_date, today))
return value
def removal_version(value, is_ansible, current_version=None, is_tombstone=False):
"""Validate a removal version string."""
msg = (
'Removal version must be a string' if is_ansible else
'Removal version must be a semantic version (https://semver.org/)'
)
if not isinstance(value, string_types):
raise Invalid(msg)
try:
if is_ansible:
version = StrictVersion()
version.parse(value)
version = LooseVersion(value) # We're storing Ansible's version as a LooseVersion
else:
version = SemanticVersion()
version.parse(value)
if version.major != 0 and (version.minor != 0 or version.patch != 0):
raise Invalid('removal_version (%r) must be a major release, not a minor or patch release '
'(see specification at https://semver.org/)' % (value, ))
if current_version is not None:
if is_tombstone:
# For a tombstone, the removal version must not be in the future
if version > current_version:
raise Invalid('The tombstone removal_version (%r) must not be after the '
'current version (%s)' % (value, current_version))
else:
# For a deprecation, the removal version must be in the future
if version <= current_version:
raise Invalid('The deprecation removal_version (%r) must be after the '
'current version (%s)' % (value, current_version))
except ValueError:
raise Invalid(msg)
return value
def any_value(value):
"""Accepts anything."""
return value
def get_ansible_version():
"""Return current ansible-core version"""
from ansible.release import __version__
return LooseVersion('.'.join(__version__.split('.')[:3]))
def get_collection_version():
"""Return current collection version, or None if it is not available"""
import importlib.util
collection_detail_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'tools', 'collection_detail.py')
collection_detail_spec = importlib.util.spec_from_file_location('collection_detail', collection_detail_path)
collection_detail = importlib.util.module_from_spec(collection_detail_spec)
sys.modules['collection_detail'] = collection_detail
collection_detail_spec.loader.exec_module(collection_detail)
# noinspection PyBroadException
try:
result = collection_detail.read_manifest_json('.') or collection_detail.read_galaxy_yml('.')
return SemanticVersion(result['version'])
except Exception: # pylint: disable=broad-except
# We do not care why it fails, in case we cannot get the version
# just return None to indicate "we don't know".
return None
def validate_metadata_file(path, is_ansible, check_deprecation_dates=False):
"""Validate explicit runtime metadata file"""
try:
with open(path, 'r') as f_path:
routing = yaml.safe_load(f_path)
except yaml.error.MarkedYAMLError as ex:
print('%s:%d:%d: YAML load failed: %s' % (path, ex.context_mark.line +
1, ex.context_mark.column + 1, re.sub(r'\s+', ' ', str(ex))))
return
except Exception as ex: # pylint: disable=broad-except
print('%s:%d:%d: YAML load failed: %s' %
(path, 0, 0, re.sub(r'\s+', ' ', str(ex))))
return
if is_ansible:
current_version = get_ansible_version()
else:
current_version = get_collection_version()
# Updates to schema MUST also be reflected in the documentation
# ~https://docs.ansible.com/ansible/devel/dev_guide/developing_collections.html
# plugin_routing schema
avoid_additional_data = Schema(
Any(
{
Required('removal_version'): any_value,
'warning_text': any_value,
},
{
Required('removal_date'): any_value,
'warning_text': any_value,
}
),
extra=PREVENT_EXTRA
)
deprecation_schema = All(
# The first schema validates the input, and the second makes sure no extra keys are specified
Schema(
{
'removal_version': partial(removal_version, is_ansible=is_ansible,
current_version=current_version),
'removal_date': partial(isodate, check_deprecation_date=check_deprecation_dates),
'warning_text': Any(*string_types),
}
),
avoid_additional_data
)
tombstoning_schema = All(
# The first schema validates the input, and the second makes sure no extra keys are specified
Schema(
{
'removal_version': partial(removal_version, is_ansible=is_ansible,
current_version=current_version, is_tombstone=True),
'removal_date': partial(isodate, is_tombstone=True),
'warning_text': Any(*string_types),
}
),
avoid_additional_data
)
plugin_routing_schema = Any(
Schema({
('deprecation'): Any(deprecation_schema),
('tombstone'): Any(tombstoning_schema),
('redirect'): Any(*string_types),
}, extra=PREVENT_EXTRA),
)
list_dict_plugin_routing_schema = [{str_type: plugin_routing_schema}
for str_type in string_types]
plugin_schema = Schema({
('action'): Any(None, *list_dict_plugin_routing_schema),
('become'): Any(None, *list_dict_plugin_routing_schema),
('cache'): Any(None, *list_dict_plugin_routing_schema),
('callback'): Any(None, *list_dict_plugin_routing_schema),
('cliconf'): Any(None, *list_dict_plugin_routing_schema),
('connection'): Any(None, *list_dict_plugin_routing_schema),
('doc_fragments'): Any(None, *list_dict_plugin_routing_schema),
('filter'): Any(None, *list_dict_plugin_routing_schema),
('httpapi'): Any(None, *list_dict_plugin_routing_schema),
('inventory'): Any(None, *list_dict_plugin_routing_schema),
('lookup'): Any(None, *list_dict_plugin_routing_schema),
('module_utils'): Any(None, *list_dict_plugin_routing_schema),
('modules'): Any(None, *list_dict_plugin_routing_schema),
('netconf'): Any(None, *list_dict_plugin_routing_schema),
('shell'): Any(None, *list_dict_plugin_routing_schema),
('strategy'): Any(None, *list_dict_plugin_routing_schema),
('terminal'): Any(None, *list_dict_plugin_routing_schema),
('test'): Any(None, *list_dict_plugin_routing_schema),
('vars'): Any(None, *list_dict_plugin_routing_schema),
}, extra=PREVENT_EXTRA)
# import_redirection schema
import_redirection_schema = Any(
Schema({
('redirect'): Any(*string_types),
# import_redirect doesn't currently support deprecation
}, extra=PREVENT_EXTRA)
)
list_dict_import_redirection_schema = [{str_type: import_redirection_schema}
for str_type in string_types]
# top level schema
schema = Schema({
# All of these are optional
('plugin_routing'): Any(plugin_schema),
('import_redirection'): Any(None, *list_dict_import_redirection_schema),
# requires_ansible: In the future we should validate this with SpecifierSet
('requires_ansible'): Any(*string_types),
('action_groups'): dict,
}, extra=PREVENT_EXTRA)
# Ensure schema is valid
try:
schema(routing)
except MultipleInvalid as ex:
for error in ex.errors:
# No way to get line/column numbers
print('%s:%d:%d: %s' % (path, 0, 0, humanize_error(routing, error)))
def main():
"""Validate runtime metadata"""
paths = sys.argv[1:] or sys.stdin.read().splitlines()
collection_legacy_file = 'meta/routing.yml'
collection_runtime_file = 'meta/runtime.yml'
# This is currently disabled, because if it is enabled this test can start failing
# at a random date. For this to be properly activated, we (a) need to be able to return
# codes for this test, and (b) make this error optional.
check_deprecation_dates = False
for path in paths:
if path == collection_legacy_file:
print('%s:%d:%d: %s' % (path, 0, 0, ("Should be called '%s'" % collection_runtime_file)))
continue
validate_metadata_file(
path,
is_ansible=path not in (collection_legacy_file, collection_runtime_file),
check_deprecation_dates=check_deprecation_dates)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,4 @@
{
"text": true,
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,119 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import re
import stat
import sys
def main():
standard_shebangs = set([
b'#!/bin/bash -eu',
b'#!/bin/bash -eux',
b'#!/bin/sh',
b'#!/usr/bin/env bash',
b'#!/usr/bin/env fish',
b'#!/usr/bin/env pwsh',
b'#!/usr/bin/env python',
b'#!/usr/bin/make -f',
])
integration_shebangs = set([
b'#!/bin/sh',
b'#!/usr/bin/env bash',
b'#!/usr/bin/env python',
])
module_shebangs = {
'': b'#!/usr/bin/python',
'.py': b'#!/usr/bin/python',
'.ps1': b'#!powershell',
}
# see https://unicode.org/faq/utf_bom.html#bom1
byte_order_marks = (
(b'\x00\x00\xFE\xFF', 'UTF-32 (BE)'),
(b'\xFF\xFE\x00\x00', 'UTF-32 (LE)'),
(b'\xFE\xFF', 'UTF-16 (BE)'),
(b'\xFF\xFE', 'UTF-16 (LE)'),
(b'\xEF\xBB\xBF', 'UTF-8'),
)
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'rb') as path_fd:
shebang = path_fd.readline().strip()
mode = os.stat(path).st_mode
executable = (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) & mode
if not shebang or not shebang.startswith(b'#!'):
if executable:
print('%s:%d:%d: file without shebang should not be executable' % (path, 0, 0))
for mark, name in byte_order_marks:
if shebang.startswith(mark):
print('%s:%d:%d: file starts with a %s byte order mark' % (path, 0, 0, name))
break
continue
is_module = False
is_integration = False
dirname = os.path.dirname(path)
if path.startswith('lib/ansible/modules/'):
is_module = True
elif re.search('^test/support/[^/]+/plugins/modules/', path):
is_module = True
elif re.search('^test/support/[^/]+/collections/ansible_collections/[^/]+/[^/]+/plugins/modules/', path):
is_module = True
elif path == 'test/lib/ansible_test/_util/target/cli/ansible_test_cli_stub.py':
pass # ansible-test entry point must be executable and have a shebang
elif path.startswith('lib/') or path.startswith('test/lib/'):
if executable:
print('%s:%d:%d: should not be executable' % (path, 0, 0))
if shebang:
print('%s:%d:%d: should not have a shebang' % (path, 0, 0))
continue
elif path.startswith('test/integration/targets/') or path.startswith('tests/integration/targets/'):
is_integration = True
if dirname.endswith('/library') or '/plugins/modules' in dirname or dirname in (
# non-standard module library directories
'test/integration/targets/module_precedence/lib_no_extension',
'test/integration/targets/module_precedence/lib_with_extension',
):
is_module = True
elif path.startswith('plugins/modules/'):
is_module = True
if is_module:
if executable:
print('%s:%d:%d: module should not be executable' % (path, 0, 0))
ext = os.path.splitext(path)[1]
expected_shebang = module_shebangs.get(ext)
expected_ext = ' or '.join(['"%s"' % k for k in module_shebangs])
if expected_shebang:
if shebang == expected_shebang:
continue
print('%s:%d:%d: expected module shebang "%s" but found: %s' % (path, 1, 1, expected_shebang, shebang))
else:
print('%s:%d:%d: expected module extension %s but found: %s' % (path, 0, 0, expected_ext, ext))
else:
if is_integration:
allowed = integration_shebangs
else:
allowed = standard_shebangs
if shebang not in allowed:
print('%s:%d:%d: unexpected non-module shebang: %s' % (path, 1, 1, shebang))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,5 @@
{
"include_directories": true,
"include_symlinks": true,
"output": "path-message"
}

View File

@@ -0,0 +1,31 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
def main():
root_dir = os.getcwd() + os.path.sep
for path in sys.argv[1:] or sys.stdin.read().splitlines():
if not os.path.islink(path.rstrip(os.path.sep)):
continue
if not os.path.exists(path):
print('%s: broken symlinks are not allowed' % path)
continue
if path.endswith(os.path.sep):
print('%s: symlinks to directories are not allowed' % path)
continue
real_path = os.path.realpath(path)
if not real_path.startswith(root_dir):
print('%s: symlinks outside content tree are not allowed: %s' % (path, os.path.relpath(real_path, os.path.dirname(path))))
continue
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,10 @@
{
"prefixes": [
"lib/ansible/modules/",
"plugins/modules/"
],
"extensions": [
".py"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'(expanduser)', text)
if match:
print('%s:%d:%d: use argspec type="path" instead of type="str" to avoid use of `expanduser`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,6 @@
{
"extensions": [
".py"
],
"output": "path-line-column-message"
}

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import sys
def main():
for path in sys.argv[1:] or sys.stdin.read().splitlines():
with open(path, 'r') as path_fd:
for line, text in enumerate(path_fd.readlines()):
match = re.search(r'((^\s*import\s+six\b)|(^\s*from\s+six\b))', text)
if match:
print('%s:%d:%d: use `ansible.module_utils.six` instead of `six`' % (
path, line + 1, match.start(1) + 1))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,15 @@
"""Read YAML from stdin and write JSON to stdout."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import sys
from yaml import load
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
json.dump(load(sys.stdin, Loader=SafeLoader), sys.stdout)

View File

@@ -0,0 +1,4 @@
E402
W503
W504
E741

View File

@@ -0,0 +1,41 @@
#Requires -Version 6
#Requires -Modules PSScriptAnalyzer, PSSA-PSCustomUseLiteralPath
$ErrorActionPreference = "Stop"
$WarningPreference = "Stop"
# Until https://github.com/PowerShell/PSScriptAnalyzer/issues/1217 is fixed we need to import Pester if it's
# available.
if (Get-Module -Name Pester -ListAvailable -ErrorAction SilentlyContinue) {
Import-Module -Name Pester
}
$LiteralPathRule = Import-Module -Name PSSA-PSCustomUseLiteralPath -PassThru
$LiteralPathRulePath = Join-Path -Path $LiteralPathRule.ModuleBase -ChildPath $LiteralPathRule.RootModule
$PSSAParams = @{
CustomRulePath = @($LiteralPathRulePath)
IncludeDefaultRules = $true
Setting = (Join-Path -Path $PSScriptRoot -ChildPath "settings.psd1")
}
$Results = @(ForEach ($Path in $Args) {
$Retries = 3
Do {
Try {
Invoke-ScriptAnalyzer -Path $Path @PSSAParams 3> $null
$Retries = 0
}
Catch {
If (--$Retries -le 0) {
Throw
}
}
}
Until ($Retries -le 0)
})
# Since pwsh 7.1 results that exceed depth will produce a warning which fails the process.
# Ignore warnings only for this step.
ConvertTo-Json -InputObject $Results -Depth 1 -WarningAction SilentlyContinue

View File

@@ -0,0 +1,13 @@
@{
ExcludeRules=@(
'PSUseOutputTypeCorrectly',
'PSUseShouldProcessForStateChangingFunctions',
# We send strings as plaintext so will always come across the 3 issues
'PSAvoidUsingPlainTextForPassword',
'PSAvoidUsingConvertToSecureStringWithPlainText',
'PSAvoidUsingUserNameAndPassWordParams',
# We send the module as a base64 encoded string and a BOM will cause
# issues here
'PSUseBOMForUnicodeEncodedFile'
)
}

View File

@@ -0,0 +1,54 @@
[MESSAGES CONTROL]
disable=
consider-using-dict-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
consider-using-set-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
cyclic-import, # consistent results require running with --jobs 1 and testing all files
duplicate-code, # consistent results require running with --jobs 1 and testing all files
import-error, # inconsistent results which depend on the availability of imports
import-outside-toplevel, # common pattern in ansible related code
no-name-in-module, # inconsistent results which depend on the availability of imports
no-self-use,
raise-missing-from, # Python 2.x does not support raise from
super-with-arguments, # Python 2.x does not support super without arguments
too-few-public-methods,
too-many-ancestors, # inconsistent results between python 3.6 and 3.7+
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-return-statements,
too-many-statements,
useless-return, # complains about returning None when the return type is optional
[BASIC]
bad-names=
_,
bar,
baz,
foo,
tata,
toto,
tutu,
good-names=
__metaclass__,
C,
ex,
i,
j,
k,
Run,
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,40}$
attr-rgx=[a-z_][a-z0-9_]{1,40}$
method-rgx=[a-z_][a-z0-9_]{1,40}$
function-rgx=[a-z_][a-z0-9_]{1,40}$
[IMPORTS]
preferred-modules =
distutils.version:ansible.module_utils.compat.version,

View File

@@ -0,0 +1,50 @@
[MESSAGES CONTROL]
disable=
cyclic-import, # consistent results require running with --jobs 1 and testing all files
duplicate-code, # consistent results require running with --jobs 1 and testing all files
import-error, # inconsistent results which depend on the availability of imports
import-outside-toplevel, # common pattern in ansible related code
no-name-in-module, # inconsistent results which depend on the availability of imports
no-self-use,
raise-missing-from, # Python 2.x does not support raise from
too-few-public-methods,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-return-statements,
too-many-statements,
useless-return, # complains about returning None when the return type is optional
[BASIC]
bad-names=
_,
bar,
baz,
foo,
tata,
toto,
tutu,
good-names=
__metaclass__,
C,
ex,
i,
j,
k,
Run,
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,40}$
attr-rgx=[a-z_][a-z0-9_]{1,40}$
method-rgx=[a-z_][a-z0-9_]{1,40}$
function-rgx=[a-z_][a-z0-9_]{1,40}$
[IMPORTS]
preferred-modules =
distutils.version:ansible.module_utils.compat.version,

View File

@@ -0,0 +1,55 @@
[MESSAGES CONTROL]
disable=
cyclic-import, # consistent results require running with --jobs 1 and testing all files
duplicate-code, # consistent results require running with --jobs 1 and testing all files
import-error, # inconsistent results which depend on the availability of imports
import-outside-toplevel, # common pattern in ansible related code
no-name-in-module, # inconsistent results which depend on the availability of imports
no-self-use,
raise-missing-from, # Python 2.x does not support raise from
too-few-public-methods,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-return-statements,
too-many-statements,
useless-return, # complains about returning None when the return type is optional
# code-smell tests should be updated so the following rules can be enabled
# once that happens the pylint sanity test can be updated to no longer special-case the code-smell tests (use standard ansible-test config instead)
missing-module-docstring,
missing-function-docstring,
[BASIC]
bad-names=
_,
bar,
baz,
foo,
tata,
toto,
tutu,
good-names=
__metaclass__,
C,
ex,
i,
j,
k,
Run,
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,40}$
attr-rgx=[a-z_][a-z0-9_]{1,40}$
method-rgx=[a-z_][a-z0-9_]{1,40}$
function-rgx=[a-z_][a-z0-9_]{1,40}$
module-rgx=[a-z_][a-z0-9_-]{2,40}$
[IMPORTS]
preferred-modules =
distutils.version:ansible.module_utils.compat.version,

View File

@@ -0,0 +1,143 @@
[MESSAGES CONTROL]
disable=
abstract-method,
access-member-before-definition,
arguments-differ,
assignment-from-no-return,
assignment-from-none,
attribute-defined-outside-init,
bad-continuation,
bad-indentation,
bad-mcs-classmethod-argument,
broad-except,
c-extension-no-member,
cell-var-from-loop,
chained-comparison,
comparison-with-callable,
consider-iterating-dictionary,
consider-merging-isinstance,
consider-using-dict-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
consider-using-dict-items,
consider-using-enumerate,
consider-using-get,
consider-using-in,
consider-using-set-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
consider-using-ternary,
consider-using-with,
cyclic-import, # consistent results require running with --jobs 1 and testing all files
deprecated-lambda,
deprecated-method,
deprecated-module,
duplicate-code, # consistent results require running with --jobs 1 and testing all files
eval-used,
exec-used,
expression-not-assigned,
fixme,
function-redefined,
global-statement,
global-variable-undefined,
import-error, # inconsistent results which depend on the availability of imports
import-outside-toplevel, # common pattern in ansible related code
import-self,
inconsistent-return-statements,
invalid-envvar-default,
invalid-name,
invalid-sequence-index,
keyword-arg-before-vararg,
len-as-condition,
line-too-long,
literal-comparison,
locally-disabled,
method-hidden,
misplaced-comparison-constant,
missing-docstring,
no-else-break,
no-else-continue,
no-else-raise,
no-else-return,
no-init,
no-member,
no-name-in-module, # inconsistent results which depend on the availability of imports
no-self-use,
no-value-for-parameter,
non-iterator-returned,
not-a-mapping,
not-an-iterable,
not-callable,
old-style-class,
pointless-statement,
pointless-string-statement,
possibly-unused-variable,
protected-access,
raise-missing-from, # Python 2.x does not support raise from
redefined-argument-from-local,
redefined-builtin,
redefined-outer-name,
redefined-variable-type,
reimported,
relative-beyond-top-level, # https://github.com/PyCQA/pylint/issues/2967
signature-differs,
simplifiable-if-expression,
simplifiable-if-statement,
subprocess-popen-preexec-fn,
super-init-not-called,
super-with-arguments, # Python 2.x does not support super without arguments
superfluous-parens,
too-few-public-methods,
too-many-ancestors, # inconsistent results between python 3.6 and 3.7+
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-function-args,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
trailing-comma-tuple,
trailing-comma-tuple,
try-except-raise,
unbalanced-tuple-unpacking,
undefined-loop-variable,
unexpected-keyword-arg,
ungrouped-imports,
unidiomatic-typecheck,
unnecessary-pass,
unsubscriptable-object,
unsupported-assignment-operation,
unsupported-delete-operation,
unsupported-membership-test,
unused-argument,
unused-import,
unused-variable,
useless-object-inheritance,
useless-return,
useless-super-delegation,
wrong-import-order,
wrong-import-position,
[BASIC]
bad-names=
_,
bar,
baz,
foo,
tata,
toto,
tutu,
good-names=
ex,
i,
j,
k,
Run,
[TYPECHECK]
ignored-modules=
_MovedItems,

View File

@@ -0,0 +1,148 @@
[MESSAGES CONTROL]
disable=
import-outside-toplevel, # common pattern in ansible related code
abstract-method,
access-member-before-definition,
arguments-differ,
assignment-from-no-return,
assignment-from-none,
attribute-defined-outside-init,
bad-continuation,
bad-indentation,
bad-mcs-classmethod-argument,
broad-except,
c-extension-no-member,
cell-var-from-loop,
chained-comparison,
comparison-with-callable,
consider-iterating-dictionary,
consider-merging-isinstance,
consider-using-dict-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
consider-using-dict-items,
consider-using-enumerate,
consider-using-get,
consider-using-in,
consider-using-set-comprehension, # requires Python 2.7+, but we still require Python 2.6 support
consider-using-ternary,
consider-using-with,
cyclic-import, # consistent results require running with --jobs 1 and testing all files
deprecated-lambda,
deprecated-method,
deprecated-module,
duplicate-code, # consistent results require running with --jobs 1 and testing all files
eval-used,
exec-used,
expression-not-assigned,
fixme,
function-redefined,
global-statement,
global-variable-undefined,
import-error, # inconsistent results which depend on the availability of imports
import-self,
inconsistent-return-statements,
invalid-envvar-default,
invalid-name,
invalid-sequence-index,
keyword-arg-before-vararg,
len-as-condition,
line-too-long,
literal-comparison,
locally-disabled,
method-hidden,
misplaced-comparison-constant,
missing-docstring,
no-else-break,
no-else-continue,
no-else-raise,
no-else-return,
no-init,
no-member,
no-name-in-module, # inconsistent results which depend on the availability of imports
no-self-use,
no-value-for-parameter,
non-iterator-returned,
not-a-mapping,
not-an-iterable,
not-callable,
old-style-class,
pointless-statement,
pointless-string-statement,
possibly-unused-variable,
protected-access,
raise-missing-from, # Python 2.x does not support raise from
redefined-argument-from-local,
redefined-builtin,
redefined-outer-name,
redefined-variable-type,
reimported,
relative-import,
signature-differs,
simplifiable-if-expression,
simplifiable-if-statement,
subprocess-popen-preexec-fn,
super-init-not-called,
super-with-arguments, # Python 2.x does not support super without arguments
superfluous-parens,
too-few-public-methods,
too-many-ancestors, # inconsistent results between python 3.6 and 3.7+
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-function-args,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
trailing-comma-tuple,
trailing-comma-tuple,
try-except-raise,
unbalanced-tuple-unpacking,
undefined-loop-variable,
unexpected-keyword-arg,
ungrouped-imports,
unidiomatic-typecheck,
unnecessary-pass,
unsubscriptable-object,
unsupported-assignment-operation,
unsupported-delete-operation,
unsupported-membership-test,
unused-argument,
unused-import,
unused-variable,
useless-object-inheritance,
useless-return,
useless-super-delegation,
wrong-import-order,
wrong-import-position,
[BASIC]
bad-names=
_,
bar,
baz,
foo,
tata,
toto,
tutu,
good-names=
ex,
i,
j,
k,
Run,
[TYPECHECK]
ignored-modules=
_MovedItems,
[IMPORTS]
preferred-modules =
distutils.version:ansible.module_utils.compat.version,

View File

@@ -0,0 +1,261 @@
"""Ansible specific plyint plugin for checking deprecations."""
# (c) 2018, Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import datetime
import re
import astroid
from pylint.interfaces import IAstroidChecker
from pylint.checkers import BaseChecker
from pylint.checkers.utils import check_messages
from ansible.module_utils.compat.version import LooseVersion
from ansible.module_utils.six import string_types
from ansible.release import __version__ as ansible_version_raw
from ansible.utils.version import SemanticVersion
MSGS = {
'E9501': ("Deprecated version (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate",
"ansible-deprecated-version",
"Used when a call to Display.deprecated specifies a version "
"less than or equal to the current version of Ansible",
{'minversion': (2, 6)}),
'E9502': ("Display.deprecated call without a version or date",
"ansible-deprecated-no-version",
"Used when a call to Display.deprecated does not specify a "
"version or date",
{'minversion': (2, 6)}),
'E9503': ("Invalid deprecated version (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid "
"Ansible version number",
{'minversion': (2, 6)}),
'E9504': ("Deprecated version (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate",
"collection-deprecated-version",
"Used when a call to Display.deprecated specifies a collection "
"version less than or equal to the current version of this "
"collection",
{'minversion': (2, 6)}),
'E9505': ("Invalid deprecated version (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"collection-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid "
"collection version number",
{'minversion': (2, 6)}),
'E9506': ("No collection name found in call to Display.deprecated or "
"AnsibleModule.deprecate",
"ansible-deprecated-no-collection-name",
"The current collection name in format `namespace.name` must "
"be provided as collection_name when calling Display.deprecated "
"or AnsibleModule.deprecate (`ansible.builtin` for ansible-core)",
{'minversion': (2, 6)}),
'E9507': ("Wrong collection name (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"wrong-collection-deprecated",
"The name of the current collection must be passed to the "
"Display.deprecated resp. AnsibleModule.deprecate calls "
"(`ansible.builtin` for ansible-core)",
{'minversion': (2, 6)}),
'E9508': ("Expired date (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate",
"ansible-deprecated-date",
"Used when a call to Display.deprecated specifies a date "
"before today",
{'minversion': (2, 6)}),
'E9509': ("Invalid deprecated date (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-date",
"Used when a call to Display.deprecated specifies an invalid "
"date. It must be a string in format `YYYY-MM-DD` (ISO 8601)",
{'minversion': (2, 6)}),
'E9510': ("Both version and date found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"ansible-deprecated-both-version-and-date",
"Only one of version and date must be specified",
{'minversion': (2, 6)}),
'E9511': ("Removal version (%r) must be a major release, not a minor or "
"patch release (see the specification at https://semver.org/)",
"removal-version-must-be-major",
"Used when a call to Display.deprecated or "
"AnsibleModule.deprecate for a collection specifies a version "
"which is not of the form x.0.0",
{'minversion': (2, 6)}),
}
ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3]))
def _get_expr_name(node):
"""Funciton to get either ``attrname`` or ``name`` from ``node.func.expr``
Created specifically for the case of ``display.deprecated`` or ``self._display.deprecated``
"""
try:
return node.func.expr.attrname
except AttributeError:
# If this fails too, we'll let it raise, the caller should catch it
return node.func.expr.name
def parse_isodate(value):
"""Parse an ISO 8601 date string."""
msg = 'Expected ISO 8601 date string (YYYY-MM-DD)'
if not isinstance(value, string_types):
raise ValueError(msg)
# From Python 3.7 in, there is datetime.date.fromisoformat(). For older versions,
# we have to do things manually.
if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', value):
raise ValueError(msg)
try:
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
raise ValueError(msg)
class AnsibleDeprecatedChecker(BaseChecker):
"""Checks for Display.deprecated calls to ensure that the ``version``
has not passed or met the time for removal
"""
__implements__ = (IAstroidChecker,)
name = 'deprecated'
msgs = MSGS
options = (
('collection-name', {
'default': None,
'type': 'string',
'metavar': '<name>',
'help': 'The collection\'s name used to check collection names in deprecations.',
}),
('collection-version', {
'default': None,
'type': 'string',
'metavar': '<version>',
'help': 'The collection\'s version number used to check deprecations.',
}),
)
def __init__(self, *args, **kwargs):
self.collection_version = None
self.collection_name = None
super().__init__(*args, **kwargs)
def set_option(self, optname, value, action=None, optdict=None):
super().set_option(optname, value, action, optdict)
if optname == 'collection-version' and value is not None:
self.collection_version = SemanticVersion(self.config.collection_version)
if optname == 'collection-name' and value is not None:
self.collection_name = self.config.collection_name
def _check_date(self, node, date):
if not isinstance(date, str):
self.add_message('invalid-date', node=node, args=(date,))
return
try:
date_parsed = parse_isodate(date)
except ValueError:
self.add_message('ansible-invalid-deprecated-date', node=node, args=(date,))
return
if date_parsed < datetime.date.today():
self.add_message('ansible-deprecated-date', node=node, args=(date,))
def _check_version(self, node, version, collection_name):
if not isinstance(version, (str, float)):
self.add_message('invalid-version', node=node, args=(version,))
return
version_no = str(version)
if collection_name == 'ansible.builtin':
# Ansible-base
try:
if not version_no:
raise ValueError('Version string should not be empty')
loose_version = LooseVersion(str(version_no))
if ANSIBLE_VERSION >= loose_version:
self.add_message('ansible-deprecated-version', node=node, args=(version,))
except ValueError:
self.add_message('ansible-invalid-deprecated-version', node=node, args=(version,))
elif collection_name:
# Collections
try:
if not version_no:
raise ValueError('Version string should not be empty')
semantic_version = SemanticVersion(version_no)
if collection_name == self.collection_name and self.collection_version is not None:
if self.collection_version >= semantic_version:
self.add_message('collection-deprecated-version', node=node, args=(version,))
if semantic_version.major != 0 and (semantic_version.minor != 0 or semantic_version.patch != 0):
self.add_message('removal-version-must-be-major', node=node, args=(version,))
except ValueError:
self.add_message('collection-invalid-deprecated-version', node=node, args=(version,))
@check_messages(*(MSGS.keys()))
def visit_call(self, node):
"""Visit a call node."""
version = None
date = None
collection_name = None
try:
if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or
node.func.attrname == 'deprecate' and _get_expr_name(node)):
if node.keywords:
for keyword in node.keywords:
if len(node.keywords) == 1 and keyword.arg is None:
# This is likely a **kwargs splat
return
if keyword.arg == 'version':
if isinstance(keyword.value.value, astroid.Name):
# This is likely a variable
return
version = keyword.value.value
if keyword.arg == 'date':
if isinstance(keyword.value.value, astroid.Name):
# This is likely a variable
return
date = keyword.value.value
if keyword.arg == 'collection_name':
if isinstance(keyword.value.value, astroid.Name):
# This is likely a variable
return
collection_name = keyword.value.value
if not version and not date:
try:
version = node.args[1].value
except IndexError:
self.add_message('ansible-deprecated-no-version', node=node)
return
if version and date:
self.add_message('ansible-deprecated-both-version-and-date', node=node)
if collection_name:
this_collection = collection_name == (self.collection_name or 'ansible.builtin')
if not this_collection:
self.add_message('wrong-collection-deprecated', node=node, args=(collection_name,))
elif self.collection_name is not None:
self.add_message('ansible-deprecated-no-collection-name', node=node)
if date:
self._check_date(node, date)
elif version:
self._check_version(node, version, collection_name)
except AttributeError:
# Not the type of node we are interested in
pass
def register(linter):
"""required method to auto register this checker """
linter.register_checker(AnsibleDeprecatedChecker(linter))

View File

@@ -0,0 +1,86 @@
"""Ansible specific pylint plugin for checking format string usage."""
# (c) 2018, Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import astroid
from pylint.interfaces import IAstroidChecker
from pylint.checkers import BaseChecker
from pylint.checkers import utils
from pylint.checkers.utils import check_messages
try:
from pylint.checkers.utils import parse_format_method_string
except ImportError:
# noinspection PyUnresolvedReferences
from pylint.checkers.strings import parse_format_method_string
MSGS = {
'E9305': ("Format string contains automatic field numbering "
"specification",
"ansible-format-automatic-specification",
"Used when a PEP 3101 format string contains automatic "
"field numbering (e.g. '{}').",
{'minversion': (2, 6)}),
'E9390': ("bytes object has no .format attribute",
"ansible-no-format-on-bytestring",
"Used when a bytestring was used as a PEP 3101 format string "
"as Python3 bytestrings do not have a .format attribute",
{'minversion': (3, 0)}),
}
class AnsibleStringFormatChecker(BaseChecker):
"""Checks string formatting operations to ensure that the format string
is valid and the arguments match the format string.
"""
__implements__ = (IAstroidChecker,)
name = 'string'
msgs = MSGS
@check_messages(*(MSGS.keys()))
def visit_call(self, node):
"""Visit a call node."""
func = utils.safe_infer(node.func)
if (isinstance(func, astroid.BoundMethod)
and isinstance(func.bound, astroid.Instance)
and func.bound.name in ('str', 'unicode', 'bytes')):
if func.name == 'format':
self._check_new_format(node, func)
def _check_new_format(self, node, func):
""" Check the new string formatting """
if (isinstance(node.func, astroid.Attribute)
and not isinstance(node.func.expr, astroid.Const)):
return
try:
strnode = next(func.bound.infer())
except astroid.InferenceError:
return
if not isinstance(strnode, astroid.Const):
return
if isinstance(strnode.value, bytes):
self.add_message('ansible-no-format-on-bytestring', node=node)
return
if not isinstance(strnode.value, str):
return
if node.starargs or node.kwargs:
return
try:
num_args = parse_format_method_string(strnode.value)[1]
except utils.IncompleteFormatString:
return
if num_args:
self.add_message('ansible-format-automatic-specification',
node=node)
return
def register(linter):
"""required method to auto register this checker """
linter.register_checker(AnsibleStringFormatChecker(linter))

View File

@@ -0,0 +1,216 @@
"""A plugin for pylint to identify imports and functions which should not be used."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import typing as t
import astroid
from pylint.checkers import BaseChecker
from pylint.interfaces import IAstroidChecker
ANSIBLE_TEST_MODULES_PATH = os.environ['ANSIBLE_TEST_MODULES_PATH']
ANSIBLE_TEST_MODULE_UTILS_PATH = os.environ['ANSIBLE_TEST_MODULE_UTILS_PATH']
class UnwantedEntry:
"""Defines an unwanted import."""
def __init__(
self,
alternative, # type: str
modules_only=False, # type: bool
names=None, # type: t.Optional[t.Tuple[str, ...]]
ignore_paths=None, # type: t.Optional[t.Tuple[str, ...]]
): # type: (...) -> None
self.alternative = alternative
self.modules_only = modules_only
self.names = set(names) if names else set()
self.ignore_paths = ignore_paths
def applies_to(self, path, name=None): # type: (str, t.Optional[str]) -> bool
"""Return True if this entry applies to the given path, otherwise return False."""
if self.names:
if not name:
return False
if name not in self.names:
return False
if self.ignore_paths and any(path.endswith(ignore_path) for ignore_path in self.ignore_paths):
return False
if self.modules_only:
return is_module_path(path)
return True
def is_module_path(path): # type: (str) -> bool
"""Return True if the given path is a module or module_utils path, otherwise return False."""
return path.startswith(ANSIBLE_TEST_MODULES_PATH) or path.startswith(ANSIBLE_TEST_MODULE_UTILS_PATH)
class AnsibleUnwantedChecker(BaseChecker):
"""Checker for unwanted imports and functions."""
__implements__ = (IAstroidChecker,)
name = 'unwanted'
BAD_IMPORT = 'ansible-bad-import'
BAD_IMPORT_FROM = 'ansible-bad-import-from'
BAD_FUNCTION = 'ansible-bad-function'
BAD_MODULE_IMPORT = 'ansible-bad-module-import'
msgs = dict(
E5101=('Import %s instead of %s',
BAD_IMPORT,
'Identifies imports which should not be used.'),
E5102=('Import %s from %s instead of %s',
BAD_IMPORT_FROM,
'Identifies imports which should not be used.'),
E5103=('Call %s instead of %s',
BAD_FUNCTION,
'Identifies functions which should not be used.'),
E5104=('Import external package or ansible.module_utils not %s',
BAD_MODULE_IMPORT,
'Identifies imports which should not be used.'),
)
unwanted_imports = dict(
# Additional imports that we may want to start checking:
# boto=UnwantedEntry('boto3', modules_only=True),
# requests=UnwantedEntry('ansible.module_utils.urls', modules_only=True),
# urllib=UnwantedEntry('ansible.module_utils.urls', modules_only=True),
# see https://docs.python.org/2/library/urllib2.html
urllib2=UnwantedEntry('ansible.module_utils.urls',
ignore_paths=(
'/lib/ansible/module_utils/urls.py',
)),
# see https://docs.python.org/3/library/collections.abc.html
collections=UnwantedEntry('ansible.module_utils.common._collections_compat',
ignore_paths=(
'/lib/ansible/module_utils/common/_collections_compat.py',
),
names=(
'MappingView',
'ItemsView',
'KeysView',
'ValuesView',
'Mapping', 'MutableMapping',
'Sequence', 'MutableSequence',
'Set', 'MutableSet',
'Container',
'Hashable',
'Sized',
'Callable',
'Iterable',
'Iterator',
)),
)
unwanted_functions = {
# see https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
'tempfile.mktemp': UnwantedEntry('tempfile.mkstemp'),
'sys.exit': UnwantedEntry('exit_json or fail_json',
ignore_paths=(
'/lib/ansible/module_utils/basic.py',
'/lib/ansible/modules/async_wrapper.py',
),
modules_only=True),
'builtins.print': UnwantedEntry('module.log or module.debug',
ignore_paths=(
'/lib/ansible/module_utils/basic.py',
),
modules_only=True),
}
def visit_import(self, node): # type: (astroid.node_classes.Import) -> None
"""Visit an import node."""
for name in node.names:
self._check_import(node, name[0])
def visit_importfrom(self, node): # type: (astroid.node_classes.ImportFrom) -> None
"""Visit an import from node."""
self._check_importfrom(node, node.modname, node.names)
def visit_attribute(self, node): # type: (astroid.node_classes.Attribute) -> None
"""Visit an attribute node."""
last_child = node.last_child()
# this is faster than using type inference and will catch the most common cases
if not isinstance(last_child, astroid.node_classes.Name):
return
module = last_child.name
entry = self.unwanted_imports.get(module)
if entry and entry.names:
if entry.applies_to(self.linter.current_file, node.attrname):
self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node)
def visit_call(self, node): # type: (astroid.node_classes.Call) -> None
"""Visit a call node."""
try:
for i in node.func.inferred():
func = None
if isinstance(i, astroid.scoped_nodes.FunctionDef) and isinstance(i.parent, astroid.scoped_nodes.Module):
func = '%s.%s' % (i.parent.name, i.name)
if not func:
continue
entry = self.unwanted_functions.get(func)
if entry and entry.applies_to(self.linter.current_file):
self.add_message(self.BAD_FUNCTION, args=(entry.alternative, func), node=node)
except astroid.exceptions.InferenceError:
pass
def _check_import(self, node, modname): # type: (astroid.node_classes.Import, str) -> None
"""Check the imports on the specified import node."""
self._check_module_import(node, modname)
entry = self.unwanted_imports.get(modname)
if not entry:
return
if entry.applies_to(self.linter.current_file):
self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node)
def _check_importfrom(self, node, modname, names): # type: (astroid.node_classes.ImportFrom, str, t.List[str]) -> None
"""Check the imports on the specified import from node."""
self._check_module_import(node, modname)
entry = self.unwanted_imports.get(modname)
if not entry:
return
for name in names:
if entry.applies_to(self.linter.current_file, name[0]):
self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node)
def _check_module_import(self, node, modname): # type: (t.Union[astroid.node_classes.Import, astroid.node_classes.ImportFrom], str) -> None
"""Check the module import on the given import or import from node."""
if not is_module_path(self.linter.current_file):
return
if modname == 'ansible.module_utils' or modname.startswith('ansible.module_utils.'):
return
if modname == 'ansible' or modname.startswith('ansible.'):
self.add_message(self.BAD_MODULE_IMPORT, args=(modname,), node=node)
def register(linter):
"""required method to auto register this checker """
linter.register_checker(AnsibleUnwantedChecker(linter))

View File

@@ -0,0 +1,3 @@
SC1090
SC1091
SC2164

View File

@@ -0,0 +1,7 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from validate_modules.main import main
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 Matt Martz <matt@sivel.net>
# Copyright (C) 2015 Rackspace US, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
__version__ = '0.0.1b'

View File

@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 Matt Martz <matt@sivel.net>
# Copyright (C) 2016 Rackspace US, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import runpy
import inspect
import json
import os
import subprocess
import sys
from contextlib import contextmanager
from ansible.executor.powershell.module_manifest import PSModuleDepFinder
from ansible.module_utils.basic import FILE_COMMON_ARGUMENTS, AnsibleModule
from ansible.module_utils.six import reraise
from ansible.module_utils._text import to_bytes, to_text
from .utils import CaptureStd, find_executable, get_module_name_from_filename
ANSIBLE_MODULE_CONSTRUCTOR_ARGS = tuple(list(inspect.signature(AnsibleModule.__init__).parameters)[1:])
class AnsibleModuleCallError(RuntimeError):
pass
class AnsibleModuleImportError(ImportError):
pass
class AnsibleModuleNotInitialized(Exception):
pass
class _FakeAnsibleModuleInit:
def __init__(self):
self.args = tuple()
self.kwargs = {}
self.called = False
def __call__(self, *args, **kwargs):
if args and isinstance(args[0], AnsibleModule):
# Make sure, due to creative calling, that we didn't end up with
# ``self`` in ``args``
self.args = args[1:]
else:
self.args = args
self.kwargs = kwargs
self.called = True
raise AnsibleModuleCallError('AnsibleModuleCallError')
def _fake_load_params():
pass
@contextmanager
def setup_env(filename):
# Used to clean up imports later
pre_sys_modules = list(sys.modules.keys())
fake = _FakeAnsibleModuleInit()
module = __import__('ansible.module_utils.basic').module_utils.basic
_original_init = module.AnsibleModule.__init__
_original_load_params = module._load_params
setattr(module.AnsibleModule, '__init__', fake)
setattr(module, '_load_params', _fake_load_params)
try:
yield fake
finally:
setattr(module.AnsibleModule, '__init__', _original_init)
setattr(module, '_load_params', _original_load_params)
# Clean up imports to prevent issues with mutable data being used in modules
for k in list(sys.modules.keys()):
# It's faster if we limit to items in ansible.module_utils
# But if this causes problems later, we should remove it
if k not in pre_sys_modules and k.startswith('ansible.module_utils.'):
del sys.modules[k]
def get_ps_argument_spec(filename, collection):
fqc_name = get_module_name_from_filename(filename, collection)
pwsh = find_executable('pwsh')
if not pwsh:
raise FileNotFoundError('Required program for PowerShell arg spec inspection "pwsh" not found.')
module_path = os.path.join(os.getcwd(), filename)
b_module_path = to_bytes(module_path, errors='surrogate_or_strict')
with open(b_module_path, mode='rb') as module_fd:
b_module_data = module_fd.read()
ps_dep_finder = PSModuleDepFinder()
ps_dep_finder.scan_module(b_module_data, fqn=fqc_name)
# For ps_argspec.ps1 to compile Ansible.Basic it also needs the AddType module_util.
ps_dep_finder._add_module(name=b"Ansible.ModuleUtils.AddType", ext=".psm1", fqn=None, optional=False, wrapper=False)
util_manifest = json.dumps({
'module_path': to_text(module_path, errors='surrogiate_or_strict'),
'ansible_basic': ps_dep_finder.cs_utils_module["Ansible.Basic"]['path'],
'ps_utils': dict([(name, info['path']) for name, info in ps_dep_finder.ps_modules.items()]),
})
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ps_argspec.ps1')
proc = subprocess.Popen(['pwsh', script_path, util_manifest], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=False)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
raise AnsibleModuleImportError("STDOUT:\n%s\nSTDERR:\n%s" % (stdout.decode('utf-8'), stderr.decode('utf-8')))
kwargs = json.loads(stdout)
# the validate-modules code expects the options spec to be under the argument_spec key not options as set in PS
kwargs['argument_spec'] = kwargs.pop('options', {})
return kwargs['argument_spec'], kwargs
def get_py_argument_spec(filename, collection):
name = get_module_name_from_filename(filename, collection)
with setup_env(filename) as fake:
try:
with CaptureStd():
runpy.run_module(name, run_name='__main__', alter_sys=True)
except AnsibleModuleCallError:
pass
except BaseException as e:
# we want to catch all exceptions here, including sys.exit
reraise(AnsibleModuleImportError, AnsibleModuleImportError('%s' % e), sys.exc_info()[2])
if not fake.called:
raise AnsibleModuleNotInitialized()
try:
# Convert positional arguments to kwargs to make sure that all parameters are actually checked
for arg, arg_name in zip(fake.args, ANSIBLE_MODULE_CONSTRUCTOR_ARGS):
fake.kwargs[arg_name] = arg
# for ping kwargs == {'argument_spec':{'data':{'type':'str','default':'pong'}}, 'supports_check_mode':True}
argument_spec = fake.kwargs.get('argument_spec') or {}
# If add_file_common_args is truish, add options from FILE_COMMON_ARGUMENTS when not present.
# This is the only modification to argument_spec done by AnsibleModule itself, and which is
# not caught by setup_env's AnsibleModule replacement
if fake.kwargs.get('add_file_common_args'):
for k, v in FILE_COMMON_ARGUMENTS.items():
if k not in argument_spec:
argument_spec[k] = v
return argument_spec, fake.kwargs
except (TypeError, IndexError):
return {}, {}
def get_argument_spec(filename, collection):
if filename.endswith('.py'):
return get_py_argument_spec(filename, collection)
else:
return get_ps_argument_spec(filename, collection)

View File

@@ -0,0 +1,109 @@
#Requires -Version 6
Set-StrictMode -Version 2.0
$ErrorActionPreference = "Stop"
$WarningPreference = "Stop"
Function Resolve-CircularReference {
<#
.SYNOPSIS
Removes known types that cause a circular reference in their json serialization.
.PARAMETER Hash
The hash to scan for circular references
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$Hash
)
foreach ($key in [String[]]$Hash.Keys) {
$value = $Hash[$key]
if ($value -is [System.Collections.IDictionary]) {
Resolve-CircularReference -Hash $value
} elseif ($value -is [Array] -or $value -is [System.Collections.IList]) {
$values = @(foreach ($v in $value) {
if ($v -is [System.Collections.IDictionary]) {
Resolve-CircularReference -Hash $v
}
,$v
})
$Hash[$key] = $values
} elseif ($value -is [DateTime]) {
$Hash[$key] = $value.ToString("yyyy-MM-dd")
} elseif ($value -is [delegate]) {
# Type can be set to a delegate function which defines it's own type. For the documentation we just
# reflection that as raw
if ($key -eq 'type') {
$Hash[$key] = 'raw'
} else {
$Hash[$key] = $value.ToString() # Shouldn't ever happen but just in case.
}
}
}
}
$manifest = ConvertFrom-Json -InputObject $args[0] -AsHashtable
if (-not $manifest.Contains('module_path') -or -not $manifest.module_path) {
Write-Error -Message "No module specified."
exit 1
}
$module_path = $manifest.module_path
# Check if the path is relative and get the full path to the module
if (-not ([System.IO.Path]::IsPathRooted($module_path))) {
$module_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($module_path)
}
if (-not (Test-Path -LiteralPath $module_path -PathType Leaf)) {
Write-Error -Message "The module at '$module_path' does not exist."
exit 1
}
$module_code = Get-Content -LiteralPath $module_path -Raw
$powershell = [PowerShell]::Create()
$powershell.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
# Load the PowerShell module utils as the module may be using them to refer to shared module options. Currently we
# can only load the PowerShell utils due to cross platform compatibility issues.
if ($manifest.Contains('ps_utils')) {
foreach ($util_info in $manifest.ps_utils.GetEnumerator()) {
$util_name = $util_info.Key
$util_path = $util_info.Value
if (-not (Test-Path -LiteralPath $util_path -PathType Leaf)) {
# Failed to find the util path, just silently ignore for now and hope for the best.
continue
}
$util_sb = [ScriptBlock]::Create((Get-Content -LiteralPath $util_path -Raw))
$powershell.AddCommand('New-Module').AddParameters(@{
Name = $util_name
ScriptBlock = $util_sb
}) > $null
$powershell.AddCommand('Import-Module').AddParameter('WarningAction', 'SilentlyContinue') > $null
$powershell.AddCommand('Out-Null').AddStatement() > $null
# Also import it into the current runspace in case ps_argspec.ps1 needs to use it.
$null = New-Module -Name $util_name -ScriptBlock $util_sb | Import-Module -WarningAction SilentlyContinue
}
}
Add-CSharpType -References @(Get-Content -LiteralPath $manifest.ansible_basic -Raw)
[Ansible.Basic.AnsibleModule]::_DebugArgSpec = $true
$powershell.AddScript($module_code) > $null
$powershell.Invoke() > $null
if ($powershell.HadErrors) {
$powershell.Streams.Error
exit 1
}
$arg_spec = $powershell.Runspace.SessionStateProxy.GetVariable('ansibleTestArgSpec')
Resolve-CircularReference -Hash $arg_spec
ConvertTo-Json -InputObject $arg_spec -Compress -Depth 99

View File

@@ -0,0 +1,589 @@
# -*- 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 ansible.module_utils.compat.version import StrictVersion
from functools import partial
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
from ansible.utils.version import SemanticVersion
from ansible.release import __version__
from .utils import parse_isodate
list_string_types = list(string_types)
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'^([^.]+(\.[^.]+)+)$')
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)
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 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 list_dict_option_schema(for_collection):
suboption_schema = Schema(
{
Required('description'): Any(list_string_types, *string_types),
'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': 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': version(for_collection),
'version_added_collection': collection_name,
'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
)
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):
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': version(for_collection),
'version_added_collection': collection_name,
'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(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 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': version(for_collection),
'version_added_collection': collection_name,
'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({any_string_types: partial(version_added, error_code='option-invalid-version-added')}),
),
Schema(type(None)),
)
def deprecation_schema(for_collection):
main_fields = {
Required('why'): Any(*string_types),
Required('alternative'): Any(*string_types),
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):
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(for_collection)),
'extends_documentation_fragment': Any(list_string_types, *string_types),
'version_added_collection': collection_name,
}
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': Any(list_string_types, *string_types),
'details': Any(list_string_types, *string_types),
'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'): Any(list_string_types, *string_types),
Required('support'): Any('full', 'partial', 'none', 'N/A'),
'details': Any(list_string_types, *string_types),
'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,
}),
'forced_action_plugin': add_default_attributes({
Required('action_plugin'): any_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)

View File

@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 Matt Martz <matt@sivel.net>
# Copyright (C) 2015 Rackspace US, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import ast
import datetime
import os
import re
import sys
from io import BytesIO, TextIOWrapper
import yaml
import yaml.reader
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.yaml import SafeLoader
from ansible.module_utils.six import string_types
from ansible.parsing.yaml.loader import AnsibleLoader
class AnsibleTextIOWrapper(TextIOWrapper):
def write(self, s):
super(AnsibleTextIOWrapper, self).write(to_text(s, self.encoding, errors='replace'))
def find_executable(executable, cwd=None, path=None):
"""Finds the full path to the executable specified"""
match = None
real_cwd = os.getcwd()
if not cwd:
cwd = real_cwd
if os.path.dirname(executable):
target = os.path.join(cwd, executable)
if os.path.exists(target) and os.access(target, os.F_OK | os.X_OK):
match = executable
else:
path = os.environ.get('PATH', os.path.defpath)
path_dirs = path.split(os.path.pathsep)
seen_dirs = set()
for path_dir in path_dirs:
if path_dir in seen_dirs:
continue
seen_dirs.add(path_dir)
if os.path.abspath(path_dir) == real_cwd:
path_dir = cwd
candidate = os.path.join(path_dir, executable)
if os.path.exists(candidate) and os.access(candidate, os.F_OK | os.X_OK):
match = candidate
break
return match
def find_globals(g, tree):
"""Uses AST to find globals in an ast tree"""
for child in tree:
if hasattr(child, 'body') and isinstance(child.body, list):
find_globals(g, child.body)
elif isinstance(child, (ast.FunctionDef, ast.ClassDef)):
g.add(child.name)
continue
elif isinstance(child, ast.Assign):
try:
g.add(child.targets[0].id)
except (IndexError, AttributeError):
pass
elif isinstance(child, ast.Import):
g.add(child.names[0].name)
elif isinstance(child, ast.ImportFrom):
for name in child.names:
g_name = name.asname or name.name
if g_name == '*':
continue
g.add(g_name)
class CaptureStd():
"""Context manager to handle capturing stderr and stdout"""
def __enter__(self):
self.sys_stdout = sys.stdout
self.sys_stderr = sys.stderr
sys.stdout = self.stdout = AnsibleTextIOWrapper(BytesIO(), encoding=self.sys_stdout.encoding)
sys.stderr = self.stderr = AnsibleTextIOWrapper(BytesIO(), encoding=self.sys_stderr.encoding)
return self
def __exit__(self, exc_type, exc_value, traceback):
sys.stdout = self.sys_stdout
sys.stderr = self.sys_stderr
def get(self):
"""Return ``(stdout, stderr)``"""
return self.stdout.buffer.getvalue(), self.stderr.buffer.getvalue()
def get_module_name_from_filename(filename, collection):
# Calculate the module's name so that relative imports work correctly
if collection:
# collection is a relative path, example: ansible_collections/my_namespace/my_collection
# filename is a relative path, example: plugins/modules/my_module.py
path = os.path.join(collection, filename)
else:
# filename is a relative path, example: lib/ansible/modules/system/ping.py
path = os.path.relpath(filename, 'lib')
name = os.path.splitext(path)[0].replace(os.path.sep, '.')
return name
def parse_yaml(value, lineno, module, name, load_all=False, ansible_loader=False):
traces = []
errors = []
data = None
if load_all:
yaml_load = yaml.load_all
else:
yaml_load = yaml.load
if ansible_loader:
loader = AnsibleLoader
else:
loader = SafeLoader
try:
data = yaml_load(value, Loader=loader)
if load_all:
data = list(data)
except yaml.MarkedYAMLError as e:
e.problem_mark.line += lineno - 1
e.problem_mark.name = '%s.%s' % (module, name)
errors.append({
'msg': '%s is not valid YAML' % name,
'line': e.problem_mark.line + 1,
'column': e.problem_mark.column + 1
})
traces.append(e)
except yaml.reader.ReaderError as e:
traces.append(e)
# TODO: Better line/column detection
errors.append({
'msg': ('%s is not valid YAML. Character '
'0x%x at position %d.' % (name, e.character, e.position)),
'line': lineno
})
except yaml.YAMLError as e:
traces.append(e)
errors.append({
'msg': '%s is not valid YAML: %s: %s' % (name, type(e), e),
'line': lineno
})
return data, errors, traces
def is_empty(value):
"""Evaluate null like values excluding False"""
if value is False:
return False
return not bool(value)
def compare_unordered_lists(a, b):
"""Safe list comparisons
Supports:
- unordered lists
- unhashable elements
"""
return len(a) == len(b) and all(x in b for x in a)
class NoArgsAnsibleModule(AnsibleModule):
"""AnsibleModule that does not actually load params. This is used to get access to the
methods within AnsibleModule without having to fake a bunch of data
"""
def _load_params(self):
self.params = {'_ansible_selinux_special_fs': [], '_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False, '_ansible_check_mode': False}
def parse_isodate(v, allow_date):
if allow_date:
if isinstance(v, datetime.date):
return v
msg = 'Expected ISO 8601 date string (YYYY-MM-DD) or YAML date'
else:
msg = 'Expected ISO 8601 date string (YYYY-MM-DD)'
if not isinstance(v, string_types):
raise ValueError(msg)
# From Python 3.7 in, there is datetime.date.fromisoformat(). For older versions,
# we have to do things manually.
if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', v):
raise ValueError(msg)
try:
return datetime.datetime.strptime(v, '%Y-%m-%d').date()
except ValueError:
raise ValueError(msg)

Some files were not shown because too many files have changed in this diff Show More