├── hosts ├── roles ├── watch │ ├── tasks │ │ ├── main.yml │ │ ├── again.yml │ │ ├── recursive.yml │ │ └── loop.yml │ ├── defaults │ │ └── main.yml │ └── README.md └── long_run │ ├── defaults │ └── main.yml │ └── tasks │ └── main.yml ├── ansible.cfg ├── docker-compose.yml ├── files └── task.sh ├── main.yml ├── README.md └── callback_plugins └── custom.py /hosts: -------------------------------------------------------------------------------- 1 | [main] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /roles/watch/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - set_fact: 4 | watch_lines: 0 5 | 6 | - name: "[{{ watch_title }}] - include_tasks: 'again.yml'" 7 | include_tasks: 'again.yml' 8 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | interpreter_python=/usr/bin/python3 3 | inventory = hosts 4 | display_ok_hosts = no 5 | display_skipped_hosts = no 6 | callback_whitelist = custom 7 | stdout_callback = custom -------------------------------------------------------------------------------- /roles/watch/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | watch_title: "watch task" 4 | 5 | watch_become: no 6 | 7 | watch_file: '/dev/null' 8 | 9 | watch_lines: 0 10 | 11 | watch_poll: 1 12 | 13 | watch_timeout: 1000 14 | 15 | watch_count: 0 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | ansible: 6 | image: lucasbasquerotto/ansible:0.0.2 7 | volumes: 8 | - .:/main:ro 9 | working_dir: /main 10 | logging: 11 | options: 12 | max-size: 50m -------------------------------------------------------------------------------- /roles/long_run/defaults/main.yml: -------------------------------------------------------------------------------- 1 | long_run_title: "long run task" 2 | long_run_become: no 3 | long_run_output_path: '/var/log/main' 4 | long_run_output_file: 'output.log' 5 | long_run_path: '/bin' 6 | long_run_cmd: 'echo "no command"' 7 | long_run_timeout: 3600 8 | long_run_poll: 5 -------------------------------------------------------------------------------- /roles/watch/tasks/again.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: '[{{ watch_title }}] - initialize watch_count' 4 | set_fact: 5 | watch_count: 0 6 | 7 | - name: '[{{ watch_title }}] - checking {{ watch_job }} status until finished' 8 | include_tasks: 'recursive.yml' 9 | 10 | - name: '[{{ watch_title }}] - watch_status is finished' 11 | assert: 12 | that: watch_status.finished 13 | -------------------------------------------------------------------------------- /files/task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | arr=( 5 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " 6 | "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " 7 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " 8 | "nisi ut aliquip ex ea commodo consequat. " 9 | "Duis aute irure dolor in reprehenderit in voluptate velit esse " 10 | "cillum dolore eu fugiat nulla pariatur. " 11 | "Excepteur sint occaecat cupidatat non proident, " 12 | "sunt in culpa qui officia deserunt mollit anim id est laborum." 13 | ) 14 | 15 | for i in {1..20}; do 16 | for s in "${arr[@]}"; do 17 | echo "$(date '+%F %X') - $i - $s" 18 | done 19 | 20 | sleep 1 21 | done -------------------------------------------------------------------------------- /main.yml: -------------------------------------------------------------------------------- 1 | - name: Play 01 - Execute the long running task and display output in real-time 2 | hosts: main 3 | tasks: 4 | - file: 5 | path: "/somedir" 6 | state: directory 7 | tags: [print_action] 8 | 9 | - file: 10 | path: "/tmp/somedir" 11 | state: directory 12 | tags: [print_action] 13 | 14 | - copy: 15 | src: files/task.sh 16 | dest: /somedir/task 17 | mode: u=+x 18 | tags: [print_action] 19 | 20 | - include_role: 21 | name: long_run 22 | vars: 23 | long_run_title: "my custom task" 24 | long_run_output_path: "/tmp/somedir" 25 | long_run_output_file: "task.log" 26 | long_run_path: "/somedir" 27 | long_run_cmd: "./task" 28 | long_run_timeout: 600 29 | long_run_poll: 1 30 | 31 | - debug: 32 | msg: "Play ended" 33 | tags: [print_action] -------------------------------------------------------------------------------- /roles/watch/tasks/recursive.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: '[{{ watch_title }}] - checking {{ watch_job }} status (recursive)' 4 | include_tasks: 'loop.yml' 5 | 6 | - name: '[{{ watch_title }}] - set watch_finished' 7 | set_fact: 8 | watch_finished: "{{ watch_status.finished | bool }}" 9 | 10 | - name: '[{{ watch_title }}] - count ({{ watch_count | int + 1 }})' 11 | set_fact: 12 | watch_count: '{{ watch_count | int + 1 }}' 13 | 14 | - name: '[{{ watch_title }}] - retries ({{ (watch_timeout | int / watch_poll | int) | int }})' 15 | set_fact: 16 | watch_retries: '{{ (watch_timeout | int / watch_poll | int) | int }}' 17 | 18 | - name: '[{{ watch_title }}] - timeout ({{ watch_timeout }} seconds)' 19 | fail: 20 | msg: "Timeout of {{ watch_timeout }} seconds exceeded ({{ watch_retries }} retries)" 21 | when: (not watch_finished) and (watch_count | int > watch_retries | int) 22 | 23 | - name: '[{{ watch_title }}] - wait for {{ watch_poll }} seconds' 24 | wait_for: 25 | timeout: '{{ watch_poll | int }}' 26 | when: not watch_finished 27 | 28 | - name: '[{{ watch_title }}] - call itself recursively' 29 | include_tasks: 'recursive.yml' 30 | when: not watch_finished -------------------------------------------------------------------------------- /roles/watch/tasks/loop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: '[{{ watch_title }}] - checking {{ watch_job }} status' 4 | become: "{{ watch_become }}" 5 | async_status: 6 | jid: '{{ job.ansible_job_id }}' 7 | register: 'watch_status' 8 | vars: 9 | job: '{{ lookup("vars", watch_job) }}' 10 | when: 'job.ansible_job_id is defined' 11 | changed_when: false 12 | 13 | - name: '[{{ watch_title }}] - tail -n +{{ watch_lines }} {{ watch_file }}' 14 | become: "{{ watch_become }}" 15 | shell: 'tail -n +{{ watch_lines }} {{ watch_file }}' 16 | register: 'watch_tail' 17 | failed_when: false 18 | changed_when: false 19 | 20 | - set_fact: 21 | watch_output: '{{ watch_tail.stdout_lines | default([]) }}' 22 | 23 | - set_fact: 24 | watch_output_lines: '{{ watch_output | length | int }}' 25 | 26 | - block: 27 | 28 | - name: '[{{ watch_title }}] - {{ watch_output_lines }} lines captured' 29 | set_fact: 30 | watch_lines: '{{ watch_lines|int + watch_output_lines|int }}' 31 | 32 | - name: '[{{ watch_title }}] - {{ watch_file }} - {{ watch_output_lines }} lines captured' 33 | debug: 34 | var: watch_output 35 | tags: ["print_action"] 36 | 37 | when: (watch_output_lines | int) > 0 38 | -------------------------------------------------------------------------------- /roles/long_run/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | long_run_before: "{{ lookup('pipe', 'date \"+%Y-%m-%d %H:%M:%S\"') }}" 3 | 4 | - name: '[{{ long_run_title }}] - create the directory "{{ long_run_output_path }}" to run' 5 | become: "{{ long_run_become }}" 6 | file: 7 | path: "{{ long_run_output_path }}" 8 | state: directory 9 | mode: 0744 10 | 11 | - name: '[{{ long_run_title }}] - clear output file before run' 12 | become: "{{ long_run_become }}" 13 | copy: 14 | content: "" 15 | dest: "{{ long_run_output_path }}/{{ long_run_output_file }}" 16 | mode: 0600 17 | 18 | - name: '[{{ long_run_title }}] - start the execution of "{{ long_run_cmd }}"' 19 | become: "{{ long_run_become }}" 20 | shell: | 21 | set -o pipefail 22 | {{ long_run_cmd }} 2>&1 | tee --append {{ long_run_output_path }}/{{ long_run_output_file }} 23 | args: 24 | executable: /bin/bash 25 | chdir: "{{ long_run_path }}" 26 | async: "{{ long_run_timeout | int }}" 27 | poll: 0 28 | register: 'long_run_register' 29 | 30 | - name: '[{{ long_run_title }}] - Watch "{{ long_run_output_path }}/output.log" until finishes' 31 | include_role: 32 | name: 'watch' 33 | vars: 34 | watch_title: "{{ long_run_title }}" 35 | watch_become: "{{ long_run_become }}" 36 | watch_file: '{{ long_run_output_path }}/{{ long_run_output_file }}' 37 | watch_job: 'long_run_register' 38 | watch_timeout: "{{ long_run_timeout | int }}" 39 | watch_poll: "{{ long_run_poll | int }}" 40 | 41 | - name: '[{{ long_run_title }}]' 42 | debug: 43 | msg: "before: {{ long_run_before }}" 44 | tags: ["print_action"] 45 | 46 | - name: '[{{ long_run_title }}]' 47 | debug: 48 | msg: "after: {{ lookup('pipe', 'date \"+%Y-%m-%d %H:%M:%S\"') }}" 49 | tags: ["print_action"] 50 | -------------------------------------------------------------------------------- /roles/watch/README.md: -------------------------------------------------------------------------------- 1 | Watch 2 | ===== 3 | 4 | Watch the output of long-running tasks. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Linux or MacOSX 10 | 11 | Role Variables 12 | -------------- 13 | 14 | * `watch_job`: The name of the job to watch. 15 | * `watch_file`: (optional) The path to a logfile to tail. 16 | * `watch_lines`: The number of logfile lines to skip. 17 | * `watch_poll`: Seconds between status checks. (default 1) 18 | * `watch_timeout`: Maximum seconds to watch a single job. (default 1000) 19 | 20 | Dependencies 21 | ------------ 22 | 23 | None. 24 | 25 | Usage 26 | ----- 27 | 28 | ```yaml 29 | 30 | - name: 'Start a long-running task.' 31 | shell: 'some-command > /some/log/file' 32 | async: 3600 # Max runtime in seconds. 33 | poll: 0 # Run in the background. 34 | register: 'longjob' # Unique Job name. 35 | 36 | - name: 'Watch /some/log/file until some-command finishes.' 37 | include_role: 38 | name: 'watch' 39 | vars: 40 | watch_file: '/some/log/file' # Output log file (optional). 41 | watch_job: 'longjob' # Job name from previous task. 42 | watch_timeout: 3600 # Set at least as high as async. 43 | 44 | - name: 'Run another task with the same logfile.' 45 | shell: 'some-other-command >> /some/log/file' 46 | async: 600 47 | poll: 0 48 | register: 'anotherjob' 49 | 50 | - name: 'Continue watching /some/log/file until some-other-command finishes.' 51 | include_role: 52 | name: 'watch' 53 | tasks_from: 'again' 54 | vars: 55 | watch_file: '/some/log/file' 56 | watch_job: 'anotherjob' 57 | 58 | ``` 59 | 60 | License 61 | ------- 62 | 63 | BSD 64 | 65 | Author Information 66 | ------------------ 67 | 68 | [Robert August Vincent II](https://github.com/pillarsdotnet) 69 | *(pronounced "Bob" or "Bob-Vee")* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Output in Real-Time 2 | 3 | This repository provides a minimal demonstration of running an ansible playbook and seeing the output of a shell command in real time (as the command is running). 4 | 5 | The playbook includes the role `long_run` that executes the task asynchronously and calls the role `watch` that watches the job and shows its output incrementally, waiting a specified amount of seconds (`long_run_poll`) between consecutive displays, giving a near real time behaviour (for small values of `long_run_poll`). 6 | 7 | ## Some features and behaviour 8 | 9 | - If the job is ended with an error code, the watch task also ends with an error. 10 | - If the job had no output in some (or all) iterations of the watch job, there will be no output displayed by ansible in such iterations. 11 | - It was created a custom callback plugin to allow printing ansible tasks with the `ok` status **only** if the task has a tag `print_action` (along with the options `display_ok_hosts = no` and `display_skipped_hosts = no` in `ansible.cfg`) to print only relevant stuff. 12 | - The time between 2 consecutive outputs from the watch job is the time specified in `long_run_poll` plus the time taken to execute the other tasks to update the state of the job, retrieving the lines, actually displaying it, and so on. 13 | 14 | ## Running the playbook 15 | 16 | To make it easier to run the playbook, it was created a docker compose file to allow running the playbook inside a docker container with Ansible 2.8 installed (the image is already provided, you can simply run the commands below). 17 | 18 | (You need to have `docker` and `docker-compose` installed to run it with docker, but if you have Ansible 2.8 you should be able to run the step 2 directly, maybe needing to fix some permisson issues or change the directories paths in `main.yml` to avoid such issues). 19 | 20 | ### 1. In this repository, start the docker container with ansible installed 21 | 22 | ```bash 23 | docker-compose run --rm ansible /bin/bash 24 | ``` 25 | 26 | ### 2. Inside the container, run the playbook and see the output in realtime 27 | 28 | ```bash 29 | ansible-playbook main.yml 30 | ``` 31 | ## Screenshot of the output 32 | 33 | ![ansible output image](https://raw.githubusercontent.com/lucasbasquerotto/my-projects/master/images/ansible-output.png) 34 | 35 | ## Tips: 36 | 37 | 1) Change `long_run_poll` in `main.yml` from `3` to `1` and see that the output is show in shorter intervals. 38 | 39 | 2) Change `files/task.sh` including some kind of error in the script to see a case in which the task is executed unsuccessfully. 40 | 41 | 3) Remove the strings in the array of `files/task.sh` to see a case in which there is no output from the task. 42 | 43 | 4) Change the file `ansible.cfg` commenting the lines `display_ok_hosts` and `stdout_callback` to see that many unnecessary stuff is displayed. It can be much worse for tasks that may take a considerable amount of time with a small output (that's the main reason for using the custom callback plugin). 44 | 45 | 5) To make it work in other projects, include the roles `long_run` and `watch` as well as the callback plugin, and enable the plugin in the `ansible.cfg` file, defining also `display_ok_hosts = no` and `display_skipped_hosts = no`. Keep in mind that the tasks in the project that ends with the `ok` status will not be shown, but you can make they show including a tag `print_action` in the task. You can use this repository as a reference. 46 | -------------------------------------------------------------------------------- /callback_plugins/custom.py: -------------------------------------------------------------------------------- 1 | # (c) 2012-2014, Michael DeHaan 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 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | callback: default 10 | type: stdout 11 | short_description: default Ansible screen output 12 | version_added: historical 13 | description: 14 | - This is the default output callback for ansible-playbook. 15 | extends_documentation_fragment: 16 | - default_callback 17 | requirements: 18 | - set as stdout in configuration 19 | ''' 20 | 21 | from ansible import constants as C 22 | from ansible import context 23 | from ansible.playbook.task_include import TaskInclude 24 | from ansible.plugins.callback import CallbackBase 25 | from ansible.utils.color import colorize, hostcolor 26 | 27 | 28 | # These values use ansible.constants for historical reasons, mostly to allow 29 | # unmodified derivative plugins to work. However, newer options added to the 30 | # plugin are not also added to ansible.constants, so authors of derivative 31 | # callback plugins will eventually need to add a reference to the common docs 32 | # fragment for the 'default' callback plugin 33 | 34 | # these are used to provide backwards compat with old plugins that subclass from default 35 | # but still don't use the new config system and/or fail to document the options 36 | COMPAT_OPTIONS = (('display_skipped_hosts', C.DISPLAY_SKIPPED_HOSTS), 37 | ('display_ok_hosts', True), 38 | ('show_custom_stats', C.SHOW_CUSTOM_STATS), 39 | ('display_failed_stderr', False),) 40 | 41 | 42 | class CallbackModule(CallbackBase): 43 | 44 | ''' 45 | This is the default callback interface, which simply prints messages 46 | to stdout when new callback events are received. 47 | ''' 48 | 49 | CALLBACK_VERSION = 2.0 50 | CALLBACK_TYPE = 'stdout' 51 | CALLBACK_NAME = 'default' 52 | 53 | def __init__(self): 54 | 55 | self._play = None 56 | self._last_task_banner = None 57 | self._last_task_name = None 58 | self._task_type_cache = {} 59 | super(CallbackModule, self).__init__() 60 | 61 | def set_options(self, task_keys=None, var_options=None, direct=None): 62 | 63 | super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) 64 | 65 | # for backwards compat with plugins subclassing default, fallback to constants 66 | for option, constant in COMPAT_OPTIONS: 67 | try: 68 | value = self.get_option(option) 69 | except (AttributeError, KeyError): 70 | value = constant 71 | setattr(self, option, value) 72 | 73 | def v2_runner_on_failed(self, result, ignore_errors=False): 74 | 75 | delegated_vars = result._result.get('_ansible_delegated_vars', None) 76 | self._clean_results(result._result, result._task.action) 77 | 78 | if self._last_task_banner != result._task._uuid: 79 | self._print_task_banner(result._task) 80 | 81 | self._handle_exception(result._result, use_stderr=self.display_failed_stderr) 82 | self._handle_warnings(result._result) 83 | 84 | if result._task.loop and 'results' in result._result: 85 | self._process_items(result) 86 | 87 | else: 88 | if delegated_vars: 89 | self._display.display("fatal: [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'], 90 | self._dump_results(result._result)), 91 | color=C.COLOR_ERROR, stderr=self.display_failed_stderr) 92 | else: 93 | self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)), 94 | color=C.COLOR_ERROR, stderr=self.display_failed_stderr) 95 | 96 | if ignore_errors: 97 | self._display.display("...ignoring", color=C.COLOR_SKIP) 98 | 99 | def v2_runner_on_ok(self, result): 100 | 101 | delegated_vars = result._result.get('_ansible_delegated_vars', None) 102 | 103 | if isinstance(result._task, TaskInclude): 104 | return 105 | elif result._result.get('changed', False): 106 | if self._last_task_banner != result._task._uuid: 107 | self._print_task_banner(result._task) 108 | 109 | if delegated_vars: 110 | msg = "changed: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) 111 | else: 112 | msg = "changed: [%s]" % result._host.get_name() 113 | color = C.COLOR_CHANGED 114 | else: 115 | if not self.display_ok_hosts: 116 | if 'print_action' not in result._task.tags: 117 | return 118 | 119 | if self._last_task_banner != result._task._uuid: 120 | self._print_task_banner(result._task) 121 | 122 | if delegated_vars: 123 | msg = "ok: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) 124 | else: 125 | msg = "ok: [%s]" % result._host.get_name() 126 | color = C.COLOR_OK 127 | 128 | self._handle_warnings(result._result) 129 | 130 | if result._task.loop and 'results' in result._result: 131 | self._process_items(result) 132 | else: 133 | self._clean_results(result._result, result._task.action) 134 | 135 | if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result: 136 | msg += " => %s" % (self._dump_results(result._result),) 137 | self._display.display(msg, color=color) 138 | 139 | def v2_runner_on_skipped(self, result): 140 | 141 | if self.display_skipped_hosts: 142 | 143 | self._clean_results(result._result, result._task.action) 144 | 145 | if self._last_task_banner != result._task._uuid: 146 | self._print_task_banner(result._task) 147 | 148 | if result._task.loop and 'results' in result._result: 149 | self._process_items(result) 150 | else: 151 | msg = "skipping: [%s]" % result._host.get_name() 152 | if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result: 153 | msg += " => %s" % self._dump_results(result._result) 154 | self._display.display(msg, color=C.COLOR_SKIP) 155 | 156 | def v2_runner_on_unreachable(self, result): 157 | if self._last_task_banner != result._task._uuid: 158 | self._print_task_banner(result._task) 159 | 160 | delegated_vars = result._result.get('_ansible_delegated_vars', None) 161 | if delegated_vars: 162 | msg = "fatal: [%s -> %s]: UNREACHABLE! => %s" % (result._host.get_name(), delegated_vars['ansible_host'], self._dump_results(result._result)) 163 | else: 164 | msg = "fatal: [%s]: UNREACHABLE! => %s" % (result._host.get_name(), self._dump_results(result._result)) 165 | self._display.display(msg, color=C.COLOR_UNREACHABLE, stderr=self.display_failed_stderr) 166 | 167 | def v2_playbook_on_no_hosts_matched(self): 168 | self._display.display("skipping: no hosts matched", color=C.COLOR_SKIP) 169 | 170 | def v2_playbook_on_no_hosts_remaining(self): 171 | self._display.banner("NO MORE HOSTS LEFT") 172 | 173 | def v2_playbook_on_task_start(self, task, is_conditional): 174 | self._task_start(task, prefix='TASK') 175 | 176 | def _task_start(self, task, prefix=None): 177 | # Cache output prefix for task if provided 178 | # This is needed to properly display 'RUNNING HANDLER' and similar 179 | # when hiding skipped/ok task results 180 | if prefix is not None: 181 | self._task_type_cache[task._uuid] = prefix 182 | 183 | # Preserve task name, as all vars may not be available for templating 184 | # when we need it later 185 | if self._play.strategy == 'free': 186 | # Explicitly set to None for strategy 'free' to account for any cached 187 | # task title from a previous non-free play 188 | self._last_task_name = None 189 | else: 190 | self._last_task_name = task.get_name().strip() 191 | 192 | # Display the task banner immediately if we're not doing any filtering based on task result 193 | if self.display_skipped_hosts and self.display_ok_hosts: 194 | self._print_task_banner(task) 195 | 196 | def _print_task_banner(self, task): 197 | # args can be specified as no_log in several places: in the task or in 198 | # the argument spec. We can check whether the task is no_log but the 199 | # argument spec can't be because that is only run on the target 200 | # machine and we haven't run it thereyet at this time. 201 | # 202 | # So we give people a config option to affect display of the args so 203 | # that they can secure this if they feel that their stdout is insecure 204 | # (shoulder surfing, logging stdout straight to a file, etc). 205 | args = '' 206 | if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT: 207 | args = u', '.join(u'%s=%s' % a for a in task.args.items()) 208 | args = u' %s' % args 209 | 210 | prefix = self._task_type_cache.get(task._uuid, 'TASK') 211 | 212 | # Use cached task name 213 | task_name = self._last_task_name 214 | if task_name is None: 215 | task_name = task.get_name().strip() 216 | 217 | self._display.banner(u"%s [%s%s]" % (prefix, task_name, args)) 218 | if self._display.verbosity >= 2: 219 | path = task.get_path() 220 | if path: 221 | self._display.display(u"task path: %s" % path, color=C.COLOR_DEBUG) 222 | 223 | self._last_task_banner = task._uuid 224 | 225 | def v2_playbook_on_cleanup_task_start(self, task): 226 | self._task_start(task, prefix='CLEANUP TASK') 227 | 228 | def v2_playbook_on_handler_task_start(self, task): 229 | self._task_start(task, prefix='RUNNING HANDLER') 230 | 231 | # def v2_runner_on_start(self, host, task): 232 | # if self.get_option('show_per_host_start'): 233 | # self._display.display(" [started %s on %s]" % (task, host), color=C.COLOR_OK) 234 | 235 | def v2_playbook_on_play_start(self, play): 236 | name = play.get_name().strip() 237 | if not name: 238 | msg = u"PLAY" 239 | else: 240 | msg = u"PLAY [%s]" % name 241 | 242 | self._play = play 243 | 244 | self._display.banner(msg) 245 | 246 | def v2_on_file_diff(self, result): 247 | if result._task.loop and 'results' in result._result: 248 | for res in result._result['results']: 249 | if 'diff' in res and res['diff'] and res.get('changed', False): 250 | diff = self._get_diff(res['diff']) 251 | if diff: 252 | if self._last_task_banner != result._task._uuid: 253 | self._print_task_banner(result._task) 254 | self._display.display(diff) 255 | elif 'diff' in result._result and result._result['diff'] and result._result.get('changed', False): 256 | diff = self._get_diff(result._result['diff']) 257 | if diff: 258 | if self._last_task_banner != result._task._uuid: 259 | self._print_task_banner(result._task) 260 | self._display.display(diff) 261 | 262 | def v2_runner_item_on_ok(self, result): 263 | 264 | delegated_vars = result._result.get('_ansible_delegated_vars', None) 265 | self._clean_results(result._result, result._task.action) 266 | if isinstance(result._task, TaskInclude): 267 | return 268 | elif result._result.get('changed', False): 269 | if self._last_task_banner != result._task._uuid: 270 | self._print_task_banner(result._task) 271 | 272 | msg = 'changed' 273 | color = C.COLOR_CHANGED 274 | else: 275 | if not self.display_ok_hosts: 276 | if 'print_action' not in result._task.tags: 277 | return 278 | 279 | if self._last_task_banner != result._task._uuid: 280 | self._print_task_banner(result._task) 281 | 282 | msg = 'ok' 283 | color = C.COLOR_OK 284 | 285 | if delegated_vars: 286 | msg += ": [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) 287 | else: 288 | msg += ": [%s]" % result._host.get_name() 289 | 290 | msg += " => (item=%s)" % (self._get_item_label(result._result),) 291 | 292 | if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result: 293 | msg += " => %s" % self._dump_results(result._result) 294 | self._display.display(msg, color=color) 295 | 296 | def v2_runner_item_on_failed(self, result): 297 | if self._last_task_banner != result._task._uuid: 298 | self._print_task_banner(result._task) 299 | 300 | delegated_vars = result._result.get('_ansible_delegated_vars', None) 301 | self._clean_results(result._result, result._task.action) 302 | self._handle_exception(result._result) 303 | 304 | msg = "failed: " 305 | if delegated_vars: 306 | msg += "[%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host']) 307 | else: 308 | msg += "[%s]" % (result._host.get_name()) 309 | 310 | self._handle_warnings(result._result) 311 | self._display.display(msg + " (item=%s) => %s" % (self._get_item_label(result._result), self._dump_results(result._result)), color=C.COLOR_ERROR) 312 | 313 | def v2_runner_item_on_skipped(self, result): 314 | if self.display_skipped_hosts: 315 | if self._last_task_banner != result._task._uuid: 316 | self._print_task_banner(result._task) 317 | 318 | self._clean_results(result._result, result._task.action) 319 | msg = "skipping: [%s] => (item=%s) " % (result._host.get_name(), self._get_item_label(result._result)) 320 | if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result: 321 | msg += " => %s" % self._dump_results(result._result) 322 | self._display.display(msg, color=C.COLOR_SKIP) 323 | 324 | def v2_playbook_on_include(self, included_file): 325 | if self.display_ok_hosts: 326 | msg = 'included: %s for %s' % (included_file._filename, ", ".join([h.name for h in included_file._hosts])) 327 | if 'item' in included_file._args: 328 | msg += " => (item=%s)" % (self._get_item_label(included_file._args),) 329 | self._display.display(msg, color=C.COLOR_SKIP) 330 | 331 | def v2_playbook_on_stats(self, stats): 332 | self._display.banner("PLAY RECAP") 333 | 334 | hosts = sorted(stats.processed.keys()) 335 | for h in hosts: 336 | t = stats.summarize(h) 337 | 338 | self._display.display( 339 | u"%s : %s %s %s %s %s %s %s" % ( 340 | hostcolor(h, t), 341 | colorize(u'ok', t['ok'], C.COLOR_OK), 342 | colorize(u'changed', t['changed'], C.COLOR_CHANGED), 343 | colorize(u'unreachable', t['unreachable'], C.COLOR_UNREACHABLE), 344 | colorize(u'failed', t['failures'], C.COLOR_ERROR), 345 | colorize(u'skipped', t['skipped'], C.COLOR_SKIP), 346 | colorize(u'rescued', t['rescued'], C.COLOR_OK), 347 | colorize(u'ignored', t['ignored'], C.COLOR_WARN), 348 | ), 349 | screen_only=True 350 | ) 351 | 352 | self._display.display( 353 | u"%s : %s %s %s %s %s %s %s" % ( 354 | hostcolor(h, t, False), 355 | colorize(u'ok', t['ok'], None), 356 | colorize(u'changed', t['changed'], None), 357 | colorize(u'unreachable', t['unreachable'], None), 358 | colorize(u'failed', t['failures'], None), 359 | colorize(u'skipped', t['skipped'], None), 360 | colorize(u'rescued', t['rescued'], None), 361 | colorize(u'ignored', t['ignored'], None), 362 | ), 363 | log_only=True 364 | ) 365 | 366 | self._display.display("", screen_only=True) 367 | 368 | # print custom stats if required 369 | if stats.custom and self.show_custom_stats: 370 | self._display.banner("CUSTOM STATS: ") 371 | # per host 372 | # TODO: come up with 'pretty format' 373 | for k in sorted(stats.custom.keys()): 374 | if k == '_run': 375 | continue 376 | self._display.display('\t%s: %s' % (k, self._dump_results(stats.custom[k], indent=1).replace('\n', ''))) 377 | 378 | # print per run custom stats 379 | if '_run' in stats.custom: 380 | self._display.display("", screen_only=True) 381 | self._display.display('\tRUN: %s' % self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')) 382 | self._display.display("", screen_only=True) 383 | 384 | def v2_playbook_on_start(self, playbook): 385 | if self._display.verbosity > 1: 386 | from os.path import basename 387 | self._display.banner("PLAYBOOK: %s" % basename(playbook._file_name)) 388 | 389 | # show CLI arguments 390 | if self._display.verbosity > 3: 391 | if context.CLIARGS.get('args'): 392 | self._display.display('Positional arguments: %s' % ' '.join(context.CLIARGS['args']), 393 | color=C.COLOR_VERBOSE, screen_only=True) 394 | 395 | for argument in (a for a in context.CLIARGS if a != 'args'): 396 | val = context.CLIARGS[argument] 397 | if val: 398 | self._display.display('%s: %s' % (argument, val), color=C.COLOR_VERBOSE, screen_only=True) 399 | 400 | def v2_runner_retry(self, result): 401 | task_name = result.task_name or result._task 402 | msg = "FAILED - RETRYING: %s (%d retries left)." % (task_name, result._result['retries'] - result._result['attempts']) 403 | if self._run_is_verbose(result, verbosity=2): 404 | msg += "Result was: %s" % self._dump_results(result._result) 405 | self._display.display(msg, color=C.COLOR_DEBUG) 406 | 407 | def v2_playbook_on_notify(self, handler, host): 408 | if self._display.verbosity > 1: 409 | self._display.display("NOTIFIED HANDLER %s for %s" % (handler.get_name(), host), color=C.COLOR_VERBOSE, screen_only=True) --------------------------------------------------------------------------------