Deprecation revisited (#69926)

* Allow to specify collection_name separately for deprecation.

* Use new functionality in Ansible.

* Use new functionality in tests.

* Update tagging/untagging functions.

* Update pylint deprecated sanity test.

* Update validate-modules. Missing are basic checks for version_added (validate semantic version format for collections).

* Improve version validation. Re-add version_added validation.

* Make sure collection names are added to return docs before schema validation.

* Extra checks to avoid crashes on bad data.

* Make C# module utils code work, and update/extend tests.

* Add changelog fragment.

* Stop extracting collection name from potentially tagged versions/dates.

* Simplify C# code.

* Update Windows modules docs.

* Forgot semicolons.
pull/69976/head
Felix Fontein 5 years ago committed by GitHub
parent 7bff3d312f
commit a862ff2d43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
major_changes:
- "When deprecations are done in code, they to specify a ``collection_name`` so that deprecation warnings can mention which collection - or ansible-base - is deprecating a feature. This affects all ``Display.deprecated()`` or ``AnsibleModule.deprecate()`` or ``Ansible.Basic.Deprecate()`` calls, and ``removed_in_version``/``removed_at_date`` or ``deprecated_aliases`` in module argument specs."

@ -209,11 +209,12 @@ options set:
- ``aliases``: A list of aliases for the module option - ``aliases``: A list of aliases for the module option
- ``choices``: A list of valid values for the module option, if ``type=list`` then each list value is validated against the choices and not the list itself - ``choices``: A list of valid values for the module option, if ``type=list`` then each list value is validated against the choices and not the list itself
- ``default``: The default value for the module option if not set - ``default``: The default value for the module option if not set
- ``deprecated_aliases``: A list of hashtables that define aliases that are deprecated and the versions they will be removed in. Each entry must contain the key ``name`` with either ``version`` or ``date`` - ``deprecated_aliases``: A list of hashtables that define aliases that are deprecated and the versions they will be removed in. Each entry must contain the keys ``name`` and ``collection_name`` with either ``version`` or ``date``
- ``elements``: When ``type=list``, this sets the type of each list value, the values are the same as ``type`` - ``elements``: When ``type=list``, this sets the type of each list value, the values are the same as ``type``
- ``no_log``: Will sanitise the input value before being returned in the ``module_invocation`` return value - ``no_log``: Will sanitise the input value before being returned in the ``module_invocation`` return value
- ``removed_in_version``: States when a deprecated module option is to be removed, a warning is displayed to the end user if set - ``removed_in_version``: States when a deprecated module option is to be removed, a warning is displayed to the end user if set
- ``removed_at_date``: States the date when a deprecated module option will be removed, a warning is displayed to the end user if set - ``removed_at_date``: States the date when a deprecated module option will be removed, a warning is displayed to the end user if set
- ``removed_from_collection``: States from which collection the deprecated module option will be removed; must be specified if one of ``removed_in_version`` and ``removed_at_date`` is specified
- ``required``: Will fail when the module option is not set - ``required``: Will fail when the module option is not set
- ``type``: The type of the module option, if not set then it defaults to ``str``. The valid types are; - ``type``: The type of the module option, if not set then it defaults to ``str``. The valid types are;
* ``bool``: A boolean value * ``bool``: A boolean value

@ -104,7 +104,9 @@ class CLI(with_metaclass(ABCMeta, object)):
alt = '' alt = ''
ver = deprecated[1].get('version') ver = deprecated[1].get('version')
date = deprecated[1].get('date') date = deprecated[1].get('date')
display.deprecated("%s option, %s %s" % (name, why, alt), version=ver, date=date) collection_name = deprecated[1].get('collection_name')
display.deprecated("%s option, %s %s" % (name, why, alt),
version=ver, date=date, collection_name=collection_name)
@staticmethod @staticmethod
def split_vault_id(vault_id): def split_vault_id(vault_id):
@ -353,7 +355,7 @@ class CLI(with_metaclass(ABCMeta, object)):
verbosity_arg = next(iter([arg for arg in self.args if arg.startswith('-v')]), None) verbosity_arg = next(iter([arg for arg in self.args if arg.startswith('-v')]), None)
if verbosity_arg: if verbosity_arg:
display.deprecated("Setting verbosity before the arg sub command is deprecated, set the verbosity " display.deprecated("Setting verbosity before the arg sub command is deprecated, set the verbosity "
"after the sub command", "ansible.builtin:2.13") "after the sub command", "2.13", collection_name='ansible.builtin')
options.verbosity = verbosity_arg.count('v') options.verbosity = verbosity_arg.count('v')
return options return options

@ -30,7 +30,12 @@ from ansible.plugins.loader import action_loader, fragment_loader
from ansible.utils.collection_loader import AnsibleCollectionConfig from ansible.utils.collection_loader import AnsibleCollectionConfig
from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
from ansible.utils.display import Display from ansible.utils.display import Display
from ansible.utils.plugin_docs import BLACKLIST, untag_versions_and_dates, get_docstring, get_versioned_doclink from ansible.utils.plugin_docs import (
BLACKLIST,
remove_current_collection_from_versions_and_dates,
get_docstring,
get_versioned_doclink,
)
display = Display() display = Display()
@ -330,11 +335,18 @@ class DocCLI(CLI):
raise ValueError('%s did not contain a DOCUMENTATION attribute' % plugin) raise ValueError('%s did not contain a DOCUMENTATION attribute' % plugin)
doc['filename'] = filename doc['filename'] = filename
untag_versions_and_dates(doc, '%s:' % (collection_name, ), is_module=(plugin_type == 'module'))
return doc, plainexamples, returndocs, metadata return doc, plainexamples, returndocs, metadata
@staticmethod @staticmethod
def format_plugin_doc(plugin, plugin_type, doc, plainexamples, returndocs, metadata): def format_plugin_doc(plugin, plugin_type, doc, plainexamples, returndocs, metadata):
collection_name = 'ansible.builtin'
if plugin.startswith('ansible_collections.'):
collection_name = '.'.join(plugin.split('.')[1:3])
# TODO: do we really want this?
# add_collection_to_versions_and_dates(doc, '(unknown)', is_module=(plugin_type == 'module'))
# remove_current_collection_from_versions_and_dates(doc, collection_name, is_module=(plugin_type == 'module'))
# assign from other sections # assign from other sections
doc['plainexamples'] = plainexamples doc['plainexamples'] = plainexamples
doc['returndocs'] = returndocs doc['returndocs'] = returndocs

@ -487,7 +487,7 @@ class TaskExecutor:
'Invoking "%s" only once while using a loop via squash_actions is deprecated. ' 'Invoking "%s" only once while using a loop via squash_actions is deprecated. '
'Instead of using a loop to supply multiple items and specifying `%s: "%s"`, ' 'Instead of using a loop to supply multiple items and specifying `%s: "%s"`, '
'please use `%s: %s` and remove the loop' % (self._task.action, found, name, found, value_text), 'please use `%s: %s` and remove the loop' % (self._task.action, found, name, found, value_text),
version='ansible.builtin:2.11' version='2.11', collection_name='ansible.builtin'
) )
for item in items: for item in items:
variables[loop_var] = item variables[loop_var] = item

@ -725,10 +725,10 @@ class AnsibleModule(object):
warn(warning) warn(warning)
self.log('[WARNING] %s' % warning) self.log('[WARNING] %s' % warning)
def deprecate(self, msg, version=None, date=None): def deprecate(self, msg, version=None, date=None, collection_name=None):
if version is not None and date is not None: if version is not None and date is not None:
raise AssertionError("implementation error -- version and date must not both be set") raise AssertionError("implementation error -- version and date must not both be set")
deprecate(msg, version=version, date=date) deprecate(msg, version=version, date=date, collection_name=collection_name)
# For compatibility, we accept that neither version nor date is set, # For compatibility, we accept that neither version nor date is set,
# and treat that the same as if version would haven been set # and treat that the same as if version would haven been set
if date is not None: if date is not None:
@ -1414,7 +1414,8 @@ class AnsibleModule(object):
for deprecation in deprecated_aliases: for deprecation in deprecated_aliases:
if deprecation['name'] in param.keys(): if deprecation['name'] in param.keys():
deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'], deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'],
version=deprecation.get('version'), date=deprecation.get('date')) version=deprecation.get('version'), date=deprecation.get('date'),
collection_name=deprecation.get('collection_name'))
return alias_results return alias_results
def _handle_no_log_values(self, spec=None, param=None): def _handle_no_log_values(self, spec=None, param=None):
@ -1430,7 +1431,8 @@ class AnsibleModule(object):
"%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'})
for message in list_deprecations(spec, param): for message in list_deprecations(spec, param):
deprecate(message['msg'], version=message.get('version'), date=message.get('date')) deprecate(message['msg'], version=message.get('version'), date=message.get('date'),
collection_name=message.get('collection_name'))
def _check_arguments(self, spec=None, param=None, legal_inputs=None): def _check_arguments(self, spec=None, param=None, legal_inputs=None):
self._syslog_facility = 'LOG_USER' self._syslog_facility = 'LOG_USER'
@ -2060,7 +2062,8 @@ class AnsibleModule(object):
if isinstance(d, SEQUENCETYPE) and len(d) == 2: if isinstance(d, SEQUENCETYPE) and len(d) == 2:
self.deprecate(d[0], version=d[1]) self.deprecate(d[0], version=d[1])
elif isinstance(d, Mapping): elif isinstance(d, Mapping):
self.deprecate(d['msg'], version=d.get('version', None), date=d.get('date', None)) self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'),
collection_name=d.get('date'))
else: else:
self.deprecate(d) # pylint: disable=ansible-deprecated-no-version self.deprecate(d) # pylint: disable=ansible-deprecated-no-version
else: else:

@ -140,12 +140,14 @@ def list_deprecations(argument_spec, params, prefix=''):
if arg_opts.get('removed_at_date') is not None: if arg_opts.get('removed_at_date') is not None:
deprecations.append({ deprecations.append({
'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix, 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
'date': arg_opts.get('removed_at_date') 'date': arg_opts.get('removed_at_date'),
'collection_name': arg_opts.get('removed_from_collection'),
}) })
elif arg_opts.get('removed_in_version') is not None: elif arg_opts.get('removed_in_version') is not None:
deprecations.append({ deprecations.append({
'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix, 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
'version': arg_opts.get('removed_in_version') 'version': arg_opts.get('removed_in_version'),
'collection_name': arg_opts.get('removed_from_collection'),
}) })
# Check sub-argument spec # Check sub-argument spec
sub_argument_spec = arg_opts.get('options') sub_argument_spec = arg_opts.get('options')

@ -18,14 +18,14 @@ def warn(warning):
raise TypeError("warn requires a string not a %s" % type(warning)) raise TypeError("warn requires a string not a %s" % type(warning))
def deprecate(msg, version=None, date=None): def deprecate(msg, version=None, date=None, collection_name=None):
if isinstance(msg, string_types): if isinstance(msg, string_types):
# For compatibility, we accept that neither version nor date is set, # For compatibility, we accept that neither version nor date is set,
# and treat that the same as if version would haven been set # and treat that the same as if version would haven been set
if date is not None: if date is not None:
_global_deprecations.append({'msg': msg, 'date': date}) _global_deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name})
else: else:
_global_deprecations.append({'msg': msg, 'version': version}) _global_deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name})
else: else:
raise TypeError("deprecate requires a string not a %s" % type(msg)) raise TypeError("deprecate requires a string not a %s" % type(msg))

@ -86,6 +86,7 @@ namespace Ansible.Basic
{ "options", new List<object>() { typeof(Hashtable), typeof(Hashtable) } }, { "options", new List<object>() { typeof(Hashtable), typeof(Hashtable) } },
{ "removed_in_version", new List<object>() { null, typeof(string) } }, { "removed_in_version", new List<object>() { null, typeof(string) } },
{ "removed_at_date", new List<object>() { null, typeof(DateTime) } }, { "removed_at_date", new List<object>() { null, typeof(DateTime) } },
{ "removed_from_collection", new List<object>() { null, typeof(string) } },
{ "required", new List<object>() { false, typeof(bool) } }, { "required", new List<object>() { false, typeof(bool) } },
{ "required_by", new List<object>() { typeof(Hashtable), typeof(Hashtable) } }, { "required_by", new List<object>() { typeof(Hashtable), typeof(Hashtable) } },
{ "required_if", new List<object>() { typeof(List<List<object>>), typeof(List<object>) } }, { "required_if", new List<object>() { typeof(List<List<object>>), typeof(List<object>) } },
@ -275,14 +276,26 @@ namespace Ansible.Basic
public void Deprecate(string message, string version) public void Deprecate(string message, string version)
{ {
deprecations.Add(new Dictionary<string, string>() { { "msg", message }, { "version", version } }); Deprecate(message, version, null);
}
public void Deprecate(string message, string version, string collectionName)
{
deprecations.Add(new Dictionary<string, string>() {
{ "msg", message }, { "version", version }, { "collection_name", collectionName } });
LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version)); LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version));
} }
public void Deprecate(string message, DateTime date) public void Deprecate(string message, DateTime date)
{
Deprecate(message, date, null);
}
public void Deprecate(string message, DateTime date, string collectionName)
{ {
string isoDate = date.ToString("yyyy-MM-dd"); string isoDate = date.ToString("yyyy-MM-dd");
deprecations.Add(new Dictionary<string, string>() { { "msg", message }, { "date", isoDate } }); deprecations.Add(new Dictionary<string, string>() {
{ "msg", message }, { "date", isoDate }, { "collection_name", collectionName } });
LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, isoDate)); LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, isoDate));
} }
@ -800,6 +813,11 @@ namespace Ansible.Basic
string msg = "A deprecated_aliases date must be a DateTime object"; string msg = "A deprecated_aliases date must be a DateTime object";
throw new ArgumentException(FormatOptionsContext(msg, " - ")); throw new ArgumentException(FormatOptionsContext(msg, " - "));
} }
string collectionName = null;
if (depInfo.ContainsKey("collection_name"))
{
collectionName = (string)depInfo["collection_name"];
}
string aliasName = (string)depInfo["name"]; string aliasName = (string)depInfo["name"];
if (parameters.Contains(aliasName)) if (parameters.Contains(aliasName))
@ -808,12 +826,12 @@ namespace Ansible.Basic
if (depInfo.ContainsKey("version")) if (depInfo.ContainsKey("version"))
{ {
string depVersion = (string)depInfo["version"]; string depVersion = (string)depInfo["version"];
Deprecate(FormatOptionsContext(msg, " - "), depVersion); Deprecate(FormatOptionsContext(msg, " - "), depVersion, collectionName);
} }
if (depInfo.ContainsKey("date")) if (depInfo.ContainsKey("date"))
{ {
DateTime depDate = (DateTime)depInfo["date"]; DateTime depDate = (DateTime)depInfo["date"];
Deprecate(FormatOptionsContext(msg, " - "), depDate); Deprecate(FormatOptionsContext(msg, " - "), depDate, collectionName);
} }
} }
} }
@ -836,14 +854,21 @@ namespace Ansible.Basic
if (!String.IsNullOrEmpty(noLogString)) if (!String.IsNullOrEmpty(noLogString))
noLogValues.Add(noLogString); noLogValues.Add(noLogString);
} }
string collectionName = null;
if (v.ContainsKey("removed_from_collection"))
{
collectionName = (string)v["removed_from_collection"];
}
object removedInVersion = v["removed_in_version"]; object removedInVersion = v["removed_in_version"];
if (removedInVersion != null && parameters.Contains(k)) if (removedInVersion != null && parameters.Contains(k))
Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), removedInVersion.ToString()); Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k),
removedInVersion.ToString(), collectionName);
object removedAtDate = v["removed_at_date"]; object removedAtDate = v["removed_at_date"];
if (removedAtDate != null && parameters.Contains(k)) if (removedAtDate != null && parameters.Contains(k))
Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), (DateTime)removedAtDate); Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k),
(DateTime)removedAtDate, collectionName);
} }
} }

