1# (c) 2016, Dag Wieers <dag@wieers.com>
2# (c) 2017 Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import (absolute_import, division, print_function)
6__metaclass__ = type
7
8DOCUMENTATION = '''
9callback: dense
10type: stdout
11short_description: minimal stdout output
12extends_documentation_fragment:
13- default_callback
14description:
15- When in verbose mode it will act the same as the default callback
16author:
17- Dag Wieers (@dagwieers)
18version_added: "2.3"
19requirements:
20- set as stdout in configuation
21'''
22
23HAS_OD = False
24try:
25    from collections import OrderedDict
26    HAS_OD = True
27except ImportError:
28    pass
29
30from ansible.module_utils.six import binary_type, text_type
31from ansible.module_utils.common._collections_compat import MutableMapping, MutableSequence
32from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
33from ansible.utils.color import colorize, hostcolor
34from ansible.utils.display import Display
35
36import sys
37
38display = Display()
39
40
41# Design goals:
42#
43#  + On screen there should only be relevant stuff
44#    - How far are we ? (during run, last line)
45#    - What issues occurred
46#    - What changes occurred
47#    - Diff output (in diff-mode)
48#
49#  + If verbosity increases, act as default output
50#    So that users can easily switch to default for troubleshooting
51#
52#  + Rewrite the output during processing
53#    - We use the cursor to indicate where in the task we are.
54#      Output after the prompt is the output of the previous task.
55#    - If we would clear the line at the start of a task, there would often
56#      be no information at all, so we leave it until it gets updated
57#
58#  + Use the same color-conventions of Ansible
59#
60#  + Ensure the verbose output (-v) is also dense.
61#    Remove information that is not essential (eg. timestamps, status)
62
63
64# TODO:
65#
66#  + Properly test for terminal capabilities, and fall back to default
67#  + Modify Ansible mechanism so we don't need to use sys.stdout directly
68#  + Find an elegant solution for progress bar line wrapping
69
70
71# FIXME: Importing constants as C simply does not work, beats me :-/
72# from ansible import constants as C
73class C:
74    COLOR_HIGHLIGHT = 'white'
75    COLOR_VERBOSE = 'blue'
76    COLOR_WARN = 'bright purple'
77    COLOR_ERROR = 'red'
78    COLOR_DEBUG = 'dark gray'
79    COLOR_DEPRECATE = 'purple'
80    COLOR_SKIP = 'cyan'
81    COLOR_UNREACHABLE = 'bright red'
82    COLOR_OK = 'green'
83    COLOR_CHANGED = 'yellow'
84
85
86# Taken from Dstat
87class vt100:
88    black = '\033[0;30m'
89    darkred = '\033[0;31m'
90    darkgreen = '\033[0;32m'
91    darkyellow = '\033[0;33m'
92    darkblue = '\033[0;34m'
93    darkmagenta = '\033[0;35m'
94    darkcyan = '\033[0;36m'
95    gray = '\033[0;37m'
96
97    darkgray = '\033[1;30m'
98    red = '\033[1;31m'
99    green = '\033[1;32m'
100    yellow = '\033[1;33m'
101    blue = '\033[1;34m'
102    magenta = '\033[1;35m'
103    cyan = '\033[1;36m'
104    white = '\033[1;37m'
105
106    blackbg = '\033[40m'
107    redbg = '\033[41m'
108    greenbg = '\033[42m'
109    yellowbg = '\033[43m'
110    bluebg = '\033[44m'
111    magentabg = '\033[45m'
112    cyanbg = '\033[46m'
113    whitebg = '\033[47m'
114
115    reset = '\033[0;0m'
116    bold = '\033[1m'
117    reverse = '\033[2m'
118    underline = '\033[4m'
119
120    clear = '\033[2J'
121#    clearline = '\033[K'
122    clearline = '\033[2K'
123    save = '\033[s'
124    restore = '\033[u'
125    save_all = '\0337'
126    restore_all = '\0338'
127    linewrap = '\033[7h'
128    nolinewrap = '\033[7l'
129
130    up = '\033[1A'
131    down = '\033[1B'
132    right = '\033[1C'
133    left = '\033[1D'
134
135
136colors = dict(
137    ok=vt100.darkgreen,
138    changed=vt100.darkyellow,
139    skipped=vt100.darkcyan,
140    ignored=vt100.cyanbg + vt100.red,
141    failed=vt100.darkred,
142    unreachable=vt100.red,
143)
144
145states = ('skipped', 'ok', 'changed', 'failed', 'unreachable')
146
147
148class CallbackModule(CallbackModule_default):
149
150    '''
151    This is the dense callback interface, where screen estate is still valued.
152    '''
153
154    CALLBACK_VERSION = 2.0
155    CALLBACK_TYPE = 'stdout'
156    CALLBACK_NAME = 'dense'
157
158    def __init__(self):
159
160        # From CallbackModule
161        self._display = display
162
163        if HAS_OD:
164
165            self.disabled = False
166            self.super_ref = super(CallbackModule, self)
167            self.super_ref.__init__()
168
169            # Attributes to remove from results for more density
170            self.removed_attributes = (
171                #                'changed',
172                'delta',
173                #                'diff',
174                'end',
175                'failed',
176                'failed_when_result',
177                'invocation',
178                'start',
179                'stdout_lines',
180            )
181
182            # Initiate data structures
183            self.hosts = OrderedDict()
184            self.keep = False
185            self.shown_title = False
186            self.count = dict(play=0, handler=0, task=0)
187            self.type = 'foo'
188
189            # Start immediately on the first line
190            sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
191            sys.stdout.flush()
192        else:
193            display.warning("The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling.")
194            self.disabled = True
195
196    def __del__(self):
197        sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
198
199    def _add_host(self, result, status):
200        name = result._host.get_name()
201
202        # Add a new status in case a failed task is ignored
203        if status == 'failed' and result._task.ignore_errors:
204            status = 'ignored'
205
206        # Check if we have to update an existing state (when looping over items)
207        if name not in self.hosts:
208            self.hosts[name] = dict(state=status)
209        elif states.index(self.hosts[name]['state']) < states.index(status):
210            self.hosts[name]['state'] = status
211
212        # Store delegated hostname, if needed
213        delegated_vars = result._result.get('_ansible_delegated_vars', None)
214        if delegated_vars:
215            self.hosts[name]['delegate'] = delegated_vars['ansible_host']
216
217        # Print progress bar
218        self._display_progress(result)
219
220#        # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
221#        if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
222        # Ensure that tasks with changes/failures stay on-screen
223        if status in ['changed', 'failed', 'unreachable']:
224            self.keep = True
225
226            if self._display.verbosity == 1:
227                # Print task title, if needed
228                self._display_task_banner()
229                self._display_results(result, status)
230
231    def _clean_results(self, result):
232        # Remove non-essential atributes
233        for attr in self.removed_attributes:
234            if attr in result:
235                del(result[attr])
236
237        # Remove empty attributes (list, dict, str)
238        for attr in result.copy():
239            if isinstance(result[attr], (MutableSequence, MutableMapping, binary_type, text_type)):
240                if not result[attr]:
241                    del(result[attr])
242
243    def _handle_exceptions(self, result):
244        if 'exception' in result:
245            # Remove the exception from the result so it's not shown every time
246            del result['exception']
247
248            if self._display.verbosity == 1:
249                return "An exception occurred during task execution. To see the full traceback, use -vvv."
250
251    def _display_progress(self, result=None):
252        # Always rewrite the complete line
253        sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline)
254        sys.stdout.write('%s %d:' % (self.type, self.count[self.type]))
255        sys.stdout.write(vt100.reset)
256        sys.stdout.flush()
257
258        # Print out each host in its own status-color
259        for name in self.hosts:
260            sys.stdout.write(' ')
261            if self.hosts[name].get('delegate', None):
262                sys.stdout.write(self.hosts[name]['delegate'] + '>')
263            sys.stdout.write(colors[self.hosts[name]['state']] + name + vt100.reset)
264            sys.stdout.flush()
265
266#        if result._result.get('diff', False):
267#            sys.stdout.write('\n' + vt100.linewrap)
268        sys.stdout.write(vt100.linewrap)
269
270#        self.keep = True
271
272    def _display_task_banner(self):
273        if not self.shown_title:
274            self.shown_title = True
275            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
276            sys.stdout.write('%s %d: %s' % (self.type, self.count[self.type], self.task.get_name().strip()))
277            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
278            sys.stdout.flush()
279        else:
280            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
281        self.keep = False
282
283    def _display_results(self, result, status):
284        # Leave the previous task on screen (as it has changes/errors)
285        if self._display.verbosity == 0 and self.keep:
286            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
287        else:
288            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
289        self.keep = False
290
291        self._clean_results(result._result)
292
293        dump = ''
294        if result._task.action == 'include':
295            return
296        elif status == 'ok':
297            return
298        elif status == 'ignored':
299            dump = self._handle_exceptions(result._result)
300        elif status == 'failed':
301            dump = self._handle_exceptions(result._result)
302        elif status == 'unreachable':
303            dump = result._result['msg']
304
305        if not dump:
306            dump = self._dump_results(result._result)
307
308        if result._task.loop and 'results' in result._result:
309            self._process_items(result)
310        else:
311            sys.stdout.write(colors[status] + status + ': ')
312
313            delegated_vars = result._result.get('_ansible_delegated_vars', None)
314            if delegated_vars:
315                sys.stdout.write(vt100.reset + result._host.get_name() + '>' + colors[status] + delegated_vars['ansible_host'])
316            else:
317                sys.stdout.write(result._host.get_name())
318
319            sys.stdout.write(': ' + dump + '\n')
320            sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
321            sys.stdout.flush()
322
323        if status == 'changed':
324            self._handle_warnings(result._result)
325
326    def v2_playbook_on_play_start(self, play):
327        # Leave the previous task on screen (as it has changes/errors)
328        if self._display.verbosity == 0 and self.keep:
329            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.bold)
330        else:
331            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.bold)
332
333        # Reset at the start of each play
334        self.keep = False
335        self.count.update(dict(handler=0, task=0))
336        self.count['play'] += 1
337        self.play = play
338
339        # Write the next play on screen IN UPPERCASE, and make it permanent
340        name = play.get_name().strip()
341        if not name:
342            name = 'unnamed'
343        sys.stdout.write('PLAY %d: %s' % (self.count['play'], name.upper()))
344        sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
345        sys.stdout.flush()
346
347    def v2_playbook_on_task_start(self, task, is_conditional):
348        # Leave the previous task on screen (as it has changes/errors)
349        if self._display.verbosity == 0 and self.keep:
350            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline)
351        else:
352            # Do not clear line, since we want to retain the previous output
353            sys.stdout.write(vt100.restore + vt100.reset + vt100.underline)
354
355        # Reset at the start of each task
356        self.keep = False
357        self.shown_title = False
358        self.hosts = OrderedDict()
359        self.task = task
360        self.type = 'task'
361
362        # Enumerate task if not setup (task names are too long for dense output)
363        if task.get_name() != 'setup':
364            self.count['task'] += 1
365
366        # Write the next task on screen (behind the prompt is the previous output)
367        sys.stdout.write('%s %d.' % (self.type, self.count[self.type]))
368        sys.stdout.write(vt100.reset)
369        sys.stdout.flush()
370
371    def v2_playbook_on_handler_task_start(self, task):
372        # Leave the previous task on screen (as it has changes/errors)
373        if self._display.verbosity == 0 and self.keep:
374            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline)
375        else:
376            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
377
378        # Reset at the start of each handler
379        self.keep = False
380        self.shown_title = False
381        self.hosts = OrderedDict()
382        self.task = task
383        self.type = 'handler'
384
385        # Enumerate handler if not setup (handler names may be too long for dense output)
386        if task.get_name() != 'setup':
387            self.count[self.type] += 1
388
389        # Write the next task on screen (behind the prompt is the previous output)
390        sys.stdout.write('%s %d.' % (self.type, self.count[self.type]))
391        sys.stdout.write(vt100.reset)
392        sys.stdout.flush()
393
394    def v2_playbook_on_cleanup_task_start(self, task):
395        # TBD
396        sys.stdout.write('cleanup.')
397        sys.stdout.flush()
398
399    def v2_runner_on_failed(self, result, ignore_errors=False):
400        self._add_host(result, 'failed')
401
402    def v2_runner_on_ok(self, result):
403        if result._result.get('changed', False):
404            self._add_host(result, 'changed')
405        else:
406            self._add_host(result, 'ok')
407
408    def v2_runner_on_skipped(self, result):
409        self._add_host(result, 'skipped')
410
411    def v2_runner_on_unreachable(self, result):
412        self._add_host(result, 'unreachable')
413
414    def v2_runner_on_include(self, included_file):
415        pass
416
417    def v2_runner_on_file_diff(self, result, diff):
418        sys.stdout.write(vt100.bold)
419        self.super_ref.v2_runner_on_file_diff(result, diff)
420        sys.stdout.write(vt100.reset)
421
422    def v2_on_file_diff(self, result):
423        sys.stdout.write(vt100.bold)
424        self.super_ref.v2_on_file_diff(result)
425        sys.stdout.write(vt100.reset)
426
427    # Old definition in v2.0
428    def v2_playbook_item_on_ok(self, result):
429        self.v2_runner_item_on_ok(result)
430
431    def v2_runner_item_on_ok(self, result):
432        if result._result.get('changed', False):
433            self._add_host(result, 'changed')
434        else:
435            self._add_host(result, 'ok')
436
437    # Old definition in v2.0
438    def v2_playbook_item_on_failed(self, result):
439        self.v2_runner_item_on_failed(result)
440
441    def v2_runner_item_on_failed(self, result):
442        self._add_host(result, 'failed')
443
444    # Old definition in v2.0
445    def v2_playbook_item_on_skipped(self, result):
446        self.v2_runner_item_on_skipped(result)
447
448    def v2_runner_item_on_skipped(self, result):
449        self._add_host(result, 'skipped')
450
451    def v2_playbook_on_no_hosts_remaining(self):
452        if self._display.verbosity == 0 and self.keep:
453            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
454        else:
455            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
456        self.keep = False
457
458        sys.stdout.write(vt100.white + vt100.redbg + 'NO MORE HOSTS LEFT')
459        sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
460        sys.stdout.flush()
461
462    def v2_playbook_on_include(self, included_file):
463        pass
464
465    def v2_playbook_on_stats(self, stats):
466        if self._display.verbosity == 0 and self.keep:
467            sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
468        else:
469            sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
470
471        # In normal mode screen output should be sufficient, summary is redundant
472        if self._display.verbosity == 0:
473            return
474
475        sys.stdout.write(vt100.bold + vt100.underline)
476        sys.stdout.write('SUMMARY')
477
478        sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
479        sys.stdout.flush()
480
481        hosts = sorted(stats.processed.keys())
482        for h in hosts:
483            t = stats.summarize(h)
484            self._display.display(
485                u"%s : %s %s %s %s %s %s" % (
486                    hostcolor(h, t),
487                    colorize(u'ok', t['ok'], C.COLOR_OK),
488                    colorize(u'changed', t['changed'], C.COLOR_CHANGED),
489                    colorize(u'unreachable', t['unreachable'], C.COLOR_UNREACHABLE),
490                    colorize(u'failed', t['failures'], C.COLOR_ERROR),
491                    colorize(u'rescued', t['rescued'], C.COLOR_OK),
492                    colorize(u'ignored', t['ignored'], C.COLOR_WARN),
493                ),
494                screen_only=True
495            )
496
497
498# When using -vv or higher, simply do the default action
499if display.verbosity >= 2 or not HAS_OD:
500    CallbackModule = CallbackModule_default
501