Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid internal error when validating argspec #81631

Open
wants to merge 3 commits into
base: devel
Choose a base branch
from

Conversation

felixfontein
Copy link
Contributor

SUMMARY

Reported in ansible-collections/community.docker#680.

When passing a boolean or number to a parameter that expects a dictionary, there is an exception in the fallback handling since it assumes that the value is a dictionary.

Fixes this and adds a regression test.

ISSUE TYPE
  • Bugfix Pull Request

@ansibot ansibot added bug This issue/PR relates to a bug. needs_triage Needs a first human triage before being processed. needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. labels Sep 5, 2023
@mkrizek mkrizek removed the needs_triage Needs a first human triage before being processed. label Sep 5, 2023
@@ -838,6 +838,9 @@ def env_fallback(*args, **kwargs):

def set_fallbacks(argument_spec, parameters):
no_log_values = set()
if not isinstance(parameters, Mapping):
# The wrong type is handled somewhere else
return no_log_values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be silently ignored. It should be an error at "somewhere else" or here directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should raise a TypeError if any of the arguments is invalid. But yeah, the input validation must happen in the outer layer, in the calling code somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the caller checked for the right type, but only after calling this function. I've switched the order.

@webknjaz webknjaz added the ci_verified Changes made in this PR are causing tests to fail. label Sep 5, 2023
@felixfontein
Copy link
Contributor Author

The reason that CI fails is that the last task in the no_log_suboptions_invalid.yml test for no_log has been broken for some time. The reason why it didn't contain any of the magic values is that it results in

TASK [Task with suboptions with incorrect data type] ***************************
task path: /path/to/ansible/test/results/.tmp/integration/no_log-xi42t4h5-ÅÑŚÌβŁÈ/test/integration/targets/no_log/no_log_suboptions_invalid.yml:39
Using module file /path/to/ansible/test/results/.tmp/integration/no_log-xi42t4h5-ÅÑŚÌβŁÈ/test/integration/targets/no_log/library/module.py
Pipelining is enabled.
<testhost> ESTABLISH LOCAL CONNECTION FOR USER: felix
<testhost> EXEC /bin/sh -c '/usr/bin/python && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "<stdin>", line 121, in <module>
  File "<stdin>", line 113, in _ansiballz_main
  File "<stdin>", line 61, in invoke_module
  File "<frozen runpy>", line 226, in run_module
  File "<frozen runpy>", line 98, in _run_module_code
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/modules/module.py", line 45, in <module>
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/modules/module.py", line 13, in main
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/basic.py", line 488, in __init__
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/arg_spec.py", line 301, in validate
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/arg_spec.py", line 252, in validate
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/parameters.py", line 751, in _validate_sub_spec
  File "/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/parameters.py", line 846, in set_fallbacks
TypeError: argument of type 'float' is not iterable
fatal: [testhost]: FAILED! => {
    "changed": false,
    "module_stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 121, in <module>\n  File \"<stdin>\", line 113, in _ansiballz_main\n  File \"<stdin>\", line 61, in invoke_module\n  File \"<frozen runpy>\", line 226, in run_module\n  File \"<frozen runpy>\", line 98, in _run_module_code\n  File \"<frozen runpy>\", line 88, in _run_code\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/modules/module.py\", line 45, in <module>\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/modules/module.py\", line 13, in main\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/basic.py\", line 488, in __init__\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/arg_spec.py\", line 301, in validate\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/arg_spec.py\", line 252, in validate\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/parameters.py\", line 751, in _validate_sub_spec\n  File \"/tmp/ansible_module_payload_u2tic5t9/ansible_module_payload.zip/ansible/module_utils/common/parameters.py\", line 846, in set_fallbacks\nTypeError: argument of type 'float' is not iterable\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}
...ignoring

That no longer happens since this PR fixes that crash, now it results in

TASK [Task with suboptions with incorrect data type] ***************************
task path: /path/to/ansible/test/results/.tmp/integration/no_log-j1u1nw6b-ÅÑŚÌβŁÈ/test/integration/targets/no_log/no_log_suboptions_invalid.yml:39
Using module file /path/to/ansible/test/results/.tmp/integration/no_log-j1u1nw6b-ÅÑŚÌβŁÈ/test/integration/targets/no_log/library/module.py
Pipelining is enabled.
<testhost> ESTABLISH LOCAL CONNECTION FOR USER: felix
<testhost> EXEC /bin/sh -c '/usr/bin/python && sleep 0'
fatal: [testhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "secret": "AGROUND",
            "state": null,
            "subopt_dict": 9068.21361,
            "subopt_list": [
                {
                    "subopt1": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                    "subopt2": null
                },
                {
                    "subopt1": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                    "subopt2": null
                }
            ]
        }
    },
    "msg": "Value '9068.21361' in the sub parameter field 'subopt_dict' must by a dict, not 'float'"
}
...ignoring

which contains the secret value AGROUND. This is another unrelated bug exposed by this bugfix.