@ -1529,7 +1529,7 @@ def url_argument_spec():
return dict( return dict(
url=dict(type='str'), url=dict(type='str'),
force=dict(type='bool', default=False, aliases=['thirsty'], force=dict(type='bool', default=False, aliases=['thirsty'],
deprecated_aliases=[dict(name='thirsty', version='ansible.builtin:2.13')]), deprecated_aliases=[dict(name='thirsty', version='2.13', collection='ansible.builtin')]),
http_agent=dict(type='str', default='ansible-httpget'), http_agent=dict(type='str', default='ansible-httpget'),
use_proxy=dict(type='bool', default=True), use_proxy=dict(type='bool', default=True),
validate_certs=dict(type='bool', default=True), validate_certs=dict(type='bool', default=True),

@ -514,7 +514,8 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead',
version='2.13', collection_name='ansible.builtin')
src = module.params['src'] src = module.params['src']
b_src = to_bytes(src, errors='surrogate_or_strict') b_src = to_bytes(src, errors='surrogate_or_strict')

@ -618,12 +618,12 @@ def main():
if not name: if not name:
module.deprecate( module.deprecate(
msg="The 'name' parameter will be required in future releases.", msg="The 'name' parameter will be required in future releases.",
version='ansible.builtin:2.12' version='2.12', collection_name='ansible.builtin'
) )
if reboot: if reboot:
module.deprecate( module.deprecate(
msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.", msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.",
version='ansible.builtin:2.12' version='2.12', collection_name='ansible.builtin'
) )
if module._diff: if module._diff:

@ -446,10 +446,12 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead',
version='2.13', collection_name='ansible.builtin')
if module.params.get('sha256sum'): if module.params.get('sha256sum'):
module.deprecate('The parameter "sha256sum" has been deprecated and will be removed, use "checksum" instead', version='ansible.builtin:2.14') module.deprecate('The parameter "sha256sum" has been deprecated and will be removed, use "checksum" instead',
version='2.14', collection_name='ansible.builtin')
url = module.params['url'] url = module.params['url']
dest = module.params['dest'] dest = module.params['dest']

@ -357,7 +357,7 @@ def main():
''' Set CLI options depending on params ''' ''' Set CLI options depending on params '''
if module.params['user'] is not None: if module.params['user'] is not None:
# handle user deprecation, mutually exclusive with scope # handle user deprecation, mutually exclusive with scope
module.deprecate("The 'user' option is being replaced by 'scope'", version='ansible.builtin:2.11') module.deprecate("The 'user' option is being replaced by 'scope'", version='2.11', collection_name='ansible.builtin')
if module.params['user']: if module.params['user']:
module.params['scope'] = 'user' module.params['scope'] = 'user'
else: else:

@ -611,7 +611,8 @@ def main():
) )
if module.params.get('thirsty'): if module.params.get('thirsty'):
module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead', version='ansible.builtin:2.13') module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead',
version='2.13', collection_name='ansible.builtin')
url = module.params['url'] url = module.params['url']
body = module.params['body'] body = module.params['body']

@ -79,7 +79,8 @@ class Playbook:
self._loader.set_basedir(cur_basedir) self._loader.set_basedir(cur_basedir)
raise AnsibleParserError("A playbook must be a list of plays, got a %s instead" % type(ds), obj=ds) raise AnsibleParserError("A playbook must be a list of plays, got a %s instead" % type(ds), obj=ds)
elif not ds: elif not ds:
display.deprecated("Empty plays will currently be skipped, in the future they will cause a syntax error", version='ansible.builtin:2.12') display.deprecated("Empty plays will currently be skipped, in the future they will cause a syntax error",
version='2.12', collection_name='ansible.builtin')
# Parse the playbook entries. For plays, we simply parse them # Parse the playbook entries. For plays, we simply parse them
# using the Play() object, and includes are parsed using the # using the Play() object, and includes are parsed using the
@ -92,7 +93,8 @@ class Playbook:
if any(action in entry for action in ('import_playbook', 'include')): if any(action in entry for action in ('import_playbook', 'include')):
if 'include' in entry: if 'include' in entry:
display.deprecated("'include' for playbook includes. You should use 'import_playbook' instead", version="ansible.builtin:2.12") display.deprecated("'include' for playbook includes. You should use 'import_playbook' instead",
version="2.12", collection_name='ansible.builtin')
pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader) pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
if pb is not None: if pb is not None:
self._entries.extend(pb._entries) self._entries.extend(pb._entries)

@ -134,7 +134,8 @@ class Conditional:
conditional = templar.template(conditional, disable_lookups=disable_lookups) conditional = templar.template(conditional, disable_lookups=disable_lookups)
if bare_vars_warning and not isinstance(conditional, bool): if bare_vars_warning and not isinstance(conditional, bool):
display.deprecated('evaluating %r as a bare variable, this behaviour will go away and you might need to add |bool' display.deprecated('evaluating %r as a bare variable, this behaviour will go away and you might need to add |bool'
' to the expression in the future. Also see CONDITIONAL_BARE_VARS configuration toggle' % original, "ansible.builtin:2.12") ' to the expression in the future. Also see CONDITIONAL_BARE_VARS configuration toggle' % original,
version="2.12", collection_name='ansible.builtin')
if not isinstance(conditional, text_type) or conditional == "": if not isinstance(conditional, text_type) or conditional == "":
return conditional return conditional

@ -156,7 +156,8 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
is_static = True is_static = True
elif t.static is not None: elif t.static is not None:
display.deprecated("The use of 'static' has been deprecated. " display.deprecated("The use of 'static' has been deprecated. "
"Use 'import_tasks' for static inclusion, or 'include_tasks' for dynamic inclusion", version='ansible.builtin:2.12') "Use 'import_tasks' for static inclusion, or 'include_tasks' for dynamic inclusion",
version='2.12', collection_name='ansible.builtin')
is_static = t.static is_static = t.static
else: else:
is_static = C.DEFAULT_TASK_INCLUDES_STATIC or \ is_static = C.DEFAULT_TASK_INCLUDES_STATIC or \
@ -257,7 +258,8 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
"later. In the future, this will be an error unless 'static: no' is used " "later. In the future, this will be an error unless 'static: no' is used "
"on the include task. If you do not want missing includes to be considered " "on the include task. If you do not want missing includes to be considered "
"dynamic, use 'static: yes' on the include or set the global ansible.cfg " "dynamic, use 'static: yes' on the include or set the global ansible.cfg "
"options to make all includes static for tasks and/or handlers" % include_file, version="ansible.builtin:2.12" "options to make all includes static for tasks and/or handlers" % include_file,
version="2.12", collection_name='ansible.builtin'
) )
task_list.append(t) task_list.append(t)
continue continue
@ -294,7 +296,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
suppress_extended_error=True, suppress_extended_error=True,
) )
display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option", display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option",
version="ansible.builtin:2.12") version="2.12", collection_name='ansible.builtin')
else: else:
tags = ti_copy.tags[:] tags = ti_copy.tags[:]
@ -332,7 +334,8 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
elif ir.static is not None: elif ir.static is not None:
display.deprecated("The use of 'static' for 'include_role' has been deprecated. " display.deprecated("The use of 'static' for 'include_role' has been deprecated. "
"Use 'import_role' for static inclusion, or 'include_role' for dynamic inclusion", version='ansible.builtin:2.12') "Use 'import_role' for static inclusion, or 'include_role' for dynamic inclusion",
version='2.12', collection_name='ansible.builtin')
is_static = ir.static is_static = ir.static
if is_static: if is_static:

