1# 2# This file is part of Ansible 3# 4# Ansible is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# Ansible is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 16 17# Make coding more python3-ish 18from __future__ import (absolute_import, division, print_function) 19__metaclass__ = type 20 21from os.path import basename 22 23import ansible.constants as C 24from ansible.errors import AnsibleParserError 25from ansible.playbook.attribute import FieldAttribute 26from ansible.playbook.block import Block 27from ansible.playbook.task_include import TaskInclude 28from ansible.playbook.role import Role 29from ansible.playbook.role.include import RoleInclude 30from ansible.utils.display import Display 31from ansible.module_utils.six import string_types 32 33__all__ = ['IncludeRole'] 34 35display = Display() 36 37 38class IncludeRole(TaskInclude): 39 40 """ 41 A Role include is derived from a regular role to handle the special 42 circumstances related to the `- include_role: ...` 43 """ 44 45 BASE = ('name', 'role') # directly assigned 46 FROM_ARGS = ('tasks_from', 'vars_from', 'defaults_from', 'handlers_from') # used to populate from dict in role 47 OTHER_ARGS = ('apply', 'public', 'allow_duplicates', 'rolespec_validate') # assigned to matching property 48 VALID_ARGS = tuple(frozenset(BASE + FROM_ARGS + OTHER_ARGS)) # all valid args 49 50 # ================================================================================= 51 # ATTRIBUTES 52 53 # private as this is a 'module options' vs a task property 54 _allow_duplicates = FieldAttribute(isa='bool', default=True, private=True) 55 _public = FieldAttribute(isa='bool', default=False, private=True) 56 _rolespec_validate = FieldAttribute(isa='bool', default=True) 57 58 def __init__(self, block=None, role=None, task_include=None): 59 60 super(IncludeRole, self).__init__(block=block, role=role, task_include=task_include) 61 62 self._from_files = {} 63 self._parent_role = role 64 self._role_name = None 65 self._role_path = None 66 67 def get_name(self): 68 ''' return the name of the task ''' 69 return self.name or "%s : %s" % (self.action, self._role_name) 70 71 def get_block_list(self, play=None, variable_manager=None, loader=None): 72 73 # only need play passed in when dynamic 74 if play is None: 75 myplay = self._parent._play 76 else: 77 myplay = play 78 79 ri = RoleInclude.load(self._role_name, play=myplay, variable_manager=variable_manager, loader=loader, collection_list=self.collections) 80 ri.vars.update(self.vars) 81 82 # build role 83 actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files, 84 from_include=True, validate=self.rolespec_validate) 85 actual_role._metadata.allow_duplicates = self.allow_duplicates 86 87 if self.statically_loaded or self.public: 88 myplay.roles.append(actual_role) 89 90 # save this for later use 91 self._role_path = actual_role._role_path 92 93 # compile role with parent roles as dependencies to ensure they inherit 94 # variables 95 if not self._parent_role: 96 dep_chain = [] 97 else: 98 dep_chain = list(self._parent_role._parents) 99 dep_chain.append(self._parent_role) 100 101 p_block = self.build_parent_block() 102 103 # collections value is not inherited; override with the value we calculated during role setup 104 p_block.collections = actual_role.collections 105 106 blocks = actual_role.compile(play=myplay, dep_chain=dep_chain) 107 for b in blocks: 108 b._parent = p_block 109 # HACK: parent inheritance doesn't seem to have a way to handle this intermediate override until squashed/finalized 110 b.collections = actual_role.collections 111 112 # updated available handlers in play 113 handlers = actual_role.get_handler_blocks(play=myplay) 114 for h in handlers: 115 h._parent = p_block 116 myplay.handlers = myplay.handlers + handlers 117 return blocks, handlers 118 119 @staticmethod 120 def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): 121 122 ir = IncludeRole(block, role, task_include=task_include).load_data(data, variable_manager=variable_manager, loader=loader) 123 124 # Validate options 125 my_arg_names = frozenset(ir.args.keys()) 126 127 # name is needed, or use role as alias 128 ir._role_name = ir.args.get('name', ir.args.get('role')) 129 if ir._role_name is None: 130 raise AnsibleParserError("'name' is a required field for %s." % ir.action, obj=data) 131 132 if 'public' in ir.args and ir.action not in C._ACTION_INCLUDE_ROLE: 133 raise AnsibleParserError('Invalid options for %s: public' % ir.action, obj=data) 134 135 # validate bad args, otherwise we silently ignore 136 bad_opts = my_arg_names.difference(IncludeRole.VALID_ARGS) 137 if bad_opts: 138 raise AnsibleParserError('Invalid options for %s: %s' % (ir.action, ','.join(list(bad_opts))), obj=data) 139 140 # build options for role includes 141 for key in my_arg_names.intersection(IncludeRole.FROM_ARGS): 142 from_key = key.replace('_from', '') 143 args_value = ir.args.get(key) 144 if not isinstance(args_value, string_types): 145 raise AnsibleParserError('Expected a string for %s but got %s instead' % (key, type(args_value))) 146 ir._from_files[from_key] = basename(args_value) 147 148 apply_attrs = ir.args.get('apply', {}) 149 if apply_attrs and ir.action not in C._ACTION_INCLUDE_ROLE: 150 raise AnsibleParserError('Invalid options for %s: apply' % ir.action, obj=data) 151 elif not isinstance(apply_attrs, dict): 152 raise AnsibleParserError('Expected a dict for apply but got %s instead' % type(apply_attrs), obj=data) 153 154 # manual list as otherwise the options would set other task parameters we don't want. 155 for option in my_arg_names.intersection(IncludeRole.OTHER_ARGS): 156 setattr(ir, option, ir.args.get(option)) 157 158 return ir 159 160 def copy(self, exclude_parent=False, exclude_tasks=False): 161 162 new_me = super(IncludeRole, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) 163 new_me.statically_loaded = self.statically_loaded 164 new_me._from_files = self._from_files.copy() 165 new_me._parent_role = self._parent_role 166 new_me._role_name = self._role_name 167 new_me._role_path = self._role_path 168 169 return new_me 170 171 def get_include_params(self): 172 v = super(IncludeRole, self).get_include_params() 173 if self._parent_role: 174 v.update(self._parent_role.get_role_params()) 175 v.setdefault('ansible_parent_role_names', []).insert(0, self._parent_role.get_name()) 176 v.setdefault('ansible_parent_role_paths', []).insert(0, self._parent_role._role_path) 177 return v 178