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 22from ansible import constants as C 23from ansible import context 24from ansible.errors import AnsibleParserError, AnsibleAssertionError 25from ansible.module_utils._text import to_native 26from ansible.module_utils.six import string_types 27from ansible.playbook.attribute import FieldAttribute 28from ansible.playbook.base import Base 29from ansible.playbook.block import Block 30from ansible.playbook.collectionsearch import CollectionSearch 31from ansible.playbook.helpers import load_list_of_blocks, load_list_of_roles 32from ansible.playbook.role import Role 33from ansible.playbook.taggable import Taggable 34from ansible.vars.manager import preprocess_vars 35from ansible.utils.display import Display 36 37display = Display() 38 39 40__all__ = ['Play'] 41 42 43class Play(Base, Taggable, CollectionSearch): 44 45 """ 46 A play is a language feature that represents a list of roles and/or 47 task/handler blocks to execute on a given set of hosts. 48 49 Usage: 50 51 Play.load(datastructure) -> Play 52 Play.something(...) 53 """ 54 55 # ================================================================================= 56 _hosts = FieldAttribute(isa='list', required=True, listof=string_types, always_post_validate=True) 57 58 # Facts 59 _gather_facts = FieldAttribute(isa='bool', default=None, always_post_validate=True) 60 _gather_subset = FieldAttribute(isa='list', default=(lambda: C.DEFAULT_GATHER_SUBSET), listof=string_types, always_post_validate=True) 61 _gather_timeout = FieldAttribute(isa='int', default=C.DEFAULT_GATHER_TIMEOUT, always_post_validate=True) 62 _fact_path = FieldAttribute(isa='string', default=C.DEFAULT_FACT_PATH) 63 64 # Variable Attributes 65 _vars_files = FieldAttribute(isa='list', default=list, priority=99) 66 _vars_prompt = FieldAttribute(isa='list', default=list, always_post_validate=False) 67 68 # Role Attributes 69 _roles = FieldAttribute(isa='list', default=list, priority=90) 70 71 # Block (Task) Lists Attributes 72 _handlers = FieldAttribute(isa='list', default=list) 73 _pre_tasks = FieldAttribute(isa='list', default=list) 74 _post_tasks = FieldAttribute(isa='list', default=list) 75 _tasks = FieldAttribute(isa='list', default=list) 76 77 # Flag/Setting Attributes 78 _force_handlers = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('force_handlers'), always_post_validate=True) 79 _max_fail_percentage = FieldAttribute(isa='percent', always_post_validate=True) 80 _serial = FieldAttribute(isa='list', default=list, always_post_validate=True) 81 _strategy = FieldAttribute(isa='string', default=C.DEFAULT_STRATEGY, always_post_validate=True) 82 _order = FieldAttribute(isa='string', always_post_validate=True) 83 84 # ================================================================================= 85 86 def __init__(self): 87 super(Play, self).__init__() 88 89 self._included_conditional = None 90 self._included_path = None 91 self._removed_hosts = [] 92 self.ROLE_CACHE = {} 93 94 self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',)) 95 self.skip_tags = set(context.CLIARGS.get('skip_tags', [])) 96 97 def __repr__(self): 98 return self.get_name() 99 100 def get_name(self): 101 ''' return the name of the Play ''' 102 return self.name 103 104 @staticmethod 105 def load(data, variable_manager=None, loader=None, vars=None): 106 if ('name' not in data or data['name'] is None) and 'hosts' in data: 107 if data['hosts'] is None or all(host is None for host in data['hosts']): 108 raise AnsibleParserError("Hosts list cannot be empty - please check your playbook") 109 if isinstance(data['hosts'], list): 110 data['name'] = ','.join(data['hosts']) 111 else: 112 data['name'] = data['hosts'] 113 p = Play() 114 if vars: 115 p.vars = vars.copy() 116 return p.load_data(data, variable_manager=variable_manager, loader=loader) 117 118 def preprocess_data(self, ds): 119 ''' 120 Adjusts play datastructure to cleanup old/legacy items 121 ''' 122 123 if not isinstance(ds, dict): 124 raise AnsibleAssertionError('while preprocessing data (%s), ds should be a dict but was a %s' % (ds, type(ds))) 125 126 # The use of 'user' in the Play datastructure was deprecated to 127 # line up with the same change for Tasks, due to the fact that 128 # 'user' conflicted with the user module. 129 if 'user' in ds: 130 # this should never happen, but error out with a helpful message 131 # to the user if it does... 132 if 'remote_user' in ds: 133 raise AnsibleParserError("both 'user' and 'remote_user' are set for %s. " 134 "The use of 'user' is deprecated, and should be removed" % self.get_name(), obj=ds) 135 136 ds['remote_user'] = ds['user'] 137 del ds['user'] 138 139 return super(Play, self).preprocess_data(ds) 140 141 def _load_tasks(self, attr, ds): 142 ''' 143 Loads a list of blocks from a list which may be mixed tasks/blocks. 144 Bare tasks outside of a block are given an implicit block. 145 ''' 146 try: 147 return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) 148 except AssertionError as e: 149 raise AnsibleParserError("A malformed block was encountered while loading tasks: %s" % to_native(e), obj=self._ds, orig_exc=e) 150 151 def _load_pre_tasks(self, attr, ds): 152 ''' 153 Loads a list of blocks from a list which may be mixed tasks/blocks. 154 Bare tasks outside of a block are given an implicit block. 155 ''' 156 try: 157 return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) 158 except AssertionError as e: 159 raise AnsibleParserError("A malformed block was encountered while loading pre_tasks", obj=self._ds, orig_exc=e) 160 161 def _load_post_tasks(self, attr, ds): 162 ''' 163 Loads a list of blocks from a list which may be mixed tasks/blocks. 164 Bare tasks outside of a block are given an implicit block. 165 ''' 166 try: 167 return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) 168 except AssertionError as e: 169 raise AnsibleParserError("A malformed block was encountered while loading post_tasks", obj=self._ds, orig_exc=e) 170 171 def _load_handlers(self, attr, ds): 172 ''' 173 Loads a list of blocks from a list which may be mixed handlers/blocks. 174 Bare handlers outside of a block are given an implicit block. 175 ''' 176 try: 177 return self._extend_value( 178 self.handlers, 179 load_list_of_blocks(ds=ds, play=self, use_handlers=True, variable_manager=self._variable_manager, loader=self._loader), 180 prepend=True 181 ) 182 except AssertionError as e: 183 raise AnsibleParserError("A malformed block was encountered while loading handlers", obj=self._ds, orig_exc=e) 184 185 def _load_roles(self, attr, ds): 186 ''' 187 Loads and returns a list of RoleInclude objects from the datastructure 188 list of role definitions and creates the Role from those objects 189 ''' 190 191 if ds is None: 192 ds = [] 193 194 try: 195 role_includes = load_list_of_roles(ds, play=self, variable_manager=self._variable_manager, 196 loader=self._loader, collection_search_list=self.collections) 197 except AssertionError as e: 198 raise AnsibleParserError("A malformed role declaration was encountered.", obj=self._ds, orig_exc=e) 199 200 roles = [] 201 for ri in role_includes: 202 roles.append(Role.load(ri, play=self)) 203 204 self.roles[:0] = roles 205 206 return self.roles 207 208 def _load_vars_prompt(self, attr, ds): 209 new_ds = preprocess_vars(ds) 210 vars_prompts = [] 211 if new_ds is not None: 212 for prompt_data in new_ds: 213 if 'name' not in prompt_data: 214 raise AnsibleParserError("Invalid vars_prompt data structure", obj=ds) 215 else: 216 vars_prompts.append(prompt_data) 217 return vars_prompts 218 219 def _compile_roles(self): 220 ''' 221 Handles the role compilation step, returning a flat list of tasks 222 with the lowest level dependencies first. For example, if a role R 223 has a dependency D1, which also has a dependency D2, the tasks from 224 D2 are merged first, followed by D1, and lastly by the tasks from 225 the parent role R last. This is done for all roles in the Play. 226 ''' 227 228 block_list = [] 229 230 if len(self.roles) > 0: 231 for r in self.roles: 232 # Don't insert tasks from ``import/include_role``, preventing 233 # duplicate execution at the wrong time 234 if r.from_include: 235 continue 236 block_list.extend(r.compile(play=self)) 237 238 return block_list 239 240 def compile_roles_handlers(self): 241 ''' 242 Handles the role handler compilation step, returning a flat list of Handlers 243 This is done for all roles in the Play. 244 ''' 245 246 block_list = [] 247 248 if len(self.roles) > 0: 249 for r in self.roles: 250 if r.from_include: 251 continue 252 block_list.extend(r.get_handler_blocks(play=self)) 253 254 return block_list 255 256 def compile(self): 257 ''' 258 Compiles and returns the task list for this play, compiled from the 259 roles (which are themselves compiled recursively) and/or the list of 260 tasks specified in the play. 261 ''' 262 263 # create a block containing a single flush handlers meta 264 # task, so we can be sure to run handlers at certain points 265 # of the playbook execution 266 flush_block = Block.load( 267 data={'meta': 'flush_handlers'}, 268 play=self, 269 variable_manager=self._variable_manager, 270 loader=self._loader 271 ) 272 273 block_list = [] 274 275 block_list.extend(self.pre_tasks) 276 block_list.append(flush_block) 277 block_list.extend(self._compile_roles()) 278 block_list.extend(self.tasks) 279 block_list.append(flush_block) 280 block_list.extend(self.post_tasks) 281 block_list.append(flush_block) 282 283 return block_list 284 285 def get_vars(self): 286 return self.vars.copy() 287 288 def get_vars_files(self): 289 if self.vars_files is None: 290 return [] 291 elif not isinstance(self.vars_files, list): 292 return [self.vars_files] 293 return self.vars_files 294 295 def get_handlers(self): 296 return self.handlers[:] 297 298 def get_roles(self): 299 return self.roles[:] 300 301 def get_tasks(self): 302 tasklist = [] 303 for task in self.pre_tasks + self.tasks + self.post_tasks: 304 if isinstance(task, Block): 305 tasklist.append(task.block + task.rescue + task.always) 306 else: 307 tasklist.append(task) 308 return tasklist 309 310 def serialize(self): 311 data = super(Play, self).serialize() 312 313 roles = [] 314 for role in self.get_roles(): 315 roles.append(role.serialize()) 316 data['roles'] = roles 317 data['included_path'] = self._included_path 318 319 return data 320 321 def deserialize(self, data): 322 super(Play, self).deserialize(data) 323 324 self._included_path = data.get('included_path', None) 325 if 'roles' in data: 326 role_data = data.get('roles', []) 327 roles = [] 328 for role in role_data: 329 r = Role() 330 r.deserialize(role) 331 roles.append(r) 332 333 setattr(self, 'roles', roles) 334 del data['roles'] 335 336 def copy(self): 337 new_me = super(Play, self).copy() 338 new_me.ROLE_CACHE = self.ROLE_CACHE.copy() 339 new_me._included_conditional = self._included_conditional 340 new_me._included_path = self._included_path 341 return new_me 342