@ -342,7 +342,7 @@ class PlayContext(Base):
""" helper function to create privilege escalation commands """ """ helper function to create privilege escalation commands """
display.deprecated( display.deprecated(
"PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead", "PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
if not cmd or not self.become: if not cmd or not self.become:

@ -163,7 +163,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
raise AnsibleError("you must specify a value when using %s" % k, obj=ds) raise AnsibleError("you must specify a value when using %s" % k, obj=ds)
new_ds['loop_with'] = loop_name new_ds['loop_with'] = loop_name
new_ds['loop'] = v new_ds['loop'] = v
# display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead", version="ansible.builtin:2.10") # display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead",
# version="2.10", collection_name='ansible.builtin')
def preprocess_data(self, ds): def preprocess_data(self, ds):
''' '''
@ -258,7 +259,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if action in ('include',) and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES: if action in ('include',) and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES:
display.deprecated("Specifying include variables at the top-level of the task is deprecated." display.deprecated("Specifying include variables at the top-level of the task is deprecated."
" Please see:\nhttps://docs.ansible.com/ansible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n" " Please see:\nhttps://docs.ansible.com/ansible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n"
" for currently supported syntax regarding included files and variables", version="ansible.builtin:2.12") " for currently supported syntax regarding included files and variables",
version="2.12", collection_name='ansible.builtin')
new_ds['vars'][k] = v new_ds['vars'][k] = v
elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs: elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs:
new_ds[k] = v new_ds[k] = v

@ -831,7 +831,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
msg = "Setting the async dir from the environment keyword " \ msg = "Setting the async dir from the environment keyword " \
"ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \
"shell option instead" "shell option instead"
self._display.deprecated(msg, "ansible.builtin:2.12") self._display.deprecated(msg, "2.12", collection_name='ansible.builtin')
else: else:
# ANSIBLE_ASYNC_DIR is not set on the task, we get the value # ANSIBLE_ASYNC_DIR is not set on the task, we get the value
# from the shell option and temporarily add to the environment # from the shell option and temporarily add to the environment

@ -33,7 +33,7 @@ class ActionModule(ActionBase):
msg = "Setting the async dir from the environment keyword " \ msg = "Setting the async dir from the environment keyword " \
"ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \ "ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \
"shell option instead" "shell option instead"
self._display.deprecated(msg, "ansible.builtin:2.12") self._display.deprecated(msg, "2.12", collection_name='ansible.builtin')
else: else:
# inject the async directory based on the shell option into the # inject the async directory based on the shell option into the
# module args # module args

@ -50,7 +50,7 @@ class FactCache(RealFactCache):
' ansible.vars.fact_cache.FactCache. If you are looking for the class' ' ansible.vars.fact_cache.FactCache. If you are looking for the class'
' to subclass for a cache plugin, you want' ' to subclass for a cache plugin, you want'
' ansible.plugins.cache.BaseCacheModule or one of its subclasses.', ' ansible.plugins.cache.BaseCacheModule or one of its subclasses.',
version='ansible.builtin:2.12') version='2.12', collection_name='ansible.builtin')
super(FactCache, self).__init__(*args, **kwargs) super(FactCache, self).__init__(*args, **kwargs)
@ -62,7 +62,8 @@ class BaseCacheModule(AnsiblePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Third party code is not using cache_loader to load plugin - fall back to previous behavior # Third party code is not using cache_loader to load plugin - fall back to previous behavior
if not hasattr(self, '_load_name'): if not hasattr(self, '_load_name'):
display.deprecated('Rather than importing custom CacheModules directly, use ansible.plugins.loader.cache_loader', version='ansible.builtin:2.14') display.deprecated('Rather than importing custom CacheModules directly, use ansible.plugins.loader.cache_loader',
version='2.14', collection_name='ansible.builtin')
self._load_name = self.__module__.split('.')[-1] self._load_name = self.__module__.split('.')[-1]
self._load_name = resource_from_fqcr(self.__module__) self._load_name = resource_from_fqcr(self.__module__)
super(BaseCacheModule, self).__init__() super(BaseCacheModule, self).__init__()

@ -242,7 +242,8 @@ class CallbackBase(AnsiblePlugin):
def _get_item(self, result): def _get_item(self, result):
''' here for backwards compat, really should have always been named: _get_item_label''' ''' here for backwards compat, really should have always been named: _get_item_label'''
cback = getattr(self, 'NAME', os.path.basename(__file__)) cback = getattr(self, 'NAME', os.path.basename(__file__))
self._display.deprecated("The %s callback plugin should be updated to use the _get_item_label method instead" % cback, version="ansible.builtin:2.11") self._display.deprecated("The %s callback plugin should be updated to use the _get_item_label method instead" % cback,
version="2.11", collection_name='ansible.builtin')
return self._get_item_label(result) return self._get_item_label(result)
def _process_items(self, result): def _process_items(self, result):

@ -237,28 +237,28 @@ class ConnectionBase(AnsiblePlugin):
def check_become_success(self, b_output): def check_become_success(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_become_success is deprecated, calling code should be using become plugins instead", "Connection.check_become_success is deprecated, calling code should be using become plugins instead",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
return self.become.check_success(b_output) return self.become.check_success(b_output)
def check_password_prompt(self, b_output): def check_password_prompt(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_password_prompt is deprecated, calling code should be using become plugins instead", "Connection.check_password_prompt is deprecated, calling code should be using become plugins instead",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
return self.become.check_password_prompt(b_output) return self.become.check_password_prompt(b_output)
def check_incorrect_password(self, b_output): def check_incorrect_password(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_incorrect_password is deprecated, calling code should be using become plugins instead", "Connection.check_incorrect_password is deprecated, calling code should be using become plugins instead",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
return self.become.check_incorrect_password(b_output) return self.become.check_incorrect_password(b_output)
def check_missing_password(self, b_output): def check_missing_password(self, b_output):
display.deprecated( display.deprecated(
"Connection.check_missing_password is deprecated, calling code should be using become plugins instead", "Connection.check_missing_password is deprecated, calling code should be using become plugins instead",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
return self.become.check_missing_password(b_output) return self.become.check_missing_password(b_output)

@ -291,19 +291,21 @@ class DeprecatedCache(object):
display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. ' display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. '
'When expecting a KeyError, use self._cache[key] instead of using self.cache.get(key). ' 'When expecting a KeyError, use self._cache[key] instead of using self.cache.get(key). '
'self._cache is a dictionary and will return a default value instead of raising a KeyError ' 'self._cache is a dictionary and will return a default value instead of raising a KeyError '
'when the key does not exist', version='ansible.builtin:2.12') 'when the key does not exist', version='2.12', collection_name='ansible.builtin')
return self.real_cacheable._cache[key] return self.real_cacheable._cache[key]
def set(self, key, value): def set(self, key, value):
display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. ' display.deprecated('InventoryModule should utilize self._cache as a dict instead of self.cache. '
'To set the self._cache dictionary, use self._cache[key] = value instead of self.cache.set(key, value). ' 'To set the self._cache dictionary, use self._cache[key] = value instead of self.cache.set(key, value). '
'To force update the underlying cache plugin with the contents of self._cache before parse() is complete, ' 'To force update the underlying cache plugin with the contents of self._cache before parse() is complete, '
'call self.set_cache_plugin and it will use the self._cache dictionary to update the cache plugin', version='ansible.builtin:2.12') 'call self.set_cache_plugin and it will use the self._cache dictionary to update the cache plugin',
version='2.12', collection_name='ansible.builtin')
self.real_cacheable._cache[key] = value self.real_cacheable._cache[key] = value
self.real_cacheable.set_cache_plugin() self.real_cacheable.set_cache_plugin()
def __getattr__(self, name): def __getattr__(self, name):
display.deprecated('InventoryModule should utilize self._cache instead of self.cache', version='ansible.builtin:2.12') display.deprecated('InventoryModule should utilize self._cache instead of self.cache',
version='2.12', collection_name='ansible.builtin')
return self.real_cacheable._cache.__getattribute__(name) return self.real_cacheable._cache.__getattribute__(name)

@ -97,7 +97,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
display.deprecated( display.deprecated(
msg="The 'cache' option is deprecated for the script inventory plugin. " msg="The 'cache' option is deprecated for the script inventory plugin. "
"External scripts implement their own caching and this option has never been used", "External scripts implement their own caching and this option has never been used",
version="ansible.builtin:2.12" version="2.12", collection_name='ansible.builtin'
) )
# Support inventory scripts that are not prefixed with some # Support inventory scripts that are not prefixed with some

@ -71,7 +71,7 @@ def SharedPluginLoaderObj():
'''This only exists for backwards compat, do not use. '''This only exists for backwards compat, do not use.
''' '''
display.deprecated('SharedPluginLoaderObj is deprecated, please directly use ansible.plugins.loader', display.deprecated('SharedPluginLoaderObj is deprecated, please directly use ansible.plugins.loader',
version='ansible.builtin:2.11') version='2.11', collection_name='ansible.builtin')
return plugin_loader return plugin_loader
@ -918,7 +918,7 @@ class StrategyBase:
"Mixing tag specify styles is prohibited for whole import hierarchy, not only for single import statement", "Mixing tag specify styles is prohibited for whole import hierarchy, not only for single import statement",
obj=included_file._task._ds) obj=included_file._task._ds)
display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option", display.deprecated("You should not specify tags in the include parameters. All tags should be specified using the task-level option",
version='ansible.builtin:2.12') version='2.12', collection_name='ansible.builtin')
included_file._task.tags = tags included_file._task.tags = tags
block_list = load_list_of_blocks( block_list = load_list_of_blocks(

@ -661,7 +661,7 @@ class Templar:
def set_available_variables(self, variables): def set_available_variables(self, variables):
display.deprecated( display.deprecated(
'set_available_variables is being deprecated. Use "@available_variables.setter" instead.', 'set_available_variables is being deprecated. Use "@available_variables.setter" instead.',
version='ansible.builtin:2.13' version='2.13', collection_name='ansible.builtin'
) )
self.available_variables = variables self.available_variables = variables

@ -255,19 +255,6 @@ class Display(with_metaclass(Singleton, object)):
if msg and msg[-1] not in ['!', '?', '.']: if msg and msg[-1] not in ['!', '?', '.']:
msg += '.' msg += '.'
# split composite collection info (if any) off date/version and use it if not otherwise spec'd
if date and isinstance(date, string_types):
parts = to_native(date.strip()).split(':', 1)
date = parts[-1]
if len(parts) == 2 and not collection_name:
collection_name = parts[0]
if version and isinstance(version, string_types):
parts = to_native(version.strip()).split(':', 1)
version = parts[-1]
if len(parts) == 2 and not collection_name:
collection_name = parts[0]
if collection_name == 'ansible.builtin': if collection_name == 'ansible.builtin':
collection_name = 'ansible-base' collection_name = 'ansible-base'

@ -40,26 +40,32 @@ def merge_fragment(target, source):
target[key] = value target[key] = value
def _process_versions_and_dates(fragment, is_module, callback): def _process_versions_and_dates(fragment, is_module, return_docs, callback):
def process_deprecation(deprecation): def process_deprecation(deprecation):
if not isinstance(deprecation, MutableMapping):
return
if is_module and 'removed_in' in deprecation: # used in module deprecations if is_module and 'removed_in' in deprecation: # used in module deprecations
callback(deprecation, 'removed_in') callback(deprecation, 'removed_in', 'removed_from_collection')
if 'removed_at_date' in deprecation: if 'removed_at_date' in deprecation:
callback(deprecation, 'removed_at_date') callback(deprecation, 'removed_at_date', 'removed_from_collection')
if not is_module and 'version' in deprecation: # used in plugin option deprecations if not is_module and 'version' in deprecation: # used in plugin option deprecations
callback(deprecation, 'version') callback(deprecation, 'version', 'removed_from_collection')
def process_option_specifiers(specifiers): def process_option_specifiers(specifiers):
for specifier in specifiers: for specifier in specifiers:
if not isinstance(specifier, MutableMapping):
continue
if 'version_added' in specifier: if 'version_added' in specifier:
callback(specifier, 'version_added') callback(specifier, 'version_added', 'version_added_collection')
if isinstance(specifier.get('deprecated'), dict): if isinstance(specifier.get('deprecated'), MutableMapping):
process_deprecation(specifier['deprecated']) process_deprecation(specifier['deprecated'])
def process_options(options): def process_options(options):
for option in options.values(): for option in options.values():
if not isinstance(option, MutableMapping):
continue
if 'version_added' in option: if 'version_added' in option:
callback(option, 'version_added') callback(option, 'version_added', 'version_added_collection')
if not is_module: if not is_module:
if isinstance(option.get('env'), list): if isinstance(option.get('env'), list):
process_option_specifiers(option['env']) process_option_specifiers(option['env'])
@ -67,40 +73,44 @@ def _process_versions_and_dates(fragment, is_module, callback):
process_option_specifiers(option['ini']) process_option_specifiers(option['ini'])
if isinstance(option.get('vars'), list): if isinstance(option.get('vars'), list):
process_option_specifiers(option['vars']) process_option_specifiers(option['vars'])
if isinstance(option.get('suboptions'), dict): if isinstance(option.get('suboptions'), MutableMapping):
process_options(option['suboptions']) process_options(option['suboptions'])
def process_return_values(return_values): def process_return_values(return_values):
for return_value in return_values.values(): for return_value in return_values.values():
if not isinstance(return_value, MutableMapping):
continue
if 'version_added' in return_value: if 'version_added' in return_value:
callback(return_value, 'version_added') callback(return_value, 'version_added', 'version_added_collection')
if isinstance(return_value.get('contains'), dict): if isinstance(return_value.get('contains'), MutableMapping):
process_return_values(return_value['contains']) process_return_values(return_value['contains'])
if return_docs:
process_return_values(fragment)
return
if 'version_added' in fragment: if 'version_added' in fragment:
callback(fragment, 'version_added') callback(fragment, 'version_added', 'version_added_collection')
if isinstance(fragment.get('deprecated'), dict): if isinstance(fragment.get('deprecated'), MutableMapping):
process_deprecation(fragment['deprecated']) process_deprecation(fragment['deprecated'])
if isinstance(fragment.get('options'), dict): if isinstance(fragment.get('options'), MutableMapping):
process_options(fragment['options']) process_options(fragment['options'])
def tag_versions_and_dates(fragment, prefix, is_module): def add_collection_to_versions_and_dates(fragment, collection_name, is_module, return_docs=False):
def tag(options, option): def add(options, option, collection_name_field):
options[option] = '%s%s' % (prefix, options[option]) if collection_name_field not in options:
options[collection_name_field] = collection_name
_process_versions_and_dates(fragment, is_module, tag) _process_versions_and_dates(fragment, is_module, return_docs, add)
def untag_versions_and_dates(fragment, prefix, is_module): def remove_current_collection_from_versions_and_dates(fragment, collection_name, is_module, return_docs=False):
def untag(options, option): def remove(options, option, collection_name_field):
v = options[option] if options.get(collection_name_field) == collection_name:
if isinstance(v, string_types): del options[collection_name_field]
v = to_native(v)
if v.startswith(prefix):
options[option] = v[len(prefix):]
_process_versions_and_dates(fragment, is_module, untag) _process_versions_and_dates(fragment, is_module, return_docs, remove)
def add_fragments(doc, filename, fragment_loader, is_module=False): def add_fragments(doc, filename, fragment_loader, is_module=False):
@ -147,7 +157,7 @@ def add_fragments(doc, filename, fragment_loader, is_module=False):
real_fragment_name = getattr(fragment_class, '_load_name') real_fragment_name = getattr(fragment_class, '_load_name')
if real_fragment_name.startswith('ansible_collections.'): if real_fragment_name.startswith('ansible_collections.'):
real_collection_name = '.'.join(real_fragment_name.split('.')[1:3]) real_collection_name = '.'.join(real_fragment_name.split('.')[1:3])
tag_versions_and_dates(fragment, '%s:' % (real_collection_name, ), is_module=is_module) add_collection_to_versions_and_dates(fragment, real_collection_name, is_module=is_module)
if 'notes' in fragment: if 'notes' in fragment:
notes = fragment.pop('notes') notes = fragment.pop('notes')
@ -195,7 +205,7 @@ def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False,
if data.get('doc', False): if data.get('doc', False):
# tag version_added # tag version_added
if collection_name is not None: if collection_name is not None:
tag_versions_and_dates(data['doc'], '%s:' % (collection_name, ), is_module=is_module) add_collection_to_versions_and_dates(data['doc'], collection_name, is_module=is_module)
# add fragments to documentation # add fragments to documentation
add_fragments(data['doc'], filename, fragment_loader=fragment_loader, is_module=is_module) add_fragments(data['doc'], filename, fragment_loader=fragment_loader, is_module=is_module)

@ -83,7 +83,7 @@ class UnsafeProxy(object):
from ansible.utils.display import Display from ansible.utils.display import Display
Display().deprecated( Display().deprecated(
'UnsafeProxy is being deprecated. Use wrap_var or AnsibleUnsafeBytes/AnsibleUnsafeText directly instead', 'UnsafeProxy is being deprecated. Use wrap_var or AnsibleUnsafeBytes/AnsibleUnsafeText directly instead',
version='ansible.builtin:2.13' version='2.13', collection_name='ansible.builtin'
) )
# In our usage we should only receive unicode strings. # In our usage we should only receive unicode strings.
# This conditional and conversion exists to sanity check the values # This conditional and conversion exists to sanity check the values

@ -99,7 +99,7 @@ class FactCache(MutableMapping):
display.deprecated('Calling FactCache().update(key, value) is deprecated. Use' display.deprecated('Calling FactCache().update(key, value) is deprecated. Use'
' FactCache().first_order_merge(key, value) if you want the old' ' FactCache().first_order_merge(key, value) if you want the old'
' behaviour or use FactCache().update({key: value}) if you want' ' behaviour or use FactCache().update({key: value}) if you want'
' dict-like behaviour.', version='ansible.builtin:2.12') ' dict-like behaviour.', version='2.12', collection_name='ansible.builtin')
return self.first_order_merge(*args) return self.first_order_merge(*args)
elif len(args) == 1: elif len(args) == 1:

@ -36,7 +36,7 @@
- get_element_attribute_wrong.matches[0]['rating']['subjective'] == 'true' - get_element_attribute_wrong.matches[0]['rating']['subjective'] == 'true'
- get_element_attribute_wrong.deprecations is defined - get_element_attribute_wrong.deprecations is defined
- get_element_attribute_wrong.deprecations[0].msg == "Parameter 'attribute=subjective' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." - get_element_attribute_wrong.deprecations[0].msg == "Parameter 'attribute=subjective' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry."
- get_element_attribute_wrong.deprecations[0].version == 'ansible.builtin:2.12' - get_element_attribute_wrong.deprecations[0].version == '2.12'
- name: Get element text - name: Get element text
xml: xml:

@ -757,10 +757,12 @@ test_no_log - Invoked with:
options = @{ options = @{
removed1 = @{removed_in_version = "2.1"} removed1 = @{removed_in_version = "2.1"}
removed2 = @{removed_in_version = "2.2"} removed2 = @{removed_in_version = "2.2"}
removed3 = @{removed_in_version = "2.3"; removed_from_collection = "ansible.builtin"}
} }
} }
Set-Variable -Name complex_args -Scope Global -Value @{ Set-Variable -Name complex_args -Scope Global -Value @{
removed1 = "value" removed1 = "value"
removed3 = "value"
} }
$m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
@ -781,12 +783,19 @@ test_no_log - Invoked with:
module_args = @{ module_args = @{
removed1 = "value" removed1 = "value"
removed2 = $null removed2 = $null
removed3 = "value"
} }
} }
deprecations = @( deprecations = @(
@{
msg = "Param 'removed3' is deprecated. See the module docs for more information"
version = "2.3"
collection_name = "ansible.builtin"
},
@{ @{
msg = "Param 'removed1' is deprecated. See the module docs for more information" msg = "Param 'removed1' is deprecated. See the module docs for more information"
version = "2.1" version = "2.1"
collection_name = $null
} }
) )
} }
@ -798,10 +807,12 @@ test_no_log - Invoked with:
options = @{ options = @{
removed1 = @{removed_at_date = [DateTime]"2020-03-10"} removed1 = @{removed_at_date = [DateTime]"2020-03-10"}
removed2 = @{removed_at_date = [DateTime]"2020-03-11"} removed2 = @{removed_at_date = [DateTime]"2020-03-11"}
removed3 = @{removed_at_date = [DateTime]"2020-06-07"; removed_from_collection = "ansible.builtin"}
} }
} }
Set-Variable -Name complex_args -Scope Global -Value @{ Set-Variable -Name complex_args -Scope Global -Value @{
removed1 = "value" removed1 = "value"
removed3 = "value"
} }
$m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
@ -822,12 +833,19 @@ test_no_log - Invoked with:
module_args = @{ module_args = @{
removed1 = "value" removed1 = "value"
removed2 = $null removed2 = $null
removed3 = "value"
} }
} }
deprecations = @( deprecations = @(
@{
msg = "Param 'removed3' is deprecated. See the module docs for more information"
date = "2020-06-07"
collection_name = "ansible.builtin"
},
@{ @{
msg = "Param 'removed1' is deprecated. See the module docs for more information" msg = "Param 'removed1' is deprecated. See the module docs for more information"
date = "2020-03-10" date = "2020-03-10"
collection_name = $null
} }
) )
} }
@ -844,12 +862,16 @@ test_no_log - Invoked with:
options = @{ options = @{
option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10"}) } option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10"}) }
option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11"}) } option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11"}) }
option3 = @{ type = "str"; aliases = "alias3"; deprecated_aliases = @(@{name = "alias3"; date = [DateTime]"2020-03-11"}) } option3 = @{ type = "str"; aliases = "alias3"; deprecated_aliases = @(@{name = "alias3"; version = "2.12"; collection_name = "ansible.builtin"}) }
option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-09"}) } option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-11"}) }
option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-09"}) }
option6 = @{ type = "str"; aliases = "alias6"; deprecated_aliases = @(@{name = "alias6"; date = [DateTime]"2020-06-01"; collection_name = "ansible.builtin"}) }
} }
} }
option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-10"}) } option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-10"}) }
option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-12"}) } option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-12"}) }
option6 = @{ type = "str"; aliases = "alias6"; deprecated_aliases = @(@{name = "alias6"; version = "2.12"; collection_name = "ansible.builtin"}) }
option7 = @{ type = "str"; aliases = "alias7"; deprecated_aliases = @(@{name = "alias7"; date = [DateTime]"2020-06-07"; collection_name = "ansible.builtin"}) }
} }
} }
@ -859,11 +881,15 @@ test_no_log - Invoked with:
option3 = @{ option3 = @{
option1 = "option1" option1 = "option1"
alias2 = "alias2" alias2 = "alias2"
option3 = "option3" alias3 = "alias3"
alias4 = "alias4" option4 = "option4"
alias5 = "alias5"
alias6 = "alias6"
} }
option4 = "option4" option4 = "option4"
alias5 = "alias5" alias5 = "alias5"
alias6 = "alias6"
alias7 = "alias7"
} }
$m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
@ -888,31 +914,63 @@ test_no_log - Invoked with:
option1 = "option1" option1 = "option1"
option2 = "alias2" option2 = "alias2"
alias2 = "alias2" alias2 = "alias2"
option3 = "option3" option3 = "alias3"
option4 = "alias4" alias3 = "alias3"
alias4 = "alias4" option4 = "option4"
option5 = "alias5"
alias5 = "alias5"
option6 = "alias6"
alias6 = "alias6"
} }
option4 = "option4" option4 = "option4"
option5 = "alias5" option5 = "alias5"
alias5 = "alias5" alias5 = "alias5"
option6 = "alias6"
alias6 = "alias6"
option7 = "alias7"
alias7 = "alias7"
} }
} }
deprecations = @( deprecations = @(
@{
msg = "Alias 'alias7' is deprecated. See the module docs for more information"
date = "2020-06-07"
collection_name = "ansible.builtin"
},
@{ @{
msg = "Alias 'alias1' is deprecated. See the module docs for more information" msg = "Alias 'alias1' is deprecated. See the module docs for more information"
version = "2.10" version = "2.10"
collection_name = $null
}, },
@{ @{
msg = "Alias 'alias5' is deprecated. See the module docs for more information" msg = "Alias 'alias5' is deprecated. See the module docs for more information"
date = "2020-03-12" date = "2020-03-12"
} collection_name = $null
},
@{
msg = "Alias 'alias6' is deprecated. See the module docs for more information"
version = "2.12"
collection_name = "ansible.builtin"
},
@{ @{
msg = "Alias 'alias2' is deprecated. See the module docs for more information - found in option3" msg = "Alias 'alias2' is deprecated. See the module docs for more information - found in option3"
version = "2.11" version = "2.11"
} collection_name = $null
},
@{ @{
msg = "Alias 'alias4' is deprecated. See the module docs for more information - found in option3" msg = "Alias 'alias5' is deprecated. See the module docs for more information - found in option3"
date = "2020-03-09" date = "2020-03-09"
collection_name = $null
},
@{
msg = "Alias 'alias3' is deprecated. See the module docs for more information - found in option3"
version = "2.12"
collection_name = "ansible.builtin"
},
@{
msg = "Alias 'alias6' is deprecated. See the module docs for more information - found in option3"
date = "2020-06-01"
collection_name = "ansible.builtin"
} }
) )
} }
@ -1141,12 +1199,15 @@ test_no_log - Invoked with:
"Deprecate and warn with version" = { "Deprecate and warn with version" = {
$m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
$m.Deprecate("message", "2.8") $m.Deprecate("message", "2.7")
$actual_deprecate_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
$m.Deprecate("message w collection", "2.8", "ansible.builtin")
$actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
$m.Warn("warning") $m.Warn("warning")
$actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
$actual_deprecate_event.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2.8" $actual_deprecate_event_1.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2.7"
$actual_deprecate_event_2.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2.8"
$actual_warn_event.EntryType | Assert-Equals -Expected "Warning" $actual_warn_event.EntryType | Assert-Equals -Expected "Warning"
$actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning" $actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning"
@ -1166,19 +1227,25 @@ test_no_log - Invoked with:
module_args = @{} module_args = @{}
} }
warnings = @("warning") warnings = @("warning")
deprecations = @(@{msg = "message"; version = "2.8"}) deprecations = @(
@{msg = "message"; version = "2.7"; collection_name = $null},
@{msg = "message w collection"; version = "2.8"; collection_name = "ansible.builtin"}
)
} }
$actual | Assert-DictionaryEquals -Expected $expected $actual | Assert-DictionaryEquals -Expected $expected
} }
"Deprecate and warn with date" = { "Deprecate and warn with date" = {
$m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
$m.Deprecate("message", [DateTime]"2020-01-02") $m.Deprecate("message", [DateTime]"2020-01-01")
$actual_deprecate_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
$m.Deprecate("message w collection", [DateTime]"2020-01-02", "ansible.builtin")
$actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
$m.Warn("warning") $m.Warn("warning")
$actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
$actual_deprecate_event.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2020-01-02" $actual_deprecate_event_1.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2020-01-01"
$actual_deprecate_event_2.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2020-01-02"
$actual_warn_event.EntryType | Assert-Equals -Expected "Warning" $actual_warn_event.EntryType | Assert-Equals -Expected "Warning"
$actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning" $actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning"
@ -1198,7 +1265,10 @@ test_no_log - Invoked with:
module_args = @{} module_args = @{}
} }
warnings = @("warning") warnings = @("warning")
deprecations = @(@{msg = "message"; date = "2020-01-02"}) deprecations = @(
@{msg = "message"; date = "2020-01-01"; collection_name = $null},
@{msg = "message w collection"; date = "2020-01-02"; collection_name = "ansible.builtin"}
)
} }
$actual | Assert-DictionaryEquals -Expected $expected $actual | Assert-DictionaryEquals -Expected $expected
} }
@ -1627,8 +1697,8 @@ test_no_log - Invoked with:
$expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
$expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, " $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
$expected_msg += "removed_in_version, removed_at_date, required, required_by, required_if, required_one_of, " $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
$expected_msg += "required_together, supports_check_mode, type" $expected_msg += "required_one_of, required_together, supports_check_mode, type"
$actual.Keys.Count | Assert-Equals -Expected 3 $actual.Keys.Count | Assert-Equals -Expected 3
$actual.failed | Assert-Equals -Expected $true $actual.failed | Assert-Equals -Expected $true
@ -1660,8 +1730,8 @@ test_no_log - Invoked with:
$expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
$expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, " $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
$expected_msg += "removed_in_version, removed_at_date, required, required_by, required_if, required_one_of, " $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
$expected_msg += "required_together, supports_check_mode, type - found in option_key -> sub_option_key" $expected_msg += "required_one_of, required_together, supports_check_mode, type - found in option_key -> sub_option_key"
$actual.Keys.Count | Assert-Equals -Expected 3 $actual.Keys.Count | Assert-Equals -Expected 3
$actual.failed | Assert-Equals -Expected $true $actual.failed | Assert-Equals -Expected $true
@ -2747,7 +2817,7 @@ test_no_log - Invoked with:
option2 = @{ option2 = @{
aliases = @("alias2_spec") aliases = @("alias2_spec")
deprecated_aliases = @( deprecated_aliases = @(
@{name = "alias2_spec"; version = "2.0"} @{name = "alias2_spec"; version = "2.0"; collection_name = "ansible.builtin"}
) )
} }
} }
@ -2761,7 +2831,7 @@ test_no_log - Invoked with:
option2 = @{ option2 = @{
aliases = @("alias2") aliases = @("alias2")
deprecated_aliases = @( deprecated_aliases = @(
@{name = "alias2"; version = "2.0"} @{name = "alias2"; version = "2.0"; collection_name = "foo.bar"}
) )
type = "str" type = "str"
} }
@ -2786,10 +2856,10 @@ test_no_log - Invoked with:
$actual.deprecations.Count | Assert-Equals -Expected 2 $actual.deprecations.Count | Assert-Equals -Expected 2
$actual.deprecations[0] | Assert-DictionaryEquals -Expected @{ $actual.deprecations[0] | Assert-DictionaryEquals -Expected @{
msg = "Alias 'alias1_spec' is deprecated. See the module docs for more information"; version = "2.0" msg = "Alias 'alias1_spec' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = $null
} }
$actual.deprecations[1] | Assert-DictionaryEquals -Expected @{ $actual.deprecations[1] | Assert-DictionaryEquals -Expected @{
msg = "Alias 'alias2' is deprecated. See the module docs for more information"; version = "2.0" msg = "Alias 'alias2' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = "foo.bar"
} }
$actual.changed | Assert-Equals -Expected $false $actual.changed | Assert-Equals -Expected $false
$actual.invocation | Assert-DictionaryEquals -Expected @{ $actual.invocation | Assert-DictionaryEquals -Expected @{
@ -2926,25 +2996,25 @@ test_no_log - Invoked with:
# Single element of the same list type not in a list # Single element of the same list type not in a list
option1 = @{ option1 = @{
aliases = "alias1" aliases = "alias1"
deprecated_aliases = @{name="alias1";version="2.0"} deprecated_aliases = @{name="alias1";version="2.0";collection_name="foo.bar"}
} }
# Arrays # Arrays
option2 = @{ option2 = @{
aliases = ,"alias2" aliases = ,"alias2"
deprecated_aliases = ,@{name="alias2";version="2.0"} deprecated_aliases = ,@{name="alias2";version="2.0";collection_name="foo.bar"}
} }
# ArrayList # ArrayList
option3 = @{ option3 = @{
aliases = [System.Collections.ArrayList]@("alias3") aliases = [System.Collections.ArrayList]@("alias3")
deprecated_aliases = [System.Collections.ArrayList]@(@{name="alias3";version="2.0"}) deprecated_aliases = [System.Collections.ArrayList]@(@{name="alias3";version="2.0";collection_name="foo.bar"})
} }
# Generic.List[Object] # Generic.List[Object]
option4 = @{ option4 = @{
aliases = [System.Collections.Generic.List[Object]]@("alias4") aliases = [System.Collections.Generic.List[Object]]@("alias4")
deprecated_aliases = [System.Collections.Generic.List[Object]]@(@{name="alias4";version="2.0"}) deprecated_aliases = [System.Collections.Generic.List[Object]]@(@{name="alias4";version="2.0";collection_name="foo.bar"})
} }
# Generic.List[T] # Generic.List[T]
@ -2954,7 +3024,7 @@ test_no_log - Invoked with:
} }
} }
} }
$spec.options.option5.deprecated_aliases.Add(@{name="alias5";version="2.0"}) $spec.options.option5.deprecated_aliases.Add(@{name="alias5";version="2.0";collection_name="foo.bar"})
Set-Variable -Name complex_args -Scope Global -Value @{ Set-Variable -Name complex_args -Scope Global -Value @{
alias1 = "option1" alias1 = "option1"
@ -2980,6 +3050,7 @@ test_no_log - Invoked with:
foreach ($dep in $actual.deprecations) { foreach ($dep in $actual.deprecations) {
$dep.msg -like "Alias 'alias?' is deprecated. See the module docs for more information" | Assert-Equals -Expected $true $dep.msg -like "Alias 'alias?' is deprecated. See the module docs for more information" | Assert-Equals -Expected $true
$dep.version | Assert-Equals -Expected '2.0' $dep.version | Assert-Equals -Expected '2.0'
$dep.collection_name | Assert-Equals -Expected 'foo.bar'
} }
$actual.invocation | Assert-DictionaryEquals -Expected @{ $actual.invocation | Assert-DictionaryEquals -Expected @{
module_args = @{ module_args = @{

@ -35,7 +35,7 @@ MSGS = {
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-version", "ansible-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"tagged Ansible version number", "Ansible version number",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9504': ("Deprecated version (%r) found in call to Display.deprecated " 'E9504': ("Deprecated version (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate", "or AnsibleModule.deprecate",
@ -48,20 +48,21 @@ MSGS = {
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"collection-invalid-deprecated-version", "collection-invalid-deprecated-version",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"tagged collection version number", "collection version number",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9506': ("Invalid tagged version (%r) found in call to " 'E9506': ("No collection name found in call to Display.deprecated or "
"Display.deprecated or AnsibleModule.deprecate", "AnsibleModule.deprecate",
"invalid-tagged-version", "ansible-deprecated-no-collection-name",
"Used when a call to Display.deprecated specifies a version " "The current collection name in format `namespace.name` must "
"number which has no collection name tag, for example " "be provided as collection_name when calling Display.deprecated "
"`community.general:1.2.3` or `ansible.builtin:2.10`", "or AnsibleModule.deprecate (`ansible.builtin` for ansible-base)",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9507': ("Version tag for wrong collection (%r) found in call to " 'E9507': ("Wrong collection name (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"wrong-collection-deprecated-version-tag", "wrong-collection-deprecated",
"Deprecation versions must be prefixed with the name of this " "The name of the current collection must be passed to the "
"collection (`ansible.builtin:` for Ansible-base)", "Display.deprecated resp. AnsibleModule.deprecate calls "
"(`ansible.builtin` for ansible-base)",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9508': ("Expired date (%r) found in call to Display.deprecated " 'E9508': ("Expired date (%r) found in call to Display.deprecated "
"or AnsibleModule.deprecate", "or AnsibleModule.deprecate",
@ -73,34 +74,18 @@ MSGS = {
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-invalid-deprecated-date", "ansible-invalid-deprecated-date",
"Used when a call to Display.deprecated specifies an invalid " "Used when a call to Display.deprecated specifies an invalid "
"date. It must be a string in format `namespace.name:YYYY-MM-DD` " "date. It must be a string in format `YYYY-MM-DD` (ISO 8601)",
"(collection identifier followed by ISO 8601)",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9510': ("Both version and date found in call to " 'E9510': ("Both version and date found in call to "
"Display.deprecated or AnsibleModule.deprecate", "Display.deprecated or AnsibleModule.deprecate",
"ansible-deprecated-both-version-and-date", "ansible-deprecated-both-version-and-date",
"Only one of version and date must be specified", "Only one of version and date must be specified",
{'minversion': (2, 6)}), {'minversion': (2, 6)}),
'E9511': ("Invalid tagged date (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"invalid-tagged-date",
"Used when a call to Display.deprecated specifies a date "
"which has no collection name tag, for example "
"`community.general:2020-01-01` or `ansible.builtin:2020-12-31`",
{'minversion': (2, 6)}),
'E9512': ("Date tag for wrong collection (%r) found in call to "
"Display.deprecated or AnsibleModule.deprecate",
"wrong-collection-deprecated-date-tag",
"Deprecation dates must be prefixed with the name of this "
"collection (`ansible.builtin:` for Ansible-base)",
{'minversion': (2, 6)}),
} }
ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3])) ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3]))
TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$')
def _get_expr_name(node): def _get_expr_name(node):
"""Funciton to get either ``attrname`` or ``name`` from ``node.func.expr`` """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr``
@ -142,7 +127,7 @@ class AnsibleDeprecatedChecker(BaseChecker):
'default': None, 'default': None,
'type': 'string', 'type': 'string',
'metavar': '<name>', 'metavar': '<name>',
'help': 'The collection\'s name used to check tagged version numbers in deprecations.', 'help': 'The collection\'s name used to check collection names in deprecations.',
}), }),
('collection-version', { ('collection-version', {
'default': None, 'default': None,
@ -166,43 +151,26 @@ class AnsibleDeprecatedChecker(BaseChecker):
def _check_date(self, node, date): def _check_date(self, node, date):
if not isinstance(date, str): if not isinstance(date, str):
self.add_message('invalid-tagged-date', node=node, args=(date,)) self.add_message('invalid-date', node=node, args=(date,))
return
matcher = TAGGED_VERSION_RE.match(date)
if not matcher:
self.add_message('invalid-tagged-date', node=node, args=(date,))
return return
collection = matcher.group(1)
date_str = matcher.group(2)
if collection != (self.collection_name or 'ansible.builtin'):
self.add_message('wrong-collection-deprecated-date-tag', node=node, args=(date,))
try: try:
if parse_isodate(date_str) < datetime.date.today(): date_parsed = parse_isodate(date)
self.add_message('ansible-deprecated-date', node=node, args=(date,))
except ValueError: except ValueError:
self.add_message('ansible-invalid-deprecated-date', node=node, args=(date,)) self.add_message('ansible-invalid-deprecated-date', node=node, args=(date,))
def _check_version(self, node, version):
if not isinstance(version, str):
self.add_message('invalid-tagged-version', node=node, args=(version,))
return return
matcher = TAGGED_VERSION_RE.match(version) if date_parsed < datetime.date.today():
if not matcher: self.add_message('ansible-deprecated-date', node=node, args=(date,))
self.add_message('invalid-tagged-version', node=node, args=(version,))
return
collection = matcher.group(1) def _check_version(self, node, version, collection_name):
version_no = matcher.group(2) if not isinstance(version, (str, float)):
self.add_message('invalid-version', node=node, args=(version,))
return
if collection != (self.collection_name or 'ansible.builtin'): version_no = str(version)
self.add_message('wrong-collection-deprecated-version-tag', node=node, args=(version,))
if collection == 'ansible.builtin': if collection_name == 'ansible.builtin':
# Ansible-base # Ansible-base
try: try:
if not version_no: if not version_no:
@ -212,13 +180,13 @@ class AnsibleDeprecatedChecker(BaseChecker):
self.add_message('ansible-deprecated-version', node=node, args=(version,)) self.add_message('ansible-deprecated-version', node=node, args=(version,))
except ValueError: except ValueError:
self.add_message('ansible-invalid-deprecated-version', node=node, args=(version,)) self.add_message('ansible-invalid-deprecated-version', node=node, args=(version,))
else: elif collection_name:
# Collections # Collections
try: try:
if not version_no: if not version_no:
raise ValueError('Version string should not be empty') raise ValueError('Version string should not be empty')
semantic_version = SemanticVersion(version_no) semantic_version = SemanticVersion(version_no)
if collection == self.collection_name and self.collection_version is not None: if collection_name == self.collection_name and self.collection_version is not None:
if self.collection_version >= semantic_version: if self.collection_version >= semantic_version:
self.add_message('collection-deprecated-version', node=node, args=(version,)) self.add_message('collection-deprecated-version', node=node, args=(version,))
except ValueError: except ValueError:
@ -228,6 +196,7 @@ class AnsibleDeprecatedChecker(BaseChecker):
def visit_call(self, node): def visit_call(self, node):
version = None version = None
date = None date = None
collection_name = None
try: try:
if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or
node.func.attrname == 'deprecate' and _get_expr_name(node)): node.func.attrname == 'deprecate' and _get_expr_name(node)):
@ -246,6 +215,11 @@ class AnsibleDeprecatedChecker(BaseChecker):
# This is likely a variable # This is likely a variable
return return
date = keyword.value.value 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: if not version and not date:
try: try:
version = node.args[1].value version = node.args[1].value
@ -254,13 +228,18 @@ class AnsibleDeprecatedChecker(BaseChecker):
return return
if version and date: if version and date:
self.add_message('ansible-deprecated-both-version-and-date', node=node) self.add_message('ansible-deprecated-both-version-and-date', node=node)
return
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,))
else:
self.add_message('ansible-deprecated-no-collection-name', node=node)
if date: if date:
self._check_date(node, date) self._check_date(node, date)
elif version:
if version: self._check_version(node, version, collection_name)
self._check_version(node, version)
except AttributeError: except AttributeError:
# Not the type of node we are interested in # Not the type of node we are interested in
pass pass

