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