1# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17 18# Make coding more python3-ish 19from __future__ import (absolute_import, division, print_function) 20__metaclass__ = type 21 22import os 23 24from ansible import constants as C 25from ansible import context 26from ansible.executor.task_queue_manager import TaskQueueManager 27from ansible.module_utils._text import to_text 28from ansible.module_utils.parsing.convert_bool import boolean 29from ansible.plugins.loader import become_loader, connection_loader, shell_loader 30from ansible.playbook import Playbook 31from ansible.template import Templar 32from ansible.utils.helpers import pct_to_int 33from ansible.utils.path import makedirs_safe 34from ansible.utils.ssh_functions import set_default_transport 35from ansible.utils.display import Display 36 37display = Display() 38 39 40class PlaybookExecutor: 41 42 ''' 43 This is the primary class for executing playbooks, and thus the 44 basis for bin/ansible-playbook operation. 45 ''' 46 47 def __init__(self, playbooks, inventory, variable_manager, loader, passwords): 48 self._playbooks = playbooks 49 self._inventory = inventory 50 self._variable_manager = variable_manager 51 self._loader = loader 52 self.passwords = passwords 53 self._unreachable_hosts = dict() 54 55 if context.CLIARGS.get('listhosts') or context.CLIARGS.get('listtasks') or \ 56 context.CLIARGS.get('listtags') or context.CLIARGS.get('syntax'): 57 self._tqm = None 58 else: 59 self._tqm = TaskQueueManager( 60 inventory=inventory, 61 variable_manager=variable_manager, 62 loader=loader, 63 passwords=self.passwords, 64 forks=context.CLIARGS.get('forks'), 65 ) 66 67 # Note: We run this here to cache whether the default ansible ssh 68 # executable supports control persist. Sometime in the future we may 69 # need to enhance this to check that ansible_ssh_executable specified 70 # in inventory is also cached. We can't do this caching at the point 71 # where it is used (in task_executor) because that is post-fork and 72 # therefore would be discarded after every task. 73 set_default_transport() 74 75 def run(self): 76 ''' 77 Run the given playbook, based on the settings in the play which 78 may limit the runs to serialized groups, etc. 79 ''' 80 81 result = 0 82 entrylist = [] 83 entry = {} 84 try: 85 # preload become/connection/shell to set config defs cached 86 list(connection_loader.all(class_only=True)) 87 list(shell_loader.all(class_only=True)) 88 list(become_loader.all(class_only=True)) 89 90 for playbook_path in self._playbooks: 91 pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader) 92 # FIXME: move out of inventory self._inventory.set_playbook_basedir(os.path.realpath(os.path.dirname(playbook_path))) 93 94 if self._tqm is None: # we are doing a listing 95 entry = {'playbook': playbook_path} 96 entry['plays'] = [] 97 else: 98 # make sure the tqm has callbacks loaded 99 self._tqm.load_callbacks() 100 self._tqm.send_callback('v2_playbook_on_start', pb) 101 102 i = 1 103 plays = pb.get_plays() 104 display.vv(u'%d plays in %s' % (len(plays), to_text(playbook_path))) 105 106 for play in plays: 107 if play._included_path is not None: 108 self._loader.set_basedir(play._included_path) 109 else: 110 self._loader.set_basedir(pb._basedir) 111 112 # clear any filters which may have been applied to the inventory 113 self._inventory.remove_restriction() 114 115 # Allow variables to be used in vars_prompt fields. 116 all_vars = self._variable_manager.get_vars(play=play) 117 templar = Templar(loader=self._loader, variables=all_vars) 118 setattr(play, 'vars_prompt', templar.template(play.vars_prompt)) 119 120 # FIXME: this should be a play 'sub object' like loop_control 121 if play.vars_prompt: 122 for var in play.vars_prompt: 123 vname = var['name'] 124 prompt = var.get("prompt", vname) 125 default = var.get("default", None) 126 private = boolean(var.get("private", True)) 127 confirm = boolean(var.get("confirm", False)) 128 encrypt = var.get("encrypt", None) 129 salt_size = var.get("salt_size", None) 130 salt = var.get("salt", None) 131 unsafe = var.get("unsafe", None) 132 133 if vname not in self._variable_manager.extra_vars: 134 if self._tqm: 135 self._tqm.send_callback('v2_playbook_on_vars_prompt', vname, private, prompt, encrypt, confirm, salt_size, salt, 136 default, unsafe) 137 play.vars[vname] = display.do_var_prompt(vname, private, prompt, encrypt, confirm, salt_size, salt, default, unsafe) 138 else: # we are either in --list-<option> or syntax check 139 play.vars[vname] = default 140 141 # Post validate so any play level variables are templated 142 all_vars = self._variable_manager.get_vars(play=play) 143 templar = Templar(loader=self._loader, variables=all_vars) 144 play.post_validate(templar) 145 146 if context.CLIARGS['syntax']: 147 continue 148 149 if self._tqm is None: 150 # we are just doing a listing 151 entry['plays'].append(play) 152 153 else: 154 self._tqm._unreachable_hosts.update(self._unreachable_hosts) 155 156 previously_failed = len(self._tqm._failed_hosts) 157 previously_unreachable = len(self._tqm._unreachable_hosts) 158 159 break_play = False 160 # we are actually running plays 161 batches = self._get_serialized_batches(play) 162 if len(batches) == 0: 163 self._tqm.send_callback('v2_playbook_on_play_start', play) 164 self._tqm.send_callback('v2_playbook_on_no_hosts_matched') 165 for batch in batches: 166 # restrict the inventory to the hosts in the serialized batch 167 self._inventory.restrict_to_hosts(batch) 168 # and run it... 169 result = self._tqm.run(play=play) 170 171 # break the play if the result equals the special return code 172 if result & self._tqm.RUN_FAILED_BREAK_PLAY != 0: 173 result = self._tqm.RUN_FAILED_HOSTS 174 break_play = True 175 176 # check the number of failures here, to see if they're above the maximum 177 # failure percentage allowed, or if any errors are fatal. If either of those 178 # conditions are met, we break out, otherwise we only break out if the entire 179 # batch failed 180 failed_hosts_count = len(self._tqm._failed_hosts) + len(self._tqm._unreachable_hosts) - \ 181 (previously_failed + previously_unreachable) 182 183 if len(batch) == failed_hosts_count: 184 break_play = True 185 break 186 187 # update the previous counts so they don't accumulate incorrectly 188 # over multiple serial batches 189 previously_failed += len(self._tqm._failed_hosts) - previously_failed 190 previously_unreachable += len(self._tqm._unreachable_hosts) - previously_unreachable 191 192 # save the unreachable hosts from this batch 193 self._unreachable_hosts.update(self._tqm._unreachable_hosts) 194 195 if break_play: 196 break 197 198 i = i + 1 # per play 199 200 if entry: 201 entrylist.append(entry) # per playbook 202 203 # send the stats callback for this playbook 204 if self._tqm is not None: 205 if C.RETRY_FILES_ENABLED: 206 retries = set(self._tqm._failed_hosts.keys()) 207 retries.update(self._tqm._unreachable_hosts.keys()) 208 retries = sorted(retries) 209 if len(retries) > 0: 210 if C.RETRY_FILES_SAVE_PATH: 211 basedir = C.RETRY_FILES_SAVE_PATH 212 elif playbook_path: 213 basedir = os.path.dirname(os.path.abspath(playbook_path)) 214 else: 215 basedir = '~/' 216 217 (retry_name, _) = os.path.splitext(os.path.basename(playbook_path)) 218 filename = os.path.join(basedir, "%s.retry" % retry_name) 219 if self._generate_retry_inventory(filename, retries): 220 display.display("\tto retry, use: --limit @%s\n" % filename) 221 222 self._tqm.send_callback('v2_playbook_on_stats', self._tqm._stats) 223 224 # if the last result wasn't zero, break out of the playbook file name loop 225 if result != 0: 226 break 227 228 if entrylist: 229 return entrylist 230 231 finally: 232 if self._tqm is not None: 233 self._tqm.cleanup() 234 if self._loader: 235 self._loader.cleanup_all_tmp_files() 236 237 if context.CLIARGS['syntax']: 238 display.display("No issues encountered") 239 return result 240 241 if context.CLIARGS['start_at_task'] and not self._tqm._start_at_done: 242 display.error( 243 "No matching task \"%s\" found." 244 " Note: --start-at-task can only follow static includes." 245 % context.CLIARGS['start_at_task'] 246 ) 247 248 return result 249 250 def _get_serialized_batches(self, play): 251 ''' 252 Returns a list of hosts, subdivided into batches based on 253 the serial size specified in the play. 254 ''' 255 256 # make sure we have a unique list of hosts 257 all_hosts = self._inventory.get_hosts(play.hosts, order=play.order) 258 all_hosts_len = len(all_hosts) 259 260 # the serial value can be listed as a scalar or a list of 261 # scalars, so we make sure it's a list here 262 serial_batch_list = play.serial 263 if len(serial_batch_list) == 0: 264 serial_batch_list = [-1] 265 266 cur_item = 0 267 serialized_batches = [] 268 269 while len(all_hosts) > 0: 270 # get the serial value from current item in the list 271 serial = pct_to_int(serial_batch_list[cur_item], all_hosts_len) 272 273 # if the serial count was not specified or is invalid, default to 274 # a list of all hosts, otherwise grab a chunk of the hosts equal 275 # to the current serial item size 276 if serial <= 0: 277 serialized_batches.append(all_hosts) 278 break 279 else: 280 play_hosts = [] 281 for x in range(serial): 282 if len(all_hosts) > 0: 283 play_hosts.append(all_hosts.pop(0)) 284 285 serialized_batches.append(play_hosts) 286 287 # increment the current batch list item number, and if we've hit 288 # the end keep using the last element until we've consumed all of 289 # the hosts in the inventory 290 cur_item += 1 291 if cur_item > len(serial_batch_list) - 1: 292 cur_item = len(serial_batch_list) - 1 293 294 return serialized_batches 295 296 def _generate_retry_inventory(self, retry_path, replay_hosts): 297 ''' 298 Called when a playbook run fails. It generates an inventory which allows 299 re-running on ONLY the failed hosts. This may duplicate some variable 300 information in group_vars/host_vars but that is ok, and expected. 301 ''' 302 try: 303 makedirs_safe(os.path.dirname(retry_path)) 304 with open(retry_path, 'w') as fd: 305 for x in replay_hosts: 306 fd.write("%s\n" % x) 307 except Exception as e: 308 display.warning("Could not create retry file '%s'.\n\t%s" % (retry_path, to_text(e))) 309 return False 310 311 return True 312