@ -44,7 +44,7 @@ from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.plugins.loader import fragment_loader from ansible.plugins.loader import fragment_loader
from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder
from ansible.utils.plugin_docs import BLACKLIST, tag_versions_and_dates, add_fragments, get_docstring from ansible.utils.plugin_docs import BLACKLIST, add_collection_to_versions_and_dates, add_fragments, get_docstring
from ansible.utils.version import SemanticVersion from ansible.utils.version import SemanticVersion
from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec
@ -288,14 +288,22 @@ class ModuleValidator(Validator):
else: else:
self.base_module = None self.base_module = None
def _create_version(self, v): def _create_version(self, v, collection_name=None):
if not v: if not v:
raise ValueError('Empty string is not a valid version') raise ValueError('Empty string is not a valid version')
if collection_name == 'ansible.builtin':
return LooseVersion(v)
if collection_name is not None:
return SemanticVersion(v)
return self._Version(v) return self._Version(v)
def _create_strict_version(self, v): def _create_strict_version(self, v, collection_name=None):
if not v: if not v:
raise ValueError('Empty string is not a valid version') raise ValueError('Empty string is not a valid version')
if collection_name == 'ansible.builtin':
return StrictVersion(v)
if collection_name is not None:
return SemanticVersion(v)
return self._StrictVersion(v) return self._StrictVersion(v)
def __enter__(self): def __enter__(self):
@ -887,24 +895,6 @@ class ModuleValidator(Validator):
msg='%s: %s' % (combined_path, error_message) msg='%s: %s' % (combined_path, error_message)
) )
@staticmethod
def _split_tagged_version(version_str):
if not isinstance(version_str, string_types):
raise ValueError('Tagged version must be string')
version_str = to_native(version_str)
if ':' not in version_str:
raise ValueError('Tagged version must have ":"')
return version_str.split(':', 1)
@staticmethod
def _extract_version_from_tag_for_msg(version_str):
if not isinstance(version_str, string_types):
return version_str
version_str = to_native(version_str)
if ':' not in version_str:
return version_str
return version_str.split(':', 1)[1]
def _validate_docs(self): def _validate_docs(self):
doc_info = self._get_docs() doc_info = self._get_docs()
doc = None doc = None
@ -945,7 +935,7 @@ class ModuleValidator(Validator):
self.name, 'DOCUMENTATION' self.name, 'DOCUMENTATION'
) )
if doc: if doc:
tag_versions_and_dates(doc, '%s:' % (self.collection_name, ), is_module=True) add_collection_to_versions_and_dates(doc, self.collection_name, is_module=True)
for error in errors: for error in errors:
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
@ -995,6 +985,14 @@ class ModuleValidator(Validator):
if 'deprecated' in doc and doc.get('deprecated'): if 'deprecated' in doc and doc.get('deprecated'):
doc_deprecated = True doc_deprecated = True
doc_deprecation = doc['deprecated'] doc_deprecation = doc['deprecated']
documentation_collection = doc_deprecation.get('removed_from_collection')
if documentation_collection != self.collection_name:
self.reporter.error(
path=self.object_path,
code='deprecation-wrong-collection',
msg='"DOCUMENTATION.deprecation.removed_from_collection must be the current collection name: %r vs. %r' % (
documentation_collection, self.collection_name)
)
else: else:
doc_deprecated = False doc_deprecated = False
@ -1067,6 +1065,8 @@ class ModuleValidator(Validator):
data, errors, traces = parse_yaml(doc_info['RETURN']['value'], data, errors, traces = parse_yaml(doc_info['RETURN']['value'],
doc_info['RETURN']['lineno'], doc_info['RETURN']['lineno'],
self.name, 'RETURN') self.name, 'RETURN')
if data:
add_collection_to_versions_and_dates(data, self.collection_name, is_module=True, return_docs=True)
self._validate_docs_schema(data, return_schema(for_collection=bool(self.collection)), self._validate_docs_schema(data, return_schema(for_collection=bool(self.collection)),
'RETURN', 'return-syntax-error') 'RETURN', 'return-syntax-error')
@ -1124,8 +1124,8 @@ class ModuleValidator(Validator):
routing_version = routing_deprecation.get('removal_version') routing_version = routing_deprecation.get('removal_version')
# The versions and dates in the module documentation are auto-tagged, so remove the tag # The versions and dates in the module documentation are auto-tagged, so remove the tag
# to make comparison possible and to avoid confusing the user. # to make comparison possible and to avoid confusing the user.
documentation_date = self._extract_version_from_tag_for_msg(doc_deprecation.get('removed_at_date')) documentation_date = doc_deprecation.get('removed_at_date')
documentation_version = self._extract_version_from_tag_for_msg(doc_deprecation.get('removed_in')) documentation_version = doc_deprecation.get('removed_in')
if routing_date != documentation_date: if routing_date != documentation_date:
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
@ -1148,45 +1148,42 @@ class ModuleValidator(Validator):
def _check_version_added(self, doc, existing_doc): def _check_version_added(self, doc, existing_doc):
version_added_raw = doc.get('version_added') version_added_raw = doc.get('version_added')
try: try:
collection_name = doc.get('version_added_collection')
version_added = self._create_strict_version( version_added = self._create_strict_version(
self._extract_version_from_tag_for_msg(str(doc.get('version_added', '0.0') or '0.0'))) str(doc.get('version_added', '0.0') or '0.0'),
except ValueError: collection_name=collection_name)
except ValueError as e:
version_added = doc.get('version_added', '0.0') version_added = doc.get('version_added', '0.0')
if self._is_new_module() or version_added != 'ansible.builtin:historical': if version_added != 'historical' or self._is_new_module():
# already reported during schema validation, except: self.reporter.error(
if version_added == 'ansible.builtin:historical': path=self.object_path,
self.reporter.error( code='module-invalid-version-added',
path=self.object_path, msg='version_added is not a valid version number: %r. Error: %s' % (version_added, e)
code='module-invalid-version-added', )
msg='version_added is not a valid version number: %r' % 'historical'
)
return return
if existing_doc and str(version_added_raw) != str(existing_doc.get('version_added')): if existing_doc and str(version_added_raw) != str(existing_doc.get('version_added')):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='module-incorrect-version-added', code='module-incorrect-version-added',
msg='version_added should be %r. Currently %r' % ( msg='version_added should be %r. Currently %r' % (existing_doc.get('version_added'), version_added_raw)
self._extract_version_from_tag_for_msg(existing_doc.get('version_added')),
self._extract_version_from_tag_for_msg(version_added_raw))
) )
if not self._is_new_module(): if not self._is_new_module():
return return
should_be = '.'.join(ansible_version.split('.')[:2]) should_be = '.'.join(ansible_version.split('.')[:2])
strict_ansible_version = self._create_strict_version(should_be) strict_ansible_version = self._create_strict_version(should_be, collection_name='ansible.builtin')
if (version_added < strict_ansible_version or if (version_added < strict_ansible_version or
strict_ansible_version < version_added): strict_ansible_version < version_added):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='module-incorrect-version-added', code='module-incorrect-version-added',
msg='version_added should be %r. Currently %r' % ( msg='version_added should be %r. Currently %r' % (should_be, version_added_raw)
should_be, self._extract_version_from_tag_for_msg(version_added_raw))
) )
def _validate_ansible_module_call(self, docs, dates_tagged=True): def _validate_ansible_module_call(self, docs):
try: try:
spec, args, kwargs = get_argument_spec(self.path, self.collection) spec, args, kwargs = get_argument_spec(self.path, self.collection)
except AnsibleModuleNotInitialized: except AnsibleModuleNotInitialized:
@ -1208,8 +1205,7 @@ class ModuleValidator(Validator):
) )
return return
self._validate_docs_schema(kwargs, ansible_module_kwargs_schema(for_collection=bool(self.collection), self._validate_docs_schema(kwargs, ansible_module_kwargs_schema(for_collection=bool(self.collection)),
dates_tagged=dates_tagged),
'AnsibleModule', 'invalid-ansiblemodule-schema') 'AnsibleModule', 'invalid-ansiblemodule-schema')
self._validate_argument_spec(docs, spec, kwargs) self._validate_argument_spec(docs, spec, kwargs)
@ -1486,8 +1482,7 @@ class ModuleValidator(Validator):
removed_at_date = data.get('removed_at_date', None) removed_at_date = data.get('removed_at_date', None)
if removed_at_date is not None: if removed_at_date is not None:
try: try:
date = self._extract_version_from_tag_for_msg(removed_at_date) if parse_isodate(removed_at_date) < datetime.date.today():
if parse_isodate(date) < datetime.date.today():
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
@ -1507,7 +1502,7 @@ class ModuleValidator(Validator):
for deprecated_alias in deprecated_aliases: for deprecated_alias in deprecated_aliases:
if 'name' in deprecated_alias and 'date' in deprecated_alias: if 'name' in deprecated_alias and 'date' in deprecated_alias:
try: try:
date = self._extract_version_from_tag_for_msg(deprecated_alias['date']) date = deprecated_alias['date']
if parse_isodate(date) < datetime.date.today(): if parse_isodate(date) < datetime.date.today():
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
@ -1525,57 +1520,97 @@ class ModuleValidator(Validator):
# time. # time.
pass pass
if not self.collection or self.collection_version is not None: has_version = False
if self.collection: if self.collection and self.collection_version is not None:
compare_version = self.collection_version compare_version = self.collection_version
version_of_what = "this collection (%s)" % self.collection_version_str version_of_what = "this collection (%s)" % self.collection_version_str
version_parser_error = "the version number is not a valid semantic version (https://semver.org/)" code_prefix = 'collection'
code_prefix = 'collection' has_version = True
else: elif not self.collection:
compare_version = LOOSE_ANSIBLE_VERSION compare_version = LOOSE_ANSIBLE_VERSION
version_of_what = "Ansible (%s)" % ansible_version version_of_what = "Ansible (%s)" % ansible_version
version_parser_error = "the version number cannot be parsed" code_prefix = 'ansible'
code_prefix = 'ansible' has_version = True
removed_in_version = data.get('removed_in_version', None)
if removed_in_version is not None:
try:
collection_name = data.get('removed_from_collection')
removed_in = self._create_version(str(removed_in_version), collection_name=collection_name)
if has_version and collection_name == self.collection_name and compare_version >= removed_in:
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has a deprecated removed_in_version %r," % removed_in_version
msg += " i.e. the version is less than or equal to the current version of %s" % version_of_what
self.reporter.error(
path=self.object_path,
code=code_prefix + '-deprecated-version',
msg=msg,
)
except ValueError as e:
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has an invalid removed_in_version number %r: %s" % (removed_in_version, e)
self.reporter.error(
path=self.object_path,
code='invalid-deprecated-version',
msg=msg,
)
except TypeError:
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has an invalid removed_in_version number %r: " % (removed_in_version, )
msg += " error while comparing to version of %s" % version_of_what
self.reporter.error(
path=self.object_path,
code='invalid-deprecated-version',
msg=msg,
)
removed_in_version = data.get('removed_in_version', None) if deprecated_aliases is not None:
if removed_in_version is not None: for deprecated_alias in deprecated_aliases:
try: if 'name' in deprecated_alias and 'version' in deprecated_alias:
collection_name, removed_in_version = self._split_tagged_version(removed_in_version) try:
if collection_name == self.collection_name and compare_version >= self._create_version(str(removed_in_version)): collection_name = deprecated_alias.get('collection')
version = self._create_version(str(deprecated_alias['version']), collection_name=collection_name)
if has_version and collection_name == self.collection_name and compare_version >= version:
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has deprecated aliases '%s' with removal in version %r," % (
deprecated_alias['name'], deprecated_alias['version'])
msg += " i.e. the version is less than or equal to the current version of %s" % version_of_what
self.reporter.error(
path=self.object_path,
code=code_prefix + '-deprecated-version',
msg=msg,
)
except ValueError as e:
msg = "Argument '%s' in argument_spec" % arg msg = "Argument '%s' in argument_spec" % arg
if context: if context:
msg += " found in %s" % " -> ".join(context) msg += " found in %s" % " -> ".join(context)
msg += " has a deprecated removed_in_version '%s'," % removed_in_version msg += " has deprecated aliases '%s' with invalid removal version %r: %s" % (
msg += " i.e. the version is less than or equal to the current version of %s" % version_of_what deprecated_alias['name'], deprecated_alias['version'], e)
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code=code_prefix + '-deprecated-version', code='invalid-deprecated-version',
msg=msg,
)
except TypeError:
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has deprecated aliases '%s' with invalid removal version %r:" % (
deprecated_alias['name'], deprecated_alias['version'])
msg += " error while comparing to version of %s" % version_of_what
self.reporter.error(
path=self.object_path,
code='invalid-deprecated-version',
msg=msg, msg=msg,
) )
except ValueError:
# Has been caught in schema validation
pass
if deprecated_aliases is not None:
for deprecated_alias in deprecated_aliases:
if 'name' in deprecated_alias and 'version' in deprecated_alias:
try:
collection_name, version = self._split_tagged_version(deprecated_alias['version'])
if collection_name == self.collection_name and compare_version >= self._create_version(str(version)):
msg = "Argument '%s' in argument_spec" % arg
if context:
msg += " found in %s" % " -> ".join(context)
msg += " has deprecated aliases '%s' with removal in version '%s'," % (
deprecated_alias['name'], deprecated_alias['version'])
msg += " i.e. the version is less than or equal to the current version of %s" % version_of_what
self.reporter.error(
path=self.object_path,
code=code_prefix + '-deprecated-version',
msg=msg,
)
except ValueError:
# Has been caught in schema validation
pass
aliases = data.get('aliases', []) aliases = data.get('aliases', [])
if arg in aliases: if arg in aliases:
@ -1978,15 +2013,18 @@ class ModuleValidator(Validator):
return return
try: try:
mod_collection_name = existing_doc.get('version_added_collection')
mod_version_added = self._create_strict_version( mod_version_added = self._create_strict_version(
self._extract_version_from_tag_for_msg(str(existing_doc.get('version_added', '0.0')))) str(existing_doc.get('version_added', '0.0')),
collection_name=mod_collection_name)
except ValueError: except ValueError:
mod_collection_name = self.collection_name
mod_version_added = self._create_strict_version('0.0') mod_version_added = self._create_strict_version('0.0')
options = doc.get('options', {}) or {} options = doc.get('options', {}) or {}
should_be = '.'.join(ansible_version.split('.')[:2]) should_be = '.'.join(ansible_version.split('.')[:2])
strict_ansible_version = self._create_strict_version(should_be) strict_ansible_version = self._create_strict_version(should_be, collection_name='ansible.builtin')
for option, details in options.items(): for option, details in options.items():
try: try:
@ -1998,11 +2036,21 @@ class ModuleValidator(Validator):
if any(name in existing_options for name in names): if any(name in existing_options for name in names):
# The option already existed. Make sure version_added didn't change. # The option already existed. Make sure version_added didn't change.
for name in names: for name in names:
existing_collection_name = existing_options.get(name, {}).get('version_added_collection')
existing_version = existing_options.get(name, {}).get('version_added') existing_version = existing_options.get(name, {}).get('version_added')
if existing_version: if existing_version:
break break
current_collection_name = details.get('version_added_collection')
current_version = details.get('version_added') current_version = details.get('version_added')
if str(current_version) != str(existing_version): if current_collection_name != existing_collection_name:
self.reporter.error(
path=self.object_path,
code='option-incorrect-version-added-collection',
msg=('version_added for existing option (%s) should '
'belong to collection %r. Currently belongs to %r' %
(option, current_collection_name, existing_collection_name))
)
elif str(current_version) != str(existing_version):
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='option-incorrect-version-added', code='option-incorrect-version-added',
@ -2013,12 +2061,22 @@ class ModuleValidator(Validator):
continue continue
try: try:
collection_name = details.get('version_added_collection')
version_added = self._create_strict_version( version_added = self._create_strict_version(
self._extract_version_from_tag_for_msg(str(details.get('version_added', '0.0')))) str(details.get('version_added', '0.0')),
except ValueError: collection_name=collection_name)
# already reported during schema validation except ValueError as e:
self.reporter.error(
path=self.object_path,
code='option-invalid-version-added',
msg=('version_added for option (%s) is not a valid '
'version for %s. Currently %r. Error: %s' %
(option, collection_name, details.get('version_added', '0.0'), e))
)
continue continue
if collection_name != self.collection_name:
continue
if (strict_ansible_version != mod_version_added and if (strict_ansible_version != mod_version_added and
(version_added < strict_ansible_version or (version_added < strict_ansible_version or
strict_ansible_version < version_added)): strict_ansible_version < version_added)):
@ -2085,24 +2143,28 @@ class ModuleValidator(Validator):
if 'removed_in' in docs['deprecated']: if 'removed_in' in docs['deprecated']:
removed_in = None removed_in = None
try: collection_name = docs['deprecated'].get('removed_from_collection')
collection_name, version = self._split_tagged_version(docs['deprecated']['removed_in']) version = docs['deprecated']['removed_in']
if collection_name != self.collection_name: if collection_name != self.collection_name:
self.reporter.error(
path=self.object_path,
code='invalid-module-deprecation-source',
msg=('The deprecation version for a module must be added in this collection')
)
else:
try:
removed_in = self._create_strict_version(str(version), collection_name=collection_name)
except ValueError as e:
self.reporter.error( self.reporter.error(
path=self.object_path, path=self.object_path,
code='invalid-module-deprecation-source', code='invalid-module-deprecation-version',
msg=('The deprecation version for a module must be added in this collection') msg=('The deprecation version %r cannot be parsed: %s' % (version, e))
) )
else:
removed_in = self._create_strict_version(str(version))
except ValueError:
# ignore and hope we previouslly reported
pass
if removed_in: if removed_in:
if not self.collection: if not self.collection:
strict_ansible_version = self._create_strict_version('.'.join(ansible_version.split('.')[:2])) strict_ansible_version = self._create_strict_version(
'.'.join(ansible_version.split('.')[:2]), self.collection_name)
end_of_deprecation_should_be_removed_only = strict_ansible_version >= removed_in end_of_deprecation_should_be_removed_only = strict_ansible_version >= removed_in
elif self.collection_version: elif self.collection_version:
strict_ansible_version = self.collection_version strict_ansible_version = self.collection_version
@ -2116,7 +2178,7 @@ class ModuleValidator(Validator):
msg = "Module's deprecated.removed_at_date date '%s' is before today" % removed_at_date msg = "Module's deprecated.removed_at_date date '%s' is before today" % removed_at_date
self.reporter.error(path=self.object_path, code='deprecated-date', msg=msg) self.reporter.error(path=self.object_path, code='deprecated-date', msg=msg)
except ValueError: except ValueError:
# ignore and hope we previouslly reported # This happens if the date cannot be parsed. This is already checked by the schema.
pass pass
if self._python_module() and not self._just_docs() and not end_of_deprecation_should_be_removed_only: if self._python_module() and not self._just_docs() and not end_of_deprecation_should_be_removed_only:
@ -2140,8 +2202,7 @@ class ModuleValidator(Validator):
if re.search(pattern, self.text) and self.object_name not in self.PS_ARG_VALIDATE_BLACKLIST: if re.search(pattern, self.text) and self.object_name not in self.PS_ARG_VALIDATE_BLACKLIST:
with ModuleValidator(docs_path, base_branch=self.base_branch, git_cache=self.git_cache) as docs_mv: with ModuleValidator(docs_path, base_branch=self.base_branch, git_cache=self.git_cache) as docs_mv:
docs = docs_mv._validate_docs()[1] docs = docs_mv._validate_docs()[1]
# Don't expect tagged dates! self._validate_ansible_module_call(docs)
self._validate_ansible_module_call(docs, dates_tagged=False)
self._check_gpl3_header() self._check_gpl3_header()
if not self._just_docs() and not end_of_deprecation_should_be_removed_only: if not self._just_docs() and not end_of_deprecation_should_be_removed_only:

