From 4c27ccf8f4fdc915203db660fbbbd453f5ec5739 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 3 Sep 2025 23:17:50 +0200 Subject: [PATCH] default callback: allow to configure indentation (#85497) * Comment on PyYAML's behavior if indentation is set to 1, or to 10+. --- .../85497-default-callback-indent.yml | 2 + lib/ansible/plugins/callback/__init__.py | 16 ++- .../doc_fragments/result_format_callback.py | 15 ++ ...ult.out.result_format_yaml_indent_2.stderr | 2 + ...ult.out.result_format_yaml_indent_2.stdout | 132 ++++++++++++++++++ .../targets/callback_default/runme.sh | 3 + 6 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/85497-default-callback-indent.yml create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stdout diff --git a/changelogs/fragments/85497-default-callback-indent.yml b/changelogs/fragments/85497-default-callback-indent.yml new file mode 100644 index 00000000000..624c851f5a8 --- /dev/null +++ b/changelogs/fragments/85497-default-callback-indent.yml @@ -0,0 +1,2 @@ +minor_changes: + - "default callback plugin - add option to configure indentation for JSON and YAML output (https://github.com/ansible/ansible/pull/85497)." diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index ea675ee444f..944606f26ff 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -290,7 +290,11 @@ class CallbackBase(AnsiblePlugin): ) if not indent and any(indent_conditions): - indent = 4 + try: + indent = self.get_option('result_indentation') + except KeyError: + # Callback does not declare result_indentation nor extend result_format_callback + indent = 4 if pretty_results is False: # pretty_results=False overrides any specified indentation indent = None @@ -391,8 +395,14 @@ class CallbackBase(AnsiblePlugin): # Callback does not declare pretty_results nor extend result_format_callback pretty_results = None + try: + indent = self.get_option('result_indentation') + except KeyError: + # Callback does not declare result_indentation nor extend result_format_callback + indent = 4 + if result_format == 'json': - return json.dumps(diff, sort_keys=True, indent=4, separators=(u',', u': ')) + u'\n' + return json.dumps(diff, sort_keys=True, indent=indent, separators=(u',', u': ')) + u'\n' if result_format == 'yaml': # None is a sentinel in this case that indicates default behavior @@ -404,7 +414,7 @@ class CallbackBase(AnsiblePlugin): allow_unicode=True, Dumper=functools.partial(_AnsibleCallbackDumper, lossy=lossy), default_flow_style=False, - indent=4, + indent=indent, # sort_keys=sort_keys # This requires PyYAML>=5.1 ), ' ' diff --git a/lib/ansible/plugins/doc_fragments/result_format_callback.py b/lib/ansible/plugins/doc_fragments/result_format_callback.py index 88e37b8c344..220a311bab0 100644 --- a/lib/ansible/plugins/doc_fragments/result_format_callback.py +++ b/lib/ansible/plugins/doc_fragments/result_format_callback.py @@ -26,6 +26,21 @@ class ModuleDocFragment(object): - json - yaml version_added: '2.13' + result_indentation: + name: Indentation of the result + description: + - Allows to configure indentation for YAML and verbose/pretty JSON. + - Please note that for O(result_format=yaml), only values between 2 and 9 will be handled as expected by PyYAML. + If indentation is set to 1, or to 10 or larger, the first level of indentation will be used, + but all further indentations will be by 2 spaces. + type: int + default: 4 + env: + - name: ANSIBLE_CALLBACK_RESULT_INDENTATION + ini: + - key: callback_result_indentation + section: defaults + version_added: '2.20' pretty_results: name: Configure output for readability description: diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stderr b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stderr new file mode 100644 index 00000000000..d3e07d472db --- /dev/null +++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stderr @@ -0,0 +1,2 @@ ++ ansible-playbook -i inventory test.yml +++ set +x diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stdout new file mode 100644 index 00000000000..50743cc6d7c --- /dev/null +++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_indent_2.stdout @@ -0,0 +1,132 @@ + +PLAY [testhost] **************************************************************** + +TASK [Changed task] ************************************************************ +changed: [testhost] + +TASK [Ok task] ***************************************************************** +ok: [testhost] + +TASK [Failed task] ************************************************************* +[ERROR]: Task failed: Action failed: no reason +Origin: TEST_PATH/test.yml:16:7 + +14 changed_when: false +15 +16 - name: Failed task + ^ column 7 + +fatal: [testhost]: FAILED! => + changed: false + msg: no reason +...ignoring + +TASK [Skipped task] ************************************************************ +skipping: [testhost] + +TASK [Task with var in name (foo bar)] ***************************************** +changed: [testhost] + +TASK [Loop task] *************************************************************** +changed: [testhost] => (item=foo-1) +changed: [testhost] => (item=foo-2) +changed: [testhost] => (item=foo-3) + +TASK [debug loop] ************************************************************** +changed: [testhost] => (item=debug-1) => + msg: debug-1 +[ERROR]: Task failed: Action failed: debug-2 +Origin: TEST_PATH/test.yml:38:7 + +36 +37 # detect "changed" debug tasks being hidden with display_ok_tasks=false +38 - name: debug loop + ^ column 7 + +failed: [testhost] (item=debug-2) => + msg: debug-2 +ok: [testhost] => (item=debug-3) => + msg: debug-3 +skipping: [testhost] => (item=debug-4) +fatal: [testhost]: FAILED! => + msg: One or more items failed +...ignoring + +TASK [EXPECTED FAILURE Failed task to be rescued] ****************************** +[ERROR]: Task failed: Action failed: Failed as requested from task +Origin: TEST_PATH/test.yml:54:11 + +52 +53 - block: +54 - name: EXPECTED FAILURE Failed task to be rescued + ^ column 11 + +fatal: [testhost]: FAILED! => + changed: false + msg: Failed as requested from task + +TASK [Rescue task] ************************************************************* +changed: [testhost] + +TASK [include_tasks] *********************************************************** +included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1) + +TASK [debug] ******************************************************************* +ok: [testhost] => + item: 1 + +TASK [copy] ******************************************************************** +changed: [testhost] + +TASK [replace] ***************************************************************** +--- before: .../test_diff.txt ++++ after: .../test_diff.txt +@@ -1 +1 @@ +-foo +\ No newline at end of file ++bar +\ No newline at end of file + +changed: [testhost] + +TASK [replace] ***************************************************************** +ok: [testhost] + +TASK [debug] ******************************************************************* +skipping: [testhost] + +TASK [debug] ******************************************************************* +skipping: [testhost] + +TASK [debug] ******************************************************************* +skipping: [testhost] => (item=1) +skipping: [testhost] => (item=2) +skipping: [testhost] + +RUNNING HANDLER [Test handler 1] *********************************************** +changed: [testhost] + +RUNNING HANDLER [Test handler 2] *********************************************** +ok: [testhost] + +RUNNING HANDLER [Test handler 3] *********************************************** +changed: [testhost] + +PLAY [testhost] **************************************************************** + +TASK [First free task] ********************************************************* +changed: [testhost] + +TASK [Second free task] ******************************************************** +changed: [testhost] + +TASK [Include some tasks] ****************************************************** +included: .../test/integration/targets/callback_default/include_me.yml for testhost => (item=1) + +TASK [debug] ******************************************************************* +ok: [testhost] => + item: 1 + +PLAY RECAP ********************************************************************* +testhost : ok=19 changed=11 unreachable=0 failed=0 skipped=4 rescued=1 ignored=2 + diff --git a/test/integration/targets/callback_default/runme.sh b/test/integration/targets/callback_default/runme.sh index 473518b4af9..9936b9ad664 100755 --- a/test/integration/targets/callback_default/runme.sh +++ b/test/integration/targets/callback_default/runme.sh @@ -203,7 +203,10 @@ export ANSIBLE_DISPLAY_FAILED_STDERR=0 export ANSIBLE_CALLBACK_RESULT_FORMAT=yaml run_test result_format_yaml test.yml +export ANSIBLE_CALLBACK_RESULT_INDENTATION=2 +run_test result_format_yaml_indent_2 test.yml export ANSIBLE_CALLBACK_RESULT_FORMAT=json +unset ANSIBLE_CALLBACK_RESULT_INDENTATION export ANSIBLE_CALLBACK_RESULT_FORMAT=yaml export ANSIBLE_CALLBACK_FORMAT_PRETTY=1