1# (c) 2017 Ansible Project
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4# Make coding more python3-ish
5from __future__ import (absolute_import, division, print_function)
6__metaclass__ = type
7
8DOCUMENTATION = '''
9    callback: yaml
10    type: stdout
11    short_description: yaml-ized Ansible screen output
12    version_added: 2.5
13    description:
14        - Ansible output that can be quite a bit easier to read than the
15          default JSON formatting.
16    extends_documentation_fragment:
17      - default_callback
18    requirements:
19      - set as stdout in configuration
20'''
21
22import yaml
23import json
24import re
25import string
26import sys
27
28from ansible.module_utils._text import to_bytes, to_text
29from ansible.module_utils.six import string_types
30from ansible.parsing.yaml.dumper import AnsibleDumper
31from ansible.plugins.callback import CallbackBase, strip_internal_keys, module_response_deepcopy
32from ansible.plugins.callback.default import CallbackModule as Default
33
34
35# from http://stackoverflow.com/a/15423007/115478
36def should_use_block(value):
37    """Returns true if string should be in block format"""
38    for c in u"\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029":
39        if c in value:
40            return True
41    return False
42
43
44def my_represent_scalar(self, tag, value, style=None):
45    """Uses block style for multi-line strings"""
46    if style is None:
47        if should_use_block(value):
48            style = '|'
49            # we care more about readable than accuracy, so...
50            # ...no trailing space
51            value = value.rstrip()
52            # ...and non-printable characters
53            value = ''.join(x for x in value if x in string.printable)
54            # ...tabs prevent blocks from expanding
55            value = value.expandtabs()
56            # ...and odd bits of whitespace
57            value = re.sub(r'[\x0b\x0c\r]', '', value)
58            # ...as does trailing space
59            value = re.sub(r' +\n', '\n', value)
60        else:
61            style = self.default_style
62    node = yaml.representer.ScalarNode(tag, value, style=style)
63    if self.alias_key is not None:
64        self.represented_objects[self.alias_key] = node
65    return node
66
67
68class CallbackModule(Default):
69
70    """
71    Variation of the Default output which uses nicely readable YAML instead
72    of JSON for printing results.
73    """
74
75    CALLBACK_VERSION = 2.0
76    CALLBACK_TYPE = 'stdout'
77    CALLBACK_NAME = 'yaml'
78
79    def __init__(self):
80        super(CallbackModule, self).__init__()
81        yaml.representer.BaseRepresenter.represent_scalar = my_represent_scalar
82
83    def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False):
84        if result.get('_ansible_no_log', False):
85            return json.dumps(dict(censored="The output has been hidden due to the fact that 'no_log: true' was specified for this result"))
86
87        # All result keys stating with _ansible_ are internal, so remove them from the result before we output anything.
88        abridged_result = strip_internal_keys(module_response_deepcopy(result))
89
90        # remove invocation unless specifically wanting it
91        if not keep_invocation and self._display.verbosity < 3 and 'invocation' in result:
92            del abridged_result['invocation']
93
94        # remove diff information from screen output
95        if self._display.verbosity < 3 and 'diff' in result:
96            del abridged_result['diff']
97
98        # remove exception from screen output
99        if 'exception' in abridged_result:
100            del abridged_result['exception']
101
102        dumped = ''
103
104        # put changed and skipped into a header line
105        if 'changed' in abridged_result:
106            dumped += 'changed=' + str(abridged_result['changed']).lower() + ' '
107            del abridged_result['changed']
108
109        if 'skipped' in abridged_result:
110            dumped += 'skipped=' + str(abridged_result['skipped']).lower() + ' '
111            del abridged_result['skipped']
112
113        # if we already have stdout, we don't need stdout_lines
114        if 'stdout' in abridged_result and 'stdout_lines' in abridged_result:
115            abridged_result['stdout_lines'] = '<omitted>'
116
117        # if we already have stderr, we don't need stderr_lines
118        if 'stderr' in abridged_result and 'stderr_lines' in abridged_result:
119            abridged_result['stderr_lines'] = '<omitted>'
120
121        if abridged_result:
122            dumped += '\n'
123            dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))
124
125        # indent by a couple of spaces
126        dumped = '\n  '.join(dumped.split('\n')).rstrip()
127        return dumped
128
129    def _serialize_diff(self, diff):
130        return to_text(yaml.dump(diff, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))
131