@ -6,16 +6,14 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import datetime
import re import re
from functools import partial from distutils.version import StrictVersion
from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid 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.six import string_types
from ansible.module_utils.common.collections import is_iterable from ansible.module_utils.common.collections import is_iterable
from ansible.utils.version import SemanticVersion from ansible.utils.version import SemanticVersion
from distutils.version import StrictVersion
from .utils import parse_isodate from .utils import parse_isodate
@ -37,31 +35,6 @@ def _add_ansible_error_code(exception, error_code):
return exception return exception
def semantic_version(v, error_code=None):
if not isinstance(v, string_types):
raise _add_ansible_error_code(Invalid('Semantic version must be a string'), error_code or 'collection-invalid-version')
if not v:
raise _add_ansible_error_code(
Invalid('Empty string is not a valid semantic version'),
error_code or 'collection-invalid-version')
try:
SemanticVersion(v)
except ValueError as e:
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'collection-invalid-version')
return v
def ansible_version(v, error_code=None):
# Assumes argument is a string or float
if 'historical' == v:
return v
try:
StrictVersion(str(v))
except ValueError as e:
raise _add_ansible_error_code(Invalid(str(e)), error_code or 'ansible-invalid-version')
return v
def isodate(v, error_code=None): def isodate(v, error_code=None):
try: try:
parse_isodate(v) parse_isodate(v)
@ -70,50 +43,29 @@ def isodate(v, error_code=None):
return v return v
TAGGED_VERSION_RE = re.compile('^([^.]+.[^.]+):(.*)$') COLLECTION_NAME_RE = re.compile('^([^.]+.[^.]+)$')
def tagged_version(v, error_code=None): def collection_name(v, error_code=None):
if not isinstance(v, string_types): if not isinstance(v, string_types):
# Should never happen to versions tagged by code raise _add_ansible_error_code(
raise _add_ansible_error_code(Invalid('Tagged version must be a string'), 'invalid-tagged-version') Invalid('Collection name must be a string'), error_code or 'collection-invalid-name')
m = TAGGED_VERSION_RE.match(v) m = COLLECTION_NAME_RE.match(v)
if not m:
# Should never happen to versions tagged by code
raise _add_ansible_error_code(Invalid('Tagged version does not match format'), 'invalid-tagged-version')
collection = m.group(1)
version = m.group(2)
if collection != 'ansible.builtin':
semantic_version(version, error_code=error_code)
else:
ansible_version(version, error_code=error_code)
return v
def tagged_isodate(v, error_code=None):
if not isinstance(v, string_types):
# Should never happen to dates tagged by code
raise _add_ansible_error_code(Invalid('Tagged date must be a string'), 'invalid-tagged-date')
m = TAGGED_VERSION_RE.match(v)
if not m: if not m:
# Should never happen to dates tagged by code raise _add_ansible_error_code(
raise _add_ansible_error_code(Invalid('Tagged date does not match format'), 'invalid-tagged-date') Invalid('Collection name must be of format `<namespace>.<name>`'), error_code or 'collection-invalid-name')
isodate(m.group(2), error_code=error_code)
return v return v
def version(for_collection=False, tagged='never', error_code=None): def version(for_collection=False):
if tagged == 'always':
return Any(partial(tagged_version, error_code=error_code))
if for_collection: if for_collection:
return Any(partial(semantic_version, error_code=error_code)) # We do not accept floats for versions in collections
return All(Any(float, *string_types), partial(ansible_version, error_code=error_code)) return Any(*string_types)
return Any(float, *string_types)
def date(tagged='never', error_code=None): def date(error_code=None):
if tagged == 'always': return Any(isodate, error_code=error_code)
return Any(partial(tagged_isodate, error_code=error_code))
return Any(isodate)
def is_callable(v): def is_callable(v):
@ -189,8 +141,24 @@ def options_with_apply_defaults(v):
return v return v
def argument_spec_schema(for_collection, dates_tagged=True): def option_deprecation(v):
dates_tagged = 'always' if dates_tagged else 'never' 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')
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) any_string_types = Any(*string_types)
schema = { schema = {
any_string_types: { any_string_types: {
@ -206,17 +174,20 @@ def argument_spec_schema(for_collection, dates_tagged=True):
'no_log': bool, 'no_log': bool,
'aliases': Any(list_string_types, tuple(list_string_types)), 'aliases': Any(list_string_types, tuple(list_string_types)),
'apply_defaults': bool, 'apply_defaults': bool,
'removed_in_version': version(for_collection, tagged='always'), 'removed_in_version': version(for_collection),
'removed_at_date': date(tagged=dates_tagged), 'removed_at_date': date(),
'removed_from_collection': collection_name,
'options': Self, 'options': Self,
'deprecated_aliases': Any([Any( 'deprecated_aliases': Any([Any(
{ {
Required('name'): Any(*string_types), Required('name'): Any(*string_types),
Required('date'): date(tagged=dates_tagged), Required('date'): date(),
Required('collection'): collection_name,
}, },
{ {
Required('name'): Any(*string_types), Required('name'): Any(*string_types),
Required('version'): version(for_collection, tagged='always'), Required('version'): version(for_collection),
Required('collection'): collection_name,
}, },
)]), )]),
} }
@ -227,13 +198,14 @@ def argument_spec_schema(for_collection, dates_tagged=True):
Schema({any_string_types: no_required_with_default}), Schema({any_string_types: no_required_with_default}),
Schema({any_string_types: elements_with_list}), Schema({any_string_types: elements_with_list}),
Schema({any_string_types: options_with_apply_defaults}), Schema({any_string_types: options_with_apply_defaults}),
Schema({any_string_types: option_deprecation}),
) )
return Schema(schemas) return Schema(schemas)
def ansible_module_kwargs_schema(for_collection, dates_tagged=True): def ansible_module_kwargs_schema(for_collection):
schema = { schema = {
'argument_spec': argument_spec_schema(for_collection, dates_tagged=dates_tagged), 'argument_spec': argument_spec_schema(for_collection),
'bypass_checks': bool, 'bypass_checks': bool,
'no_log': bool, 'no_log': bool,
'check_invalid_arguments': Any(None, bool), 'check_invalid_arguments': Any(None, bool),
@ -253,6 +225,39 @@ json_value = Schema(Any(
)) ))
def version_added(v):
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':
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-base version: '
'%s' % (version_added, exc)),
error_code='deprecation-either-date-or-version')
else:
try:
version = SemanticVersion()
version.parse(version_added)
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='deprecation-either-date-or-version')
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): def list_dict_option_schema(for_collection):
suboption_schema = Schema( suboption_schema = Schema(
{ {
@ -260,7 +265,8 @@ def list_dict_option_schema(for_collection):
'required': bool, 'required': bool,
'choices': list, 'choices': list,
'aliases': Any(list_string_types), 'aliases': Any(list_string_types),
'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'), 'version_added': version(for_collection),
'version_added_collection': collection_name,
'default': json_value, 'default': json_value,
# Note: Types are strings, not literal bools, such as True or False # 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'), 'type': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
@ -282,7 +288,8 @@ def list_dict_option_schema(for_collection):
'required': bool, 'required': bool,
'choices': list, 'choices': list,
'aliases': Any(list_string_types), 'aliases': Any(list_string_types),
'version_added': version(for_collection, tagged='always', error_code='option-invalid-version-added'), 'version_added': version(for_collection),
'version_added_collection': collection_name,
'default': json_value, 'default': json_value,
'suboptions': Any(None, *list_dict_suboption_schema), 'suboptions': Any(None, *list_dict_suboption_schema),
# Note: Types are strings, not literal bools, such as True or False # Note: Types are strings, not literal bools, such as True or False
@ -293,9 +300,16 @@ def list_dict_option_schema(for_collection):
extra=PREVENT_EXTRA extra=PREVENT_EXTRA
) )
option_version_added = Schema(
All({
'suboptions': Any(None, *[{str_type: Self} for str_type in string_types]),
}, version_added),
extra=ALLOW_EXTRA
)
# This generates list of dicts with keys from string_types and option_schema value # This generates list of dicts with keys from string_types and option_schema value
# for example in Python 3: {str: option_schema} # for example in Python 3: {str: option_schema}
return [{str_type: option_schema} for str_type in string_types] return [{str_type: All(option_schema, option_version_added)} for str_type in string_types]
def return_contains(v): def return_contains(v):
@ -318,7 +332,8 @@ def return_schema(for_collection):
Required('description'): Any(list_string_types, *string_types), Required('description'): Any(list_string_types, *string_types),
'returned': Any(*string_types), # only returned on top level 'returned': Any(*string_types), # only returned on top level
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'), Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': version(for_collection, error_code='return-invalid-version-added'), 'version_added': version(for_collection),
'version_added_collection': collection_name,
'sample': json_value, 'sample': json_value,
'example': json_value, 'example': json_value,
'contains': Any(None, *list({str_type: Self} for str_type in string_types)), 'contains': Any(None, *list({str_type: Self} for str_type in string_types)),
@ -326,7 +341,8 @@ def return_schema(for_collection):
'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'), 'elements': Any(None, 'bits', 'bool', 'bytes', 'dict', 'float', 'int', 'json', 'jsonarg', 'list', 'path', 'raw', 'sid', 'str'),
} }
), ),
Schema(return_contains) Schema(return_contains),
Schema(version_added),
), ),
Schema(type(None)), Schema(type(None)),
) )
@ -343,7 +359,8 @@ def return_schema(for_collection):
Required('description'): Any(list_string_types, *string_types), Required('description'): Any(list_string_types, *string_types),
Required('returned'): Any(*string_types), Required('returned'): Any(*string_types),
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'), Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': version(for_collection, error_code='return-invalid-version-added'), 'version_added': version(for_collection),
'version_added_collection': collection_name,
'sample': json_value, 'sample': json_value,
'example': json_value, 'example': json_value,
'contains': Any(None, *list_dict_return_contains_schema), 'contains': Any(None, *list_dict_return_contains_schema),
@ -352,7 +369,8 @@ def return_schema(for_collection):
} }
} }
), ),
Schema({any_string_types: return_contains}) Schema({any_string_types: return_contains}),
Schema({any_string_types: version_added}),
), ),
Schema(type(None)), Schema(type(None)),
) )
@ -362,17 +380,18 @@ def deprecation_schema(for_collection):
main_fields = { main_fields = {
Required('why'): Any(*string_types), Required('why'): Any(*string_types),
Required('alternative'): Any(*string_types), Required('alternative'): Any(*string_types),
Required('removed_from_collection'): collection_name,
'removed': Any(True), 'removed': Any(True),
} }
date_schema = { date_schema = {
Required('removed_at_date'): date(tagged='always'), Required('removed_at_date'): date(),
} }
date_schema.update(main_fields) date_schema.update(main_fields)
if for_collection: if for_collection:
version_schema = { version_schema = {
Required('removed_in'): version(for_collection, tagged='always'), Required('removed_in'): version(for_collection),
} }
else: else:
version_schema = { version_schema = {
@ -381,9 +400,7 @@ def deprecation_schema(for_collection):
# 2.3 -> removed_in: "2.5" + n for docs stub # 2.3 -> removed_in: "2.5" + n for docs stub
# 2.4 -> removed_in: "2.8" + n for docs stub # 2.4 -> removed_in: "2.8" + n for docs stub
Required('removed_in'): Any( Required('removed_in'): Any(
"ansible.builtin:2.2", "ansible.builtin:2.3", "ansible.builtin:2.4", "ansible.builtin:2.5", "2.2", "2.3", "2.4", "2.5", "2.6", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13", "2.14"),
"ansible.builtin:2.6", "ansible.builtin:2.8", "ansible.builtin:2.9", "ansible.builtin:2.10",
"ansible.builtin:2.11", "ansible.builtin:2.12", "ansible.builtin:2.13", "ansible.builtin:2.14"),
} }
version_schema.update(main_fields) version_schema.update(main_fields)
@ -419,16 +436,15 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False):
'requirements': list_string_types, 'requirements': list_string_types,
'todo': Any(None, list_string_types, *string_types), 'todo': Any(None, list_string_types, *string_types),
'options': Any(None, *list_dict_option_schema(for_collection)), 'options': Any(None, *list_dict_option_schema(for_collection)),
'extends_documentation_fragment': Any(list_string_types, *string_types) 'extends_documentation_fragment': Any(list_string_types, *string_types),
'version_added_collection': collection_name,
} }
if for_collection: if for_collection:
# Optional # Optional
doc_schema_dict['version_added'] = version( doc_schema_dict['version_added'] = version(for_collection=True)
for_collection=True, tagged='always', error_code='module-invalid-version-added')
else: else:
doc_schema_dict[Required('version_added')] = version( doc_schema_dict[Required('version_added')] = version(for_collection=False)
for_collection=False, tagged='always', error_code='module-invalid-version-added')
if deprecated_module: if deprecated_module:
deprecation_required_scheme = { deprecation_required_scheme = {

@ -449,8 +449,8 @@ class AzureRMModuleBase(object):
''' '''
self.module.fail_json(msg=msg, **kwargs) self.module.fail_json(msg=msg, **kwargs)
def deprecate(self, msg, version=None): def deprecate(self, msg, version=None, collection_name=None):
self.module.deprecate(msg, version) self.module.deprecate(msg, version, collection_name=collection_name)
def log(self, msg, pretty_print=False): def log(self, msg, pretty_print=False):
if pretty_print: if pretty_print:

@ -86,7 +86,8 @@ def main():
module = AnsibleAWSModule(argument_spec=argument_spec) module = AnsibleAWSModule(argument_spec=argument_spec)
if module._name == 'aws_az_facts': if module._name == 'aws_az_facts':
module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'", version='ansible.builtin:2.14') module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'",
version='2.14', collection_name='ansible.builtin')
connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff())