@felixfontein
Copy link
Contributor Author

The reason why AGROUND is not removed from the output is that _list_no_log_values also does validation, and raises exceptions instead of completing its job. This shows that raising exceptions instead of ignoring errors can be extremely dangerous.

I'll push a change that disables the broken test once CI completes. This should be fixed in another PR. If you want to keep the double type checking with raising TypeErrors in _list_no_log_values you have to handle no_log somehow differently.

@ansibot ansibot removed ci_verified Changes made in this PR are causing tests to fail. needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. labels Sep 6, 2023
Comment on lines +46 to +48
when: false # TODO FIXME: The code that should prevent 'AGROUND' ending up in the output
# has been broken for some time; its misbehavior was masked by
# the bug fixed in https://github.com/ansible/ansible/pull/81631.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the underlying bug is in the outer validate method with how we use _list_no_log_values. An error anywhere in the spec prevents marking any of the options as no_log (and we stop at the first error). Maybe we could accumulate the errors non-fatally the first time.

diff --git a/lib/ansible/module_utils/common/arg_spec.py b/lib/ansible/module_utils/common/arg_spec.py
index d9f716efce..44548c71ec 100644
--- a/lib/ansible/module_utils/common/arg_spec.py
+++ b/lib/ansible/module_utils/common/arg_spec.py
@@ -201,10 +201,10 @@ class ArgumentSpecValidator:
                 'collection_name': deprecation.get('collection_name'),
             })
 
-        try:
-            result._no_log_values.update(_list_no_log_values(self.argument_spec, result._validated_parameters))
-        except TypeError as te:
-            result.errors.append(NoLogError(to_native(te)))
+        no_log_introspection_errors = []
+        result._no_log_values.update(_list_no_log_values(self.argument_spec, result._validated_parameters, no_log_introspection_errors))
+        for error in no_log_introspection_errors:
+            result.errors.append(NoLogError(error))
 
         try:
             result._deprecations.extend(_list_deprecations(self.argument_spec, result._validated_parameters))
diff --git a/lib/ansible/module_utils/common/parameters.py b/lib/ansible/module_utils/common/parameters.py
index 386eb875ce..4e146683c5 100644
--- a/lib/ansible/module_utils/common/parameters.py
+++ b/lib/ansible/module_utils/common/parameters.py
@@ -305,7 +305,7 @@ def _list_deprecations(argument_spec, parameters, prefix=''):
     return deprecations
 
 
-def _list_no_log_values(argument_spec, params):
+def _list_no_log_values(argument_spec, params, errors=None):
     """Return set of no log values
 
     :arg argument_spec: An argument spec dictionary
@@ -324,6 +324,9 @@ def _list_no_log_values(argument_spec, params):
                 try:
                     no_log_values.update(_return_datastructure_name(no_log_object))
                 except TypeError as e:
+                    if errors is not None:
+                        errors.append('Failed to convert "%s": %s' % (arg_name, to_native(e)))
+                        continue
                     raise TypeError('Failed to convert "%s": %s' % (arg_name, to_native(e)))
 
         # Get no_log values from suboptions
@@ -342,9 +345,19 @@ def _list_no_log_values(argument_spec, params):
@@ -342,9 +345,19 @@ def _list_no_log_values(argument_spec, params):
                         # Validate dict fields in case they came in as strings
 
                         if isinstance(sub_param, string_types):
-                            sub_param = check_type_dict(sub_param)
+                            try:
+                                sub_param = check_type_dict(sub_param)
+                            except TypeError as e:
+                                if errors is not None:
+                                    errors.append(to_native(e))
+                                    continue
+                                raise
 
                         if not isinstance(sub_param, Mapping):
+                            if errors is not None:
+                                errors.append("Value '{1}' in the sub parameter field '{0}' must by a {2}, "
+                                              "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type))
+                                continue
                             raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, "
                                             "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type))

We also call _list_no_log_values in _validate_sub_spec, but since _list_no_log_values is recursive I don't think that's necessary if the outer scope is fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#81672 contains a slightly modified version of your patch.

@s-hertel
Copy link
Contributor

s-hertel commented Sep 8, 2023

I'll push a change that disables the broken test once CI completes. This should be fixed in another PR.

I think we should fix that first then.

@felixfontein
Copy link
Contributor Author

I'll revert 1e61657 once #81672 or something similar has been merged.

@ansibot ansibot added the stale_ci This PR has been tested by CI more than one week ago. Close and re-open this PR to get it retested. label Sep 16, 2023
@nitzmahone nitzmahone self-assigned this May 1, 2024
@nitzmahone
Copy link
Member

2.18 task- there are several related things that could use some attention, and some we've already fixed in other branches

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue/PR relates to a bug. stale_ci This PR has been tested by CI more than one week ago. Close and re-open this PR to get it retested.
Projects
ansible-core 2.18
  
Awaiting triage
Development

Successfully merging this pull request may close these issues.

None yet

6 participants