PageRenderTime 93ms CodeModel.GetById 11ms app.highlight 73ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/ansible/playbook/play.py

https://github.com/ajanthanm/ansible
Python | 855 lines | 698 code | 82 blank | 75 comment | 142 complexity | fb3764debef9a5653a83e1ee58ebcfa6 MD5 | raw file
  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#############################################
 19
 20from ansible.utils.template import template
 21from ansible import utils
 22from ansible import errors
 23from ansible.playbook.task import Task
 24import ansible.constants as C
 25import pipes
 26import shlex
 27import os
 28import sys
 29import uuid
 30
 31class Play(object):
 32
 33    __slots__ = [
 34       'hosts', 'name', 'vars', 'default_vars', 'vars_prompt', 'vars_files',
 35       'handlers', 'remote_user', 'remote_port', 'included_roles', 'accelerate',
 36       'accelerate_port', 'accelerate_ipv6', 'sudo', 'sudo_user', 'transport', 'playbook',
 37       'tags', 'gather_facts', 'serial', '_ds', '_handlers', '_tasks',
 38       'basedir', 'any_errors_fatal', 'roles', 'max_fail_pct', '_play_hosts', 'su', 'su_user', 'vault_password'
 39    ]
 40
 41    # to catch typos and so forth -- these are userland names
 42    # and don't line up 1:1 with how they are stored
 43    VALID_KEYS = [
 44       'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
 45       'tasks', 'handlers', 'remote_user', 'user', 'port', 'include', 'accelerate', 'accelerate_port', 'accelerate_ipv6',
 46       'sudo', 'sudo_user', 'connection', 'tags', 'gather_facts', 'serial',
 47       'any_errors_fatal', 'roles', 'role_names', 'pre_tasks', 'post_tasks', 'max_fail_percentage',
 48       'su', 'su_user', 'vault_password'
 49    ]
 50
 51    # *************************************************
 52
 53    def __init__(self, playbook, ds, basedir, vault_password=None):
 54        ''' constructor loads from a play datastructure '''
 55
 56        for x in ds.keys():
 57            if not x in Play.VALID_KEYS:
 58                raise errors.AnsibleError("%s is not a legal parameter in an Ansible Playbook" % x)
 59
 60        # allow all playbook keys to be set by --extra-vars
 61        self.vars             = ds.get('vars', {})
 62        self.vars_prompt      = ds.get('vars_prompt', {})
 63        self.playbook         = playbook
 64        self.vars             = self._get_vars()
 65        self.basedir          = basedir
 66        self.roles            = ds.get('roles', None)
 67        self.tags             = ds.get('tags', None)
 68        self.vault_password   = vault_password
 69
 70        if self.tags is None:
 71            self.tags = []
 72        elif type(self.tags) in [ str, unicode ]:
 73            self.tags = self.tags.split(",")
 74        elif type(self.tags) != list:
 75            self.tags = []
 76
 77        # We first load the vars files from the datastructure
 78        # so we have the default variables to pass into the roles
 79        self.vars_files = ds.get('vars_files', [])
 80        if not isinstance(self.vars_files, list):
 81            raise errors.AnsibleError('vars_files must be a list')
 82        self._update_vars_files_for_host(None)
 83
 84        # now we load the roles into the datastructure
 85        self.included_roles = []
 86        ds = self._load_roles(self.roles, ds)
 87        
 88        # and finally re-process the vars files as they may have
 89        # been updated by the included roles
 90        self.vars_files = ds.get('vars_files', [])
 91        if not isinstance(self.vars_files, list):
 92            raise errors.AnsibleError('vars_files must be a list')
 93
 94        self._update_vars_files_for_host(None)
 95
 96        # apply any extra_vars specified on the command line now
 97        if type(self.playbook.extra_vars) == dict:
 98            self.vars = utils.combine_vars(self.vars, self.playbook.extra_vars)
 99