@ -136,7 +136,8 @@ class AzureRMFunctionAppInfo(AzureRMModuleBase):
is_old_facts = self.module._name == 'azure_rm_functionapp_facts' is_old_facts = self.module._name == 'azure_rm_functionapp_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_functionapp_facts' module has been renamed to 'azure_rm_functionapp_info'", version='ansible.builtin:2.13') self.module.deprecate("The 'azure_rm_functionapp_facts' module has been renamed to 'azure_rm_functionapp_info'",
version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -140,7 +140,7 @@ class AzureRMMariaDbConfigurationInfo(AzureRMModuleBase):
is_old_facts = self.module._name == 'azure_rm_mariadbconfiguration_facts' is_old_facts = self.module._name == 'azure_rm_mariadbconfiguration_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbconfiguration_facts' module has been renamed to 'azure_rm_mariadbconfiguration_info'", self.module.deprecate("The 'azure_rm_mariadbconfiguration_facts' module has been renamed to 'azure_rm_mariadbconfiguration_info'",
version='ansible.builtin:2.13') version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -146,7 +146,7 @@ class AzureRMMariaDbDatabaseInfo(AzureRMModuleBase):
is_old_facts = self.module._name == 'azure_rm_mariadbdatabase_facts' is_old_facts = self.module._name == 'azure_rm_mariadbdatabase_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbdatabase_facts' module has been renamed to 'azure_rm_mariadbdatabase_info'", self.module.deprecate("The 'azure_rm_mariadbdatabase_facts' module has been renamed to 'azure_rm_mariadbdatabase_info'",
version='ansible.builtin:2.13') version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -142,7 +142,7 @@ class AzureRMMariaDbFirewallRuleInfo(AzureRMModuleBase):
is_old_facts = self.module._name == 'azure_rm_mariadbfirewallrule_facts' is_old_facts = self.module._name == 'azure_rm_mariadbfirewallrule_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbfirewallrule_facts' module has been renamed to 'azure_rm_mariadbfirewallrule_info'", self.module.deprecate("The 'azure_rm_mariadbfirewallrule_facts' module has been renamed to 'azure_rm_mariadbfirewallrule_info'",
version='ansible.builtin:2.13') version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -193,7 +193,8 @@ class AzureRMMariaDbServerInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_mariadbserver_facts' is_old_facts = self.module._name == 'azure_rm_mariadbserver_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_mariadbserver_facts' module has been renamed to 'azure_rm_mariadbserver_info'", version='ansible.builtin:2.13') self.module.deprecate("The 'azure_rm_mariadbserver_facts' module has been renamed to 'azure_rm_mariadbserver_info'",
version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -336,7 +336,8 @@ class AzureRMResourceInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_resource_facts' is_old_facts = self.module._name == 'azure_rm_resource_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_resource_facts' module has been renamed to 'azure_rm_resource_info'", version='ansible.builtin:2.13') self.module.deprecate("The 'azure_rm_resource_facts' module has been renamed to 'azure_rm_resource_info'",
version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -265,7 +265,8 @@ class AzureRMWebAppInfo(AzureRMModuleBase):
def exec_module(self, **kwargs): def exec_module(self, **kwargs):
is_old_facts = self.module._name == 'azure_rm_webapp_facts' is_old_facts = self.module._name == 'azure_rm_webapp_facts'
if is_old_facts: if is_old_facts:
self.module.deprecate("The 'azure_rm_webapp_facts' module has been renamed to 'azure_rm_webapp_info'", version='ansible.builtin:2.13') self.module.deprecate("The 'azure_rm_webapp_facts' module has been renamed to 'azure_rm_webapp_info'",
version='2.13', collection_name='ansible.builtin')
for key in self.module_arg_spec: for key in self.module_arg_spec:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])

