From 22568305d60a833b1163e636863cc0c3eed5145a Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 18 Oct 2023 01:05:21 +1000 Subject: [PATCH] Add ignore invalid options override for mod wrapper (#81899) Adds an option that can have an action plugin tell the module to ignore options that do not fit its arg spec. This is to enable support for core running modules that exist outside of the collection that may not be new enough to support some of the options supplied to it. --- .../module-ignore-unknown-options.yml | 6 ++++ lib/ansible/module_utils/basic.py | 2 ++ lib/ansible/module_utils/common/parameters.py | 3 +- .../module_utils/csharp/Ansible.Basic.cs | 6 ++-- lib/ansible/plugins/action/__init__.py | 15 +++++++--- .../library/ansible_basic_tests.ps1 | 28 +++++++++++++++++++ .../module_utils/basic/test_argument_spec.py | 2 ++ 7 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/module-ignore-unknown-options.yml 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 = (