diff --git a/changelogs/fragments/3478-yaml-callback.yml b/changelogs/fragments/3478-yaml-callback.yml new file mode 100644 index 0000000000..ec1801beaa --- /dev/null +++ b/changelogs/fragments/3478-yaml-callback.yml @@ -0,0 +1,2 @@ +bugfixes: + - "yaml callback plugin - avoid modifying PyYAML so that other plugins using it on the controller, like the ``to_yaml`` filter, do not produce different output (https://github.com/ansible-collections/community.general/issues/3471, https://github.com/ansible-collections/community.general/pull/3478)." diff --git a/plugins/callback/yaml.py b/plugins/callback/yaml.py index 24140116ed..59fb350934 100644 --- a/plugins/callback/yaml.py +++ b/plugins/callback/yaml.py @@ -42,28 +42,29 @@ def should_use_block(value): return False -def my_represent_scalar(self, tag, value, style=None): - """Uses block style for multi-line strings""" - if style is None: - if should_use_block(value): - style = '|' - # we care more about readable than accuracy, so... - # ...no trailing space - value = value.rstrip() - # ...and non-printable characters - value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0) - # ...tabs prevent blocks from expanding - value = value.expandtabs() - # ...and odd bits of whitespace - value = re.sub(r'[\x0b\x0c\r]', '', value) - # ...as does trailing space - value = re.sub(r' +\n', '\n', value) - else: - style = self.default_style - node = yaml.representer.ScalarNode(tag, value, style=style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - return node +class MyDumper(AnsibleDumper): + def represent_scalar(self, tag, value, style=None): + """Uses block style for multi-line strings""" + if style is None: + if should_use_block(value): + style = '|' + # we care more about readable than accuracy, so... + # ...no trailing space + value = value.rstrip() + # ...and non-printable characters + value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0) + # ...tabs prevent blocks from expanding + value = value.expandtabs() + # ...and odd bits of whitespace + value = re.sub(r'[\x0b\x0c\r]', '', value) + # ...as does trailing space + value = re.sub(r' +\n', '\n', value) + else: + style = self.default_style + node = yaml.representer.ScalarNode(tag, value, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node class CallbackModule(Default): @@ -79,7 +80,6 @@ class CallbackModule(Default): def __init__(self): super(CallbackModule, self).__init__() - yaml.representer.BaseRepresenter.represent_scalar = my_represent_scalar def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False): if result.get('_ansible_no_log', False): @@ -121,7 +121,7 @@ class CallbackModule(Default): if abridged_result: dumped += '\n' - dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False)) + dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=MyDumper, default_flow_style=False)) # indent by a couple of spaces dumped = '\n '.join(dumped.split('\n')).rstrip() diff --git a/tests/integration/targets/callback_yaml/tasks/main.yml b/tests/integration/targets/callback_yaml/tasks/main.yml index 21b43717f5..a764f44a6a 100644 --- a/tests/integration/targets/callback_yaml/tasks/main.yml +++ b/tests/integration/targets/callback_yaml/tasks/main.yml @@ -58,3 +58,40 @@ "PLAY RECAP *********************************************************************", "testhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 " ] + - name: Test to_yaml + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.yaml + playbook: | + - hosts: testhost + gather_facts: false + vars: + data: | + line 1 + line 2 + line 3 + tasks: + - name: Test to_yaml + debug: + msg: "{{ '{{' }}'{{ '{{' }}'{{ '}}' }} data | to_yaml {{ '{{' }}'{{ '}}' }}'{{ '}}' }}" + # The above should be: msg: "{{ data | to_yaml }}" + # Unfortunately, the way Ansible handles templating, we need to do some funny 'escaping' tricks... + expected_output: [ + "", + "PLAY [testhost] ****************************************************************", + "", + "TASK [Test to_yaml] ************************************************************", + "ok: [testhost] => ", + " msg: |-", + " 'line 1", + " ", + " line 2", + " ", + " line 3", + " ", + " '", + "", + "PLAY RECAP *********************************************************************", + "testhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 " + ]