@ -302,7 +302,8 @@ def main():
is_old_facts = module._name == 'cloudformation_facts' is_old_facts = module._name == 'cloudformation_facts'
if is_old_facts: if is_old_facts:
module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', " module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', "
"and the renamed one no longer returns ansible_facts", version='ansible.builtin:2.13') "and the renamed one no longer returns ansible_facts",
version='2.13', collection_name='ansible.builtin')
service_mgr = CloudFormationServiceManager(module) service_mgr = CloudFormationServiceManager(module)

@ -450,7 +450,7 @@ class SwarmManager(DockerBaseClass):
if self.state == 'inspect': if self.state == 'inspect':
self.client.module.deprecate( self.client.module.deprecate(
"The 'inspect' state is deprecated, please use 'docker_swarm_info' to inspect swarm cluster", "The 'inspect' state is deprecated, please use 'docker_swarm_info' to inspect swarm cluster",
version='ansible.builtin:2.12') version='2.12', collection_name='ansible.builtin')
choice_map.get(self.state)() choice_map.get(self.state)()

@ -1695,7 +1695,7 @@ def main():
module.deprecate( module.deprecate(
msg='Support for passing both group and group_id has been deprecated. ' msg='Support for passing both group and group_id has been deprecated. '
'Currently group_id is ignored, in future passing both will result in an error', 'Currently group_id is ignored, in future passing both will result in an error',
version='ansible.builtin:2.14') version='2.14', collection_name='ansible.builtin')
if not HAS_BOTO: if not HAS_BOTO:
module.fail_json(msg='boto required for this module') module.fail_json(msg='boto required for this module')