100        # template everything to be efficient, but do not pre-mature template
101        # tasks/handlers as they may have inventory scope overrides
102        _tasks    = ds.pop('tasks', [])
103        _handlers = ds.pop('handlers', [])
104        ds = template(basedir, ds, self.vars)
105        ds['tasks'] = _tasks
106        ds['handlers'] = _handlers
107
108        self._ds = ds
109
110        hosts = ds.get('hosts')
111        if hosts is None:
112            raise errors.AnsibleError('hosts declaration is required')
113        elif isinstance(hosts, list):
114            hosts = ';'.join(hosts)
115        self.serial           = int(ds.get('serial', 0))
116        self.hosts            = hosts
117        self.name             = ds.get('name', self.hosts)
118        self._tasks           = ds.get('tasks', [])
119        self._handlers        = ds.get('handlers', [])
120        self.remote_user      = ds.get('remote_user', ds.get('user', self.playbook.remote_user))
121        self.remote_port      = ds.get('port', self.playbook.remote_port)
122        self.sudo             = ds.get('sudo', self.playbook.sudo)
123        self.sudo_user        = ds.get('sudo_user', self.playbook.sudo_user)
124        self.transport        = ds.get('connection', self.playbook.transport)
125        self.remote_port      = self.remote_port
126        self.any_errors_fatal = utils.boolean(ds.get('any_errors_fatal', 'false'))
127        self.accelerate       = utils.boolean(ds.get('accelerate', 'false'))
128        self.accelerate_port  = ds.get('accelerate_port', None)
129        self.accelerate_ipv6  = ds.get('accelerate_ipv6', False)
130        self.max_fail_pct     = int(ds.get('max_fail_percentage', 100))
131        self.su               = ds.get('su', self.playbook.su)
132        self.su_user          = ds.get('su_user', self.playbook.su_user)
133
134        # gather_facts is not a simple boolean, as None means  that a 'smart'
135        # fact gathering mode will be used, so we need to be careful here as
136        # calling utils.boolean(None) returns False
137        self.gather_facts = ds.get('gather_facts', None)
138        if self.gather_facts:
139            self.gather_facts = utils.boolean(self.gather_facts)
140
141        # Fail out if user specifies a sudo param with a su param in a given play
142        if (ds.get('sudo') or ds.get('sudo_user')) and (ds.get('su') or ds.get('su_user')):
143            raise errors.AnsibleError('sudo params ("sudo", "sudo_user") and su params '
144                                      '("su", "su_user") cannot be used together')
145
146        load_vars = {}
147        load_vars['role_names'] = ds.get('role_names',[])
148        load_vars['playbook_dir'] = self.basedir
149        if self.playbook.inventory.basedir() is not None:
150            load_vars['inventory_dir'] = self.playbook.inventory.basedir()
151
152        self._tasks      = self._load_tasks(self._ds.get('tasks', []), load_vars)
153        self._handlers   = self._load_tasks(self._ds.get('handlers', []), load_vars)
154
155        # apply any missing tags to role tasks
156        self._late_merge_role_tags()
157
158        if self.sudo_user != 'root':
159            self.sudo = True
160
161        # place holder for the discovered hosts to be used in this play
162        self._play_hosts = None
163
164    # *************************************************
165
166    def _get_role_path(self, role):
167        """
168        Returns the path on disk to the directory containing
169        the role directories like tasks, templates, etc. Also 
170        returns any variables that were included with the role
171        """
172        orig_path = template(self.basedir,role,self.vars)
173
174        role_vars = {}
175        if type(orig_path) == dict:
176            # what, not a path?
177            role_name = orig_path.get('role', None)
178            if role_name is None:
179                raise errors.AnsibleError("expected a role name in dictionary: %s" % orig_path)
180            role_vars = orig_path
181            orig_path = role_name
182
183        role_path = None
184
185        possible_paths = [
186            utils.path_dwim(self.basedir, os.path.join('roles', orig_path)),
187            utils.path_dwim(self.basedir, orig_path)
188        ]
189
190        if C.DEFAULT_ROLES_PATH:
191            search_locations = C.DEFAULT_ROLES_PATH.split(os.pathsep)
192            for loc in search_locations:
193                loc = os.path.expanduser(loc)
194                possible_paths.append(utils.path_dwim(loc, orig_path))
195
196        for path_option in possible_paths:
197            if os.path.isdir(path_option):
198                role_path = path_option
199                break
200
201        if role_path is None:
202            raise errors.AnsibleError("cannot find role in %s" % " or ".join(possible_paths))
203
204        return (role_path, role_vars)
205
206    def _build_role_dependencies(self, roles, dep_stack, passed_vars={}, level=0):
207        # this number is arbitrary, but it seems sane
208        if level > 20:
209            raise errors.AnsibleError("too many levels of recursion while resolving role dependencies")
210        for role in roles:
211            role_path,role_vars = self._get_role_path(role)
212            role_vars = utils.combine_vars(passed_vars, role_vars)
213            vars = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'vars')))
214            vars_data = {}
215            if os.path.isfile(vars):
216                vars_data = utils.parse_yaml_from_file(vars, vault_password=self.vault_password)
217                if vars_data:
218                    if not isinstance(vars_data, dict):
219                        raise errors.AnsibleError("vars from '%s' are not a dict" % vars)
220                    role_vars = utils.combine_vars(vars_data, role_vars)
221            defaults = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'defaults')))
222            defaults_data = {}
223            if os.path.isfile(defaults):
224                defaults_data = utils.parse_yaml_from_file(defaults, vault_password=self.vault_password)
225            # the meta directory contains the yaml that should
226            # hold the list of dependencies (if any)
227            meta = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'meta')))
228            if os.path.isfile(meta):
229                data = utils.parse_yaml_from_file(meta, vault_password=self.vault_password)
230                if data:
231                    dependencies = data.get('dependencies',[])
232                    if dependencies is None:
233                        dependencies = []
234                    for dep in dependencies:
235                        allow_dupes = False
236                        (dep_path,dep_vars) = self._get_role_path(dep)
237                        meta = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'meta')))
238                        if os.path.isfile(meta):
239                            meta_data = utils.parse_yaml_from_file(meta, vault_password=self.vault_password)
240                            if meta_data:
241                                allow_dupes = utils.boolean(meta_data.get('allow_duplicates',''))
242
243                        # if any tags were specified as role/dep variables, merge
244                        # them into the current dep_vars so they're passed on to any 
245                        # further dependencies too, and so we only have one place
246                        # (dep_vars) to look for tags going forward
247                        def __merge_tags(var_obj):
248                            old_tags = dep_vars.get('tags', [])
249                            if isinstance(old_tags, basestring):
250                                old_tags = [old_tags, ]
251                            if isinstance(var_obj, dict):
252                                new_tags = var_obj.get('tags', [])
253                                if isinstance(new_tags, basestring):
254                                    new_tags = [new_tags, ]
255                            else:
256                                new_tags = []
257                            return list(set(old_tags).union(set(new_tags)))
258
259                        dep_vars['tags'] = __merge_tags(role_vars)
260                        dep_vars['tags'] = __merge_tags(passed_vars)
261
262                        # if tags are set from this role, merge them
263                        # into the tags list for the dependent role
264                        if "tags" in passed_vars:
265                            for included_role_dep in dep_stack:
266                                included_dep_name = included_role_dep[0]
267                                included_dep_vars = included_role_dep[2]
268                                if included_dep_name == dep:
269                                    if "tags" in included_dep_vars:
270                                        included_dep_vars["tags"] = list(set(included_dep_vars["tags"]).union(set(passed_vars["tags"])))
271                                    else:
272                                        included_dep_vars["tags"] = passed_vars["tags"][:]
273
274                        dep_vars = utils.combine_vars(passed_vars, dep_vars)
275                        dep_vars = utils.combine_vars(role_vars, dep_vars)
276                        vars = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'vars')))
277                        vars_data = {}
278                        if os.path.isfile(vars):
279                            vars_data = utils.parse_yaml_from_file(vars, vault_password=self.vault_password)
280                            if vars_data:
281                                dep_vars = utils.combine_vars(vars_data, dep_vars)
282                        defaults = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'defaults')))
283                        dep_defaults_data = {}
284                        if os.path.isfile(defaults):
285                            dep_defaults_data = utils.parse_yaml_from_file(defaults, vault_password=self.vault_password)
286                        if 'role' in dep_vars:
287                            del dep_vars['role']
288
289                        if not allow_dupes:
290                            if dep in self.included_roles:
291                                # skip back to the top, since we don't want to
292                                # do anything else with this role
293                                continue
294                            else:
295                                self.included_roles.append(dep)
296
297                        def _merge_conditional(cur_conditionals, new_conditionals):
298                            if isinstance(new_conditionals, (basestring, bool)):
299                                cur_conditionals.append(new_conditionals)
300                            elif isinstance(new_conditionals, list):
301                                cur_conditionals.extend(new_conditionals)
302
303                        # pass along conditionals from roles to dep roles
304                        passed_when = passed_vars.get('when')
305                        role_when = role_vars.get('when')
306                        dep_when = dep_vars.get('when')
307
308                        tmpcond = []
309                        _merge_conditional(tmpcond, passed_when)
310                        _merge_conditional(tmpcond, role_when)
311                        _merge_conditional(tmpcond, dep_when)
312
313                        if len(tmpcond) > 0:
314                            dep_vars['when'] = tmpcond
315
316                        self._build_role_dependencies([dep], dep_stack, passed_vars=dep_vars, level=level+1)
317                        dep_stack.append([dep,dep_path,dep_vars,dep_defaults_data])
318
319            # only add the current role when we're at the top level,
320            # otherwise we'll end up in a recursive loop 
321            if level == 0:
322                self.included_roles.append(role)
323                dep_stack.append([role,role_path,role_vars,defaults_data])
324        return dep_stack
325
326    def _load_role_defaults(self, defaults_files):
327        # process default variables
328        default_vars = {}
329        for filename in defaults_files:
330            if os.path.exists(filename):
331                new_default_vars = utils.parse_yaml_from_file(filename, vault_password=self.vault_password)
332                if new_default_vars:
333                    if type(new_default_vars) != dict:
334                        raise errors.AnsibleError("%s must be stored as dictionary/hash: %s" % (filename, type(new_default_vars)))
335                    default_vars = utils.combine_vars(default_vars, new_default_vars)
336
337        return default_vars
338
339    def _load_roles(self, roles, ds):
340        # a role is a name that auto-includes the following if they exist
341        #    <rolename>/tasks/main.yml
342        #    <rolename>/handlers/main.yml
343        #    <rolename>/vars/main.yml
344        #    <rolename>/library
345        # and it auto-extends tasks/handlers/vars_files/module paths as appropriate if found
346
347        if roles is None:
348            roles = []
349        if type(roles) != list:
350            raise errors.AnsibleError("value of 'roles:' must be a list")
351
352        new_tasks = []
353        new_handlers = []
354        new_vars_files = []
355        defaults_files = []
356
357        pre_tasks = ds.get('pre_tasks', None)
358        if type(pre_tasks) != list:
359            pre_tasks = []
360        for x in pre_tasks:
361            new_tasks.append(x)
362
363        # flush handlers after pre_tasks
364        new_tasks.append(dict(meta='flush_handlers'))
365
366        roles = self._build_role_dependencies(roles, [], self.vars)
367
368        # give each role a uuid
369        for idx, val in enumerate(roles):
370            this_uuid = str(uuid.uuid4())
371            roles[idx][-2]['role_uuid'] = this_uuid
372
373        role_names = []
374
375        for (role,role_path,role_vars,default_vars) in roles:
376            # special vars must be extracted from the dict to the included tasks
377            special_keys = [ "sudo", "sudo_user", "when", "with_items" ]
378            special_vars = {}
379            for k in special_keys:
380                if k in role_vars:
381                    special_vars[k] = role_vars[k]
382
383            task_basepath     = utils.path_dwim(self.basedir, os.path.join(role_path, 'tasks'))
384            handler_basepath  = utils.path_dwim(self.basedir, os.path.join(role_path, 'handlers'))
385            vars_basepath     = utils.path_dwim(self.basedir, os.path.join(role_path, 'vars'))
386            meta_basepath     = utils.path_dwim(self.basedir, os.path.join(role_path, 'meta'))
387            defaults_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'defaults'))
388
389            task      = self._resolve_main(task_basepath)
390            handler   = self._resolve_main(handler_basepath)
391            vars_file = self._resolve_main(vars_basepath)
392            meta_file = self._resolve_main(meta_basepath)
393            defaults_file = self._resolve_main(defaults_basepath)
394
395            library   = utils.path_dwim(self.basedir, os.path.join(role_path, 'library'))
396
397            missing = lambda f: not os.path.isfile(f)
398            if missing(task) and missing(handler) and missing(vars_file) and missing(defaults_file) and missing(meta_file) and missing(library):
399                raise errors.AnsibleError("found role at %s, but cannot find %s or %s or %s or %s or %s or %s" % (role_path, task, handler, vars_file, defaults_file, meta_file, library))
400
401            if isinstance(role, dict):
402                role_name = role['role']
403            else:
404                role_name = role
405
406            role_names.append(role_name)
407            if os.path.isfile(task):
408                nt = dict(include=pipes.quote(task), vars=role_vars, default_vars=default_vars, role_name=role_name)
409                for k in special_keys:
410                    if k in special_vars:
411                        nt[k] = special_vars[k]
412                new_tasks.append(nt)
413            if os.path.isfile(handler):
414                nt = dict(include=pipes.quote(handler), vars=role_vars, role_name=role_name)
415                for k in special_keys:
416                    if k in special_vars:
417                        nt[k] = special_vars[k]
418                new_handlers.append(nt)
419            if os.path.isfile(vars_file):
420                new_vars_files.append(vars_file)
421            if os.path.isfile(defaults_file):
422                defaults_files.append(defaults_file)
423            if os.path.isdir(library):
424                utils.plugins.module_finder.add_directory(library)
425
426        tasks      = ds.get('tasks', None)
427        post_tasks = ds.get('post_tasks', None)
428        handlers   = ds.get('handlers', None)
429        vars_files = ds.get('vars_files', None)
430
431        if type(tasks) != list:
432            tasks = []
433        if type(handlers) != list:
434            handlers = []
435        if type(vars_files) != list:
436            vars_files = []
437        if type(post_tasks) != list:
438            post_tasks = []
439
440        new_tasks.extend(tasks)
441        # flush handlers after tasks + role tasks
442        new_tasks.append(dict(meta='flush_handlers'))
443        new_tasks.extend(post_tasks)
444        # flush handlers after post tasks
445        new_tasks.append(dict(meta='flush_handlers'))
446
447        new_handlers.extend(handlers)
448        new_vars_files.extend(vars_files)
449
450        ds['tasks'] = new_tasks
451        ds['handlers'] = new_handlers
452        ds['vars_files'] = new_vars_files
453        ds['role_names'] = role_names
454
455        self.default_vars = self._load_role_defaults(defaults_files)
456
457        return ds
458
459    # *************************************************
460
461    def _resolve_main(self, basepath):
462        ''' flexibly handle variations in main filenames '''
463        # these filenames are acceptable:
464        mains = (
465                 os.path.join(basepath, 'main'),
466                 os.path.join(basepath, 'main.yml'),
467                 os.path.join(basepath, 'main.yaml'),
468                 os.path.join(basepath, 'main.json'),
469                )
470        if sum([os.path.isfile(x) for x in mains]) > 1:
471            raise errors.AnsibleError("found multiple main files at %s, only one allowed" % (basepath))
472        else:
473            for m in mains:
474                if os.path.isfile(m):
475                    return m # exactly one main file
476            return mains[0] # zero mains (we still need to return something)
477
478    # *************************************************
479
480    def _load_tasks(self, tasks, vars=None, default_vars=None, sudo_vars=None,
481                    additional_conditions=None, original_file=None, role_name=None):
482        ''' handle task and handler include statements '''
483
484        results = []
485        if tasks is None:
486            # support empty handler files, and the like.
487            tasks = []
488        if additional_conditions is None:
489            additional_conditions = []
490        if vars is None:
491            vars = {}
492        if default_vars is None:
493            default_vars = {}
494        if sudo_vars is None:
495            sudo_vars = {}
496
497        old_conditions = list(additional_conditions)
498
499        for x in tasks:
500
501            # prevent assigning the same conditions to each task on an include
502            included_additional_conditions = list(old_conditions)
503
504            if not isinstance(x, dict):
505                raise errors.AnsibleError("expecting dict; got: %s, error in %s" % (x, original_file))
506
507            # evaluate sudo vars for current and child tasks 
508            included_sudo_vars = {}
509            for k in ["sudo", "sudo_user"]:
510                if k in x:
511                    included_sudo_vars[k] = x[k]
512                elif k in sudo_vars:
513                    included_sudo_vars[k] = sudo_vars[k]
514                    x[k] = sudo_vars[k]
515
516            if 'meta' in x:
517                if x['meta'] == 'flush_handlers':
518                    results.append(Task(self, x))
519                    continue
520
521            task_vars = self.vars.copy()
522            task_vars.update(vars)
523            if original_file:
524                task_vars['_original_file'] = original_file
525
526            if 'include' in x:
527                tokens = shlex.split(str(x['include']))
528                included_additional_conditions = list(additional_conditions)
529                include_vars = {}
530                for k in x:
531                    if k.startswith("with_"):
532                        if original_file:
533                            offender = " (in %s)" % original_file
534                        else:
535                            offender = ""
536                        utils.deprecated("include + with_items is a removed deprecated feature" + offender, "1.5", removed=True)
537                    elif k.startswith("when_"):
538                        utils.deprecated("\"when_<criteria>:\" is a removed deprecated feature, use the simplified 'when:' conditional directly", None, removed=True)
539                    elif k == 'when':
540                        if type(x[k]) is str:
541                            included_additional_conditions.insert(0, x[k])
542                        elif type(x[k]) is list:
543                            for i in x[k]:
544                                included_additional_conditions.insert(0, i)
545                    elif k in ("include", "vars", "default_vars", "sudo", "sudo_user", "role_name", "no_log"):
546                        continue
547                    else:
548                        include_vars[k] = x[k]
549
550                default_vars = x.get('default_vars', {})
551                if not default_vars:
552                    default_vars = self.default_vars
553                else:
554                    default_vars = utils.combine_vars(self.default_vars, default_vars)
555
556                # append the vars defined with the include (from above) 
557                # as well as the old-style 'vars' element. The old-style
558                # vars are given higher precedence here (just in case)
559                task_vars = utils.combine_vars(task_vars, include_vars)
560                if 'vars' in x:
561                    task_vars = utils.combine_vars(task_vars, x['vars'])
562
563                if 'when' in x:
564                    if isinstance(x['when'], (basestring, bool)):
565                        included_additional_conditions.append(x['when'])
566                    elif isinstance(x['when'], list):
567                        included_additional_conditions.extend(x['when'])
568
569                new_role = None
570                if 'role_name' in x:
571                    new_role = x['role_name']
572
573                mv = task_vars.copy()
574                for t in tokens[1:]:
575                    (k,v) = t.split("=", 1)
576                    mv[k] = template(self.basedir, v, mv)
577                dirname = self.basedir
578                if original_file:
579                    dirname = os.path.dirname(original_file)
580                include_file = template(dirname, tokens[0], mv)
581                include_filename = utils.path_dwim(dirname, include_file)
582                data = utils.parse_yaml_from_file(include_filename, vault_password=self.vault_password)
583                if 'role_name' in x and data is not None:
584                    for y in data:
585                        if isinstance(y, dict) and 'include' in y:
586                            y['role_name'] = new_role
587                loaded = self._load_tasks(data, mv, default_vars, included_sudo_vars, list(included_additional_conditions), original_file=include_filename, role_name=new_role)
588                results += loaded
589            elif type(x) == dict:
590                task = Task(
591                    self, x,
592                    module_vars=task_vars,
593                    default_vars=default_vars,
594                    additional_conditions=list(additional_conditions),
595                    role_name=role_name
596                )
597                results.append(task)
598            else:
599                raise Exception("unexpected task type")
600
601        for x in results:
602            if self.tags is not None:
603                x.tags.extend(self.tags)
604
605        return results
606
607    # *************************************************
608
609    def _is_valid_tag(self, tag_list):
610        """
611        Check to see if the list of tags passed in is in the list of tags 
612        we only want (playbook.only_tags), or if it is not in the list of 
613        tags we don't want (playbook.skip_tags).
614        """
615        matched_skip_tags = set(tag_list) & set(self.playbook.skip_tags)
616        matched_only_tags = set(tag_list) & set(self.playbook.only_tags)
617        if len(matched_skip_tags) > 0 or (self.playbook.only_tags != ['all'] and len(matched_only_tags) == 0):
618            return False
619        return True
620
621    # *************************************************
622
623    def tasks(self):
624        ''' return task objects for this play '''
625        return self._tasks
626
627    def handlers(self):
628        ''' return handler objects for this play '''
629        return self._handlers
630
631    # *************************************************
632
633    def _get_vars(self):
634        ''' load the vars section from a play, accounting for all sorts of variable features
635        including loading from yaml files, prompting, and conditional includes of the first
636        file found in a list. '''
637
638        if self.vars is None:
639            self.vars = {}
640
641        if type(self.vars) not in [dict, list]:
642            raise errors.AnsibleError("'vars' section must contain only key/value pairs")
643
644        vars = {}
645
646        # translate a list of vars into a dict
647        if type(self.vars) == list:
648            for item in self.vars:
649                if getattr(item, 'items', None) is None:
650                    raise errors.AnsibleError("expecting a key-value pair in 'vars' section")
651                k, v = item.items()[0]
652                vars[k] = v
653        else:
654            vars.update(self.vars)
655
656        if type(self.vars_prompt) == list:
657            for var in self.vars_prompt:
658                if not 'name' in var:
659                    raise errors.AnsibleError("'vars_prompt' item is missing 'name:'")
660
661                vname = var['name']
662                prompt = var.get("prompt", vname)
663                default = var.get("default", None)
664                private = var.get("private", True)
665
666                confirm = var.get("confirm", False)
667                encrypt = var.get("encrypt", None)
668                salt_size = var.get("salt_size", None)
669                salt = var.get("salt", None)
670
671                if vname not in self.playbook.extra_vars:
672                    vars[vname] = self.playbook.callbacks.on_vars_prompt(
673                                     vname, private, prompt, encrypt, confirm, salt_size, salt, default
674                                  )
675
676        elif type(self.vars_prompt) == dict:
677            for (vname, prompt) in self.vars_prompt.iteritems():
678                prompt_msg = "%s: " % prompt
679                if vname not in self.playbook.extra_vars:
680                    vars[vname] = self.playbook.callbacks.on_vars_prompt(
681                                     varname=vname, private=False, prompt=prompt_msg, default=None
682                                  )
683
684        else:
685            raise errors.AnsibleError("'vars_prompt' section is malformed, see docs")
686
687        if type(self.playbook.extra_vars) == dict:
688            vars = utils.combine_vars(vars, self.playbook.extra_vars)
689
690        return vars
691
692    # *************************************************
693
694    def update_vars_files(self, hosts, vault_password=None):
695        ''' calculate vars_files, which requires that setup runs first so ansible facts can be mixed in '''
696
697        # now loop through all the hosts...
698        for h in hosts:
699            self._update_vars_files_for_host(h, vault_password=vault_password)
700
701    # *************************************************
702
703    def compare_tags(self, tags):
704        ''' given a list of tags that the user has specified, return two lists:
705        matched_tags:   tags were found within the current play and match those given
706                        by the user
707        unmatched_tags: tags that were found within the current play but do not match
708                        any provided by the user '''
709
710        # gather all the tags in all the tasks and handlers into one list
711        # FIXME: isn't this in self.tags already?
712
713        all_tags = []
714        for task in self._tasks:
715            if not task.meta:
716                all_tags.extend(task.tags)
717        for handler in self._handlers:
718            all_tags.extend(handler.tags)
719
720        # compare the lists of tags using sets and return the matched and unmatched
721        all_tags_set = set(all_tags)
722        tags_set = set(tags)
723        matched_tags = all_tags_set & tags_set
724        unmatched_tags = all_tags_set - tags_set
725
726        return matched_tags, unmatched_tags
727
728    # *************************************************
729
730    def _late_merge_role_tags(self):
731        # build a local dict of tags for roles
732        role_tags = {}
733        for task in self._ds['tasks']:
734            if 'role_name' in task:
735                this_role = task['role_name'] + "-" + task['vars']['role_uuid']
736
737                if this_role not in role_tags:
738                    role_tags[this_role] = []
739
740                if 'tags' in task['vars']:
741                    if isinstance(task['vars']['tags'], basestring):
742                        role_tags[this_role] += shlex.split(task['vars']['tags'])
743                    else:
744                        role_tags[this_role] += task['vars']['tags']
745
746        # apply each role's tags to it's tasks
747        for idx, val in enumerate(self._tasks):
748            if getattr(val, 'role_name', None) is not None:
749                this_role = val.role_name + "-" + val.module_vars['role_uuid']
750                if this_role in role_tags:
751                    self._tasks[idx].tags = sorted(set(self._tasks[idx].tags + role_tags[this_role]))
752
753    # *************************************************
754
755    def _has_vars_in(self, msg):
756        return "$" in msg or "{{" in msg
757
758    # *************************************************
759
760    def _update_vars_files_for_host(self, host, vault_password=None):
761
762        def generate_filenames(host, inject, filename):
763
764            """ Render the raw filename into 3 forms """
765
766            filename2 = template(self.basedir, filename, self.vars)
767            filename3 = filename2
768            if host is not None:
769                filename3 = template(self.basedir, filename2, inject)
770            if self._has_vars_in(filename3) and host is not None:
771                # allow play scoped vars and host scoped vars to template the filepath
772                inject.update(self.vars)
773                filename4 = template(self.basedir, filename3, inject)
774                filename4 = utils.path_dwim(self.basedir, filename4)
775            else:    
776                filename4 = utils.path_dwim(self.basedir, filename3)
777            return filename2, filename3, filename4
778
779
780        def update_vars_cache(host, inject, data, filename):
781
782            """ update a host's varscache with new var data """
783
784            data = utils.combine_vars(inject, data)
785            self.playbook.VARS_CACHE[host] = utils.combine_vars(self.playbook.VARS_CACHE.get(host, {}), data)
786            self.playbook.callbacks.on_import_for_host(host, filename4)
787
788        def process_files(filename, filename2, filename3, filename4, host=None):
789
790            """ pseudo-algorithm for deciding where new vars should go """
791
792            data = utils.parse_yaml_from_file(filename4, vault_password=self.vault_password)
793            if data:
794                if type(data) != dict:
795                    raise errors.AnsibleError("%s must be stored as a dictionary/hash" % filename4)
796                if host is not None:
797                    if self._has_vars_in(filename2) and not self._has_vars_in(filename3):
798                        # running a host specific pass and has host specific variables
799                        # load into setup cache
800                        update_vars_cache(host, inject, data, filename4)
801                    elif self._has_vars_in(filename3) and not self._has_vars_in(filename4):
802                        # handle mixed scope variables in filepath
803                        update_vars_cache(host, inject, data, filename4)
804
805                elif not self._has_vars_in(filename4):
806                    # found a non-host specific variable, load into vars and NOT
807                    # the setup cache
808                    if host is not None:
809                        self.vars.update(data)
810                    else:
811                        self.vars = utils.combine_vars(self.vars, data)
812
813        # Enforce that vars_files is always a list
814        if type(self.vars_files) != list:
815            self.vars_files = [ self.vars_files ]
816
817        # Build an inject if this is a host run started by self.update_vars_files
818        if host is not None:
819            inject = {}
820            inject.update(self.playbook.inventory.get_variables(host, vault_password=vault_password))
821            inject.update(self.playbook.SETUP_CACHE.get(host, {}))
822            inject.update(self.playbook.VARS_CACHE.get(host, {}))
823        else:
824            inject = None            
825
826        for filename in self.vars_files:
827            if type(filename) == list:
828                # loop over all filenames, loading the first one, and failing if none found
829                found = False
830                sequence = []
831                for real_filename in filename:
832                    filename2, filename3, filename4 = generate_filenames(host, inject, real_filename)
833                    sequence.append(filename4)
834                    if os.path.exists(filename4):
835                        found = True
836                        process_files(filename, filename2, filename3, filename4, host=host)
837                    elif host is not None:
838                        self.playbook.callbacks.on_not_import_for_host(host, filename4)
839                    if found:
840                        break
841                if not found and host is not None:
842                    raise errors.AnsibleError(
843                        "%s: FATAL, no files matched for vars_files import sequence: %s" % (host, sequence)
844                    )
845
846            else:
847                # just one filename supplied, load it!
848                filename2, filename3, filename4 = generate_filenames(host, inject, filename)
849                if self._has_vars_in(filename4):
850                    continue
851                process_files(filename, filename2, filename3, filename4, host=host)
852
853        # finally, update the VARS_CACHE for the host, if it is set
854        if host is not None:
855            self.playbook.VARS_CACHE[host].update(self.playbook.extra_vars)