diff --git a/changelogs/fragments/module-ignore-unknown-options.yml b/changelogs/fragments/module-ignore-unknown-options.yml new file mode 100644 index 00000000000..c2d380f0901 --- /dev/null +++ b/changelogs/fragments/module-ignore-unknown-options.yml @@ -0,0 +1,6 @@ +minor_changes: +- >- + modules - Add the ability for an action plugin to call ``self._execute_module(*, ignore_unknown_opts=True)`` to execute a module with options that may not be + supported for the version being called. This tells the module basic wrapper to ignore validating the options provided match the arg spec. +bugfixes: +- fetch - Do not calculate the file size for Windows fetch targets to improve performance. diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 604b69b35ec..9cfe32e56f0 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -480,6 +480,8 @@ class AnsibleModule(object): try: error = self.validation_result.errors[0] + if isinstance(error, UnsupportedError) and self._ignore_unknown_opts: + error = None except IndexError: error = None diff --git a/lib/ansible/module_utils/common/parameters.py b/lib/ansible/module_utils/common/parameters.py index b6200749934..fb61bd38c49 100644 --- a/lib/ansible/module_utils/common/parameters.py +++ b/lib/ansible/module_utils/common/parameters.py @@ -87,6 +87,7 @@ PASS_VARS = { 'debug': ('_debug', False), 'diff': ('_diff', False), 'keep_remote_files': ('_keep_remote_files', False), + 'ignore_unknown_opts': ('_ignore_unknown_opts', False), 'module_name': ('_name', None), 'no_log': ('no_log', False), 'remote_tmp': ('_remote_tmp', None), @@ -100,7 +101,7 @@ PASS_VARS = { 'version': ('ansible_version', '0.0'), } -PASS_BOOLS = ('check_mode', 'debug', 'diff', 'keep_remote_files', 'no_log') +PASS_BOOLS = ('check_mode', 'debug', 'diff', 'keep_remote_files', 'ignore_unknown_opts', 'no_log') DEFAULT_TYPE_VALIDATORS = { 'str': check_type_str, diff --git a/lib/ansible/module_utils/csharp/Ansible.Basic.cs b/lib/ansible/module_utils/csharp/Ansible.Basic.cs index 97f5f3e2d78..120536331bc 100644 --- a/lib/ansible/module_utils/csharp/Ansible.Basic.cs +++ b/lib/ansible/module_utils/csharp/Ansible.Basic.cs @@ -49,6 +49,7 @@ namespace Ansible.Basic private static List BOOLEANS_TRUE = new List() { "y", "yes", "on", "1", "true", "t", "1.0" }; private static List BOOLEANS_FALSE = new List() { "n", "no", "off", "0", "false", "f", "0.0" }; + private bool ignoreUnknownOpts = false; private string remoteTmp = Path.GetTempPath(); private string tmpdir = null; private HashSet noLogValues = new HashSet(); @@ -64,6 +65,7 @@ namespace Ansible.Basic { "debug", "DebugMode" }, { "diff", "DiffMode" }, { "keep_remote_files", "KeepRemoteFiles" }, + { "ignore_unknown_opts", "ignoreUnknownOpts" }, { "module_name", "ModuleName" }, { "no_log", "NoLog" }, { "remote_tmp", "remoteTmp" }, @@ -76,7 +78,7 @@ namespace Ansible.Basic { "verbosity", "Verbosity" }, { "version", "AnsibleVersion" }, }; - private List passBools = new List() { "check_mode", "debug", "diff", "keep_remote_files", "no_log" }; + private List passBools = new List() { "check_mode", "debug", "diff", "keep_remote_files", "ignore_unknown_opts", "no_log" }; private List passInts = new List() { "verbosity" }; private Dictionary> specDefaults = new Dictionary>() { @@ -1043,7 +1045,7 @@ namespace Ansible.Basic foreach (string parameter in removedParameters) param.Remove(parameter); - if (unsupportedParameters.Count > 0) + if (unsupportedParameters.Count > 0 && !ignoreUnknownOpts) { legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", ""))); string msg = String.Format("Unsupported parameters for ({0}) module: {1}", ModuleName, String.Join(", ", unsupportedParameters)); diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 9f7fac1b0b8..b557e3b68cc 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -849,10 +849,13 @@ class ActionBase(ABC): path=path, follow=follow, get_checksum=checksum, + get_size=False, # ansible.windows.win_stat added this in 1.11.0 checksum_algorithm='sha1', ) + # Unknown opts are ignored as module_args could be specific for the + # module that is being executed. mystat = self._execute_module(module_name='ansible.legacy.stat', module_args=module_args, task_vars=all_vars, - wrap_async=False) + wrap_async=False, ignore_unknown_opts=True) if mystat.get('failed'): msg = mystat.get('module_stderr') @@ -936,7 +939,7 @@ class ActionBase(ABC): data = re.sub(r'^((\r)?\n)?BECOME-SUCCESS.*(\r)?\n', '', data) return data - def _update_module_args(self, module_name, module_args, task_vars): + def _update_module_args(self, module_name, module_args, task_vars, ignore_unknown_opts: bool = False): # set check mode in the module arguments, if required if self._task.check_mode: @@ -994,7 +997,11 @@ class ActionBase(ABC): # make sure the remote_tmp value is sent through in case modules needs to create their own module_args['_ansible_remote_tmp'] = self.get_shell_option('remote_tmp', default='~/.ansible/tmp') - def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=None, wrap_async=False): + # tells the module to ignore options that are not in its argspec. + module_args['_ansible_ignore_unknown_opts'] = ignore_unknown_opts + + def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=None, wrap_async=False, + ignore_unknown_opts: bool = False): ''' Transfer and run a module along with its arguments. ''' @@ -1030,7 +1037,7 @@ class ActionBase(ABC): if module_args is None: module_args = self._task.args - self._update_module_args(module_name, module_args, task_vars) + self._update_module_args(module_name, module_args, task_vars, ignore_unknown_opts=ignore_unknown_opts) remove_async_dir = None if wrap_async or self._task.async_val: diff --git a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 index 9644df93c02..df8bf8db794 100644 --- a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 +++ b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 @@ -2253,6 +2253,34 @@ test_no_log - Invoked with: $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args } } + "Unsupported options with ignore" = { + $spec = @{ + options = @{ + option_key = @{ + type = "str" + } + } + } + Set-Variable -Name complex_args -Scope Global -Value @{ + option_key = "abc" + invalid_key = "def" + another_key = "ghi" + _ansible_ignore_unknown_opts = $true + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + $m.Params | Assert-DictionaryEqual -Expected @{ option_key = "abc"; invalid_key = "def"; another_key = "ghi" } + try { + $m.ExitJson() + } + catch [System.Management.Automation.RuntimeException] { + $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output) + } + $output.Keys.Count | Assert-Equal -Expected 2 + $output.changed | Assert-Equal -Expected $false + $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "abc"; invalid_key = "def"; another_key = "ghi" } } + } + "Check mode and module doesn't support check mode" = { $spec = @{ options = @{ diff --git a/test/units/module_utils/basic/test_argument_spec.py b/test/units/module_utils/basic/test_argument_spec.py index ac5a4dfb49e..28568b80af5 100644 --- a/test/units/module_utils/basic/test_argument_spec.py +++ b/test/units/module_utils/basic/test_argument_spec.py @@ -66,6 +66,8 @@ VALID_SPECS = ( ({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': [42, 32]}, ['42', '32']), # parameter is required ({'arg': {'required': True}}, {'arg': 42}, '42'), + # ignored unknown parameters + ({'arg': {'type': 'int'}}, {'arg': 1, 'invalid': True, '_ansible_ignore_unknown_opts': True}, 1), ) INVALID_SPECS = (