@ -270,7 +270,8 @@ def main():
module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
if module._module._name == 'ec2_ami_facts': if module._module._name == 'ec2_ami_facts':
module._module.deprecate("The 'ec2_ami_facts' module has been renamed to 'ec2_ami_info'", version='ansible.builtin:2.13') module._module.deprecate("The 'ec2_ami_facts' module has been renamed to 'ec2_ami_info'",
version='2.13', collection_name='ansible.builtin')
ec2_client = module.client('ec2') ec2_client = module.client('ec2')

@ -259,7 +259,8 @@ def main():
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'ec2_eni_facts': if module._name == 'ec2_eni_facts':
module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'", version='ansible.builtin:2.13') module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'",
version='2.13', collection_name='ansible.builtin')
if not HAS_BOTO3: if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module') module.fail_json(msg='boto3 required for this module')

@ -552,7 +552,8 @@ def main():
supports_check_mode=True supports_check_mode=True
) )
if module._name == 'ec2_instance_facts': if module._name == 'ec2_instance_facts':
module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", version='ansible.builtin:2.13') module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'",
version='2.13', collection_name='ansible.builtin')
if not HAS_BOTO3: if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module') module.fail_json(msg='boto3 required for this module')

@ -637,7 +637,8 @@ def main():
if module.params.get('purge_policies') is None: if module.params.get('purge_policies') is None:
module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.' module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.'
' To maintain the existing behaviour explicity set purge_policies=true', version='ansible.builtin:2.14') ' To maintain the existing behaviour explicity set purge_policies=true',
version='2.14', collection_name='ansible.builtin')
if module.params.get('boundary'): if module.params.get('boundary'):
if module.params.get('create_instance_profile'): if module.params.get('create_instance_profile'):

@ -142,7 +142,8 @@ class KubernetesInfoModule(KubernetesAnsibleModule):
supports_check_mode=True, supports_check_mode=True,
**kwargs) **kwargs)
if self._name == 'k8s_facts': if self._name == 'k8s_facts':
self.deprecate("The 'k8s_facts' module has been renamed to 'k8s_info'", version='ansible.builtin:2.13') self.deprecate("The 'k8s_facts' module has been renamed to 'k8s_info'",
version='2.13', collection_name='ansible.builtin')
def execute_module(self): def execute_module(self):
self.client = self.get_api_client() self.client = self.get_api_client()

@ -2654,7 +2654,7 @@ def main():
if provider == 'assertonly': if provider == 'assertonly':
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of " module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
"the 'openssl_certificate' module on how to replace it with other modules", "the 'openssl_certificate' module on how to replace it with other modules",
version='ansible.builtin:2.13') version='2.13', collection_name='ansible.builtin')
elif provider == 'selfsigned': elif provider == 'selfsigned':
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None:
module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.')
@ -2702,7 +2702,8 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15') module.fail_json(msg='You need to have PyOpenSSL>=0.15')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.13', collection_name='ansible.builtin')
if provider == 'selfsigned': if provider == 'selfsigned':
certificate = SelfSignedCertificate(module) certificate = SelfSignedCertificate(module)
elif provider == 'acme': elif provider == 'acme':

@ -845,7 +845,8 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15') module.fail_json(msg='You need to have PyOpenSSL>=0.15')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.13', collection_name='ansible.builtin')
certificate = CertificateInfoPyOpenSSL(module) certificate = CertificateInfoPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

@ -1091,7 +1091,8 @@ def main():
if module.params['version'] != 1: if module.params['version'] != 1:
module.deprecate('The version option will only support allowed values from Ansible 2.14 on. ' module.deprecate('The version option will only support allowed values from Ansible 2.14 on. '
'Currently, only the value 1 is allowed by RFC 2986', version='ansible.builtin:2.14') 'Currently, only the value 1 is allowed by RFC 2986',
version='2.14', collection_name='ansible.builtin')
base_dir = os.path.dirname(module.params['path']) or '.' base_dir = os.path.dirname(module.params['path']) or '.'
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):
@ -1125,7 +1126,8 @@ def main():
except AttributeError: except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs') module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.13', collection_name='ansible.builtin')
csr = CertificateSigningRequestPyOpenSSL(module) csr = CertificateSigningRequestPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

@ -908,7 +908,8 @@ def main():
if not PYOPENSSL_FOUND: if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR) exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='ansible.builtin:2.13') module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.13', collection_name='ansible.builtin')
private_key = PrivateKeyPyOpenSSL(module) private_key = PrivateKeyPyOpenSSL(module)
elif backend == 'cryptography': elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:

@ -118,7 +118,8 @@ def main():
supports_check_mode=True, supports_check_mode=True,
) )
if module._name == 'python_requirements_facts': if module._name == 'python_requirements_facts':
module.deprecate("The 'python_requirements_facts' module has been renamed to 'python_requirements_info'", version='ansible.builtin:2.13') module.deprecate("The 'python_requirements_facts' module has been renamed to 'python_requirements_info'",
version='2.13', collection_name='ansible.builtin')
if not HAS_DISTUTILS: if not HAS_DISTUTILS:
module.fail_json( module.fail_json(
msg='Could not import "distutils" and "pkg_resources" libraries to introspect python environment.', msg='Could not import "distutils" and "pkg_resources" libraries to introspect python environment.',

@ -886,7 +886,7 @@ def main():
# TODO: Remove this in Ansible v2.12 (and reinstate strict parameter test above) and remove the integration test example # TODO: Remove this in Ansible v2.12 (and reinstate strict parameter test above) and remove the integration test example
if content == 'attribute' and attribute is not None: if content == 'attribute' and attribute is not None:
module.deprecate("Parameter 'attribute=%s' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." % attribute, module.deprecate("Parameter 'attribute=%s' is ignored when using 'content=attribute' only 'xpath' is used. Please remove entry." % attribute,
'ansible.builtin:2.12') '2.12', collection_name='ansible.builtin')
# Check if the file exists # Check if the file exists
if xml_string: if xml_string:

@ -22,23 +22,31 @@ def test_warn(am, capfd):
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
def test_deprecate(am, capfd): def test_deprecate(am, capfd):
am.deprecate('deprecation1') am.deprecate('deprecation1')
am.deprecate('deprecation2', 'ansible.builtin:2.3') am.deprecate('deprecation2', '2.3') # pylint: disable=ansible-deprecated-no-collection-name
am.deprecate('deprecation3', version='ansible.builtin:2.4') am.deprecate('deprecation3', version='2.4') # pylint: disable=ansible-deprecated-no-collection-name
am.deprecate('deprecation4', date='ansible.builtin:2020-03-10') am.deprecate('deprecation4', date='2020-03-10') # pylint: disable=ansible-deprecated-no-collection-name
am.deprecate('deprecation5', collection_name='ansible.builtin')
am.deprecate('deprecation6', '2.3', collection_name='ansible.builtin')
am.deprecate('deprecation7', version='2.4', collection_name='ansible.builtin')
am.deprecate('deprecation8', date='2020-03-10', collection_name='ansible.builtin')
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
am.exit_json(deprecations=['deprecation5', ('deprecation6', 'ansible.builtin:2.4')]) am.exit_json(deprecations=['deprecation9', ('deprecation10', '2.4')])
out, err = capfd.readouterr() out, err = capfd.readouterr()
output = json.loads(out) output = json.loads(out)
assert ('warnings' not in output or output['warnings'] == []) assert ('warnings' not in output or output['warnings'] == [])
assert output['deprecations'] == [ assert output['deprecations'] == [
{u'msg': u'deprecation1', u'version': None}, {u'msg': u'deprecation1', u'version': None, u'collection_name': None},
{u'msg': u'deprecation2', u'version': 'ansible.builtin:2.3'}, {u'msg': u'deprecation2', u'version': '2.3', u'collection_name': None},
{u'msg': u'deprecation3', u'version': 'ansible.builtin:2.4'}, {u'msg': u'deprecation3', u'version': '2.4', u'collection_name': None},
{u'msg': u'deprecation4', u'date': 'ansible.builtin:2020-03-10'}, {u'msg': u'deprecation4', u'date': '2020-03-10', u'collection_name': None},
{u'msg': u'deprecation5', u'version': None}, {u'msg': u'deprecation5', u'version': None, u'collection_name': 'ansible.builtin'},
{u'msg': u'deprecation6', u'version': 'ansible.builtin:2.4'}, {u'msg': u'deprecation6', u'version': '2.3', u'collection_name': 'ansible.builtin'},
{u'msg': u'deprecation7', u'version': '2.4', u'collection_name': 'ansible.builtin'},
{u'msg': u'deprecation8', u'date': '2020-03-10', u'collection_name': 'ansible.builtin'},
{u'msg': u'deprecation9', u'version': None, u'collection_name': None},
{u'msg': u'deprecation10', u'version': '2.4', u'collection_name': None},
] ]
@ -51,7 +59,7 @@ def test_deprecate_without_list(am, capfd):
output = json.loads(out) output = json.loads(out)
assert ('warnings' not in output or output['warnings'] == []) assert ('warnings' not in output or output['warnings'] == [])
assert output['deprecations'] == [ assert output['deprecations'] == [
{u'msg': u'Simple deprecation warning', u'version': None}, {u'msg': u'Simple deprecation warning', u'version': None, u'collection_name': None},
] ]

@ -16,20 +16,50 @@ from ansible.module_utils.six import PY3
@pytest.fixture @pytest.fixture
def deprecation_messages(): def deprecation_messages():
return [ return [
{'msg': 'First deprecation', 'version': None}, {'msg': 'First deprecation', 'version': None, 'collection_name': None},
{'msg': 'Second deprecation', 'version': '2.14'}, {'msg': 'Second deprecation', 'version': None, 'collection_name': 'ansible.builtin'},
{'msg': 'Third deprecation', 'version': '2.9'}, {'msg': 'Third deprecation', 'version': '2.14', 'collection_name': None},
{'msg': 'Fourth deprecation', 'version': '2.9', 'collection_name': None},
{'msg': 'Fifth deprecation', 'version': '2.9', 'collection_name': 'ansible.builtin'},
{'msg': 'Sixth deprecation', 'date': '2199-12-31', 'collection_name': None},
{'msg': 'Seventh deprecation', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'},
] ]
def test_deprecate_message_only(): def test_deprecate_message_only():
deprecate('Deprecation message') deprecate('Deprecation message')
assert warnings._global_deprecations == [{'msg': 'Deprecation message', 'version': None}] assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'version': None, 'collection_name': None}]
def test_deprecate_with_collection():
deprecate(msg='Deprecation message', collection_name='ansible.builtin')
assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'version': None, 'collection_name': 'ansible.builtin'}]
def test_deprecate_with_version(): def test_deprecate_with_version():
deprecate(msg='Deprecation message', version='2.14') deprecate(msg='Deprecation message', version='2.14')
assert warnings._global_deprecations == [{'msg': 'Deprecation message', 'version': '2.14'}] assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'version': '2.14', 'collection_name': None}]
def test_deprecate_with_version_and_collection():
deprecate(msg='Deprecation message', version='2.14', collection_name='ansible.builtin')
assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'version': '2.14', 'collection_name': 'ansible.builtin'}]
def test_deprecate_with_date():
deprecate(msg='Deprecation message', date='2199-12-31')
assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': None}]
def test_deprecate_with_date_and_collection():
deprecate(msg='Deprecation message', date='2199-12-31', collection_name='ansible.builtin')
assert warnings._global_deprecations == [
{'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'}]
def test_multiple_deprecations(deprecation_messages): def test_multiple_deprecations(deprecation_messages):
@ -45,7 +75,7 @@ def test_get_deprecation_messages(deprecation_messages):
accessor_deprecations = get_deprecation_messages() accessor_deprecations = get_deprecation_messages()
assert isinstance(accessor_deprecations, tuple) assert isinstance(accessor_deprecations, tuple)
assert len(accessor_deprecations) == 3 assert len(accessor_deprecations) == 7
@pytest.mark.parametrize( @pytest.mark.parametrize(

Loading…
Cancel
Save