/lib/ansible/playbook/play.py

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