Dynamic role include (#17401)

* dynamic role_include

* more fixes for dynamic include roles

* set play yfrom iterator when dynamic

* changes from jimi-c

* avoid modules that break ad hoc

TODO: should really be a config
This commit is contained in:
Brian Coca 2016-09-05 20:07:58 -04:00 committed by GitHub
parent d5aecfdd14
commit ff34f5548d
9 changed files with 148 additions and 49 deletions

View file

@ -155,6 +155,10 @@ class AdHocCLI(CLI):
err = err + ' (did you mean to run ansible-playbook?)' err = err + ' (did you mean to run ansible-playbook?)'
raise AnsibleOptionsError(err) raise AnsibleOptionsError(err)
# Avoid modules that don't work with ad-hoc
if self.options.module_name in ('include', 'include_role'):
raise AnsibleOptionsError("'%s' is not a valid action for ad-hoc commands" % self.options.module_name)
# dynamically load any plugins from the playbook directory # dynamically load any plugins from the playbook directory
for name, obj in get_all_plugin_loaders(): for name, obj in get_all_plugin_loaders():
if obj.subdir: if obj.subdir:

View file

@ -403,7 +403,7 @@ class TaskExecutor:
return dict(changed=False, skipped=True, skip_reason='Conditional check failed', _ansible_no_log=self._play_context.no_log) return dict(changed=False, skipped=True, skip_reason='Conditional check failed', _ansible_no_log=self._play_context.no_log)
except AnsibleError: except AnsibleError:
# skip conditional exception in the case of includes as the vars needed might not be avaiable except in the included tasks or due to tags # skip conditional exception in the case of includes as the vars needed might not be avaiable except in the included tasks or due to tags
if self._task.action != 'include': if self._task.action in ['include', 'include_role']:
raise raise
# if we ran into an error while setting up the PlayContext, raise it now # if we ran into an error while setting up the PlayContext, raise it now
@ -425,10 +425,10 @@ class TaskExecutor:
# if this task is a IncludeRole, we just return now with a success code so the main thread can expand the task list for the given host # if this task is a IncludeRole, we just return now with a success code so the main thread can expand the task list for the given host
elif self._task.action == 'include_role': elif self._task.action == 'include_role':
include_variables = self._task.args.copy() include_variables = self._task.args.copy()
role = include_variables.pop('name') role = templar.template(self._task._role_name)
if not role: if not role:
return dict(failed=True, msg="No role was specified to include") return dict(failed=True, msg="No role was specified to include")
return dict(name=role, include_variables=include_variables) return dict(include_role=role, include_variables=include_variables)
# Now we do final validation on the task, which sets all fields to their final values. # Now we do final validation on the task, which sets all fields to their final values.
self._task.post_validate(templar=templar) self._task.post_validate(templar=templar)

View file

@ -108,13 +108,10 @@ class BaseMeta(type):
# its value from a parent object # its value from a parent object
method = "_get_attr_%s" % attr_name method = "_get_attr_%s" % attr_name
if method in src_dict or method in dst_dict: if method in src_dict or method in dst_dict:
#print("^ assigning generic_g_method to %s" % attr_name)
getter = partial(_generic_g_method, attr_name) getter = partial(_generic_g_method, attr_name)
elif '_get_parent_attribute' in dst_dict and value.inherit: elif '_get_parent_attribute' in dst_dict and value.inherit:
#print("^ assigning generic_g_parent to %s" % attr_name)
getter = partial(_generic_g_parent, attr_name) getter = partial(_generic_g_parent, attr_name)
else: else:
#print("^ assigning generic_g to %s" % attr_name)
getter = partial(_generic_g, attr_name) getter = partial(_generic_g, attr_name)
setter = partial(_generic_s, attr_name) setter = partial(_generic_s, attr_name)
@ -140,7 +137,6 @@ class BaseMeta(type):
# now create the attributes based on the FieldAttributes # now create the attributes based on the FieldAttributes
# available, including from parent (and grandparent) objects # available, including from parent (and grandparent) objects
#print("creating class %s" % name)
_create_attrs(dct, dct) _create_attrs(dct, dct)
_process_parents(parents, dct) _process_parents(parents, dct)
@ -201,7 +197,6 @@ class Base(with_metaclass(BaseMeta, object)):
if hasattr(self, '_parent') and self._parent: if hasattr(self, '_parent') and self._parent:
self._parent.dump_me(depth+2) self._parent.dump_me(depth+2)
dep_chain = self._parent.get_dep_chain() dep_chain = self._parent.get_dep_chain()
#print("%s^ dep chain: %s" % (" "*(depth+2), dep_chain))
if dep_chain: if dep_chain:
for dep in dep_chain: for dep in dep_chain:
dep.dump_me(depth+2) dep.dump_me(depth+2)

View file

@ -22,7 +22,7 @@ import os
from ansible import constants as C from ansible import constants as C
from ansible.compat.six import string_types from ansible.compat.six import string_types
from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleError from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound
try: try:
from __main__ import display from __main__ import display
@ -260,16 +260,41 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
task_list.append(t) task_list.append(t)
elif 'include_role' in task_ds: elif 'include_role' in task_ds:
task_list.extend(
IncludeRole.load( ir = IncludeRole.load(
task_ds, task_ds,
block=block, block=block,
role=role, role=role,
task_include=None, task_include=None,
variable_manager=variable_manager, variable_manager=variable_manager,
loader=loader loader=loader
) )
)
# 1. the user has set the 'static' option to false or true
# 2. one of the appropriate config options was set
if ir.static is not None:
is_static = ir.static
else:
display.debug('Determine if include_role is static')
# Check to see if this include is dynamic or static:
all_vars = variable_manager.get_vars(loader=loader, play=play, task=ir)
templar = Templar(loader=loader, variables=all_vars)
needs_templating = False
for param in ir.args:
if templar._contains_vars(ir.args[param]):
if not templar.templatable(ir.args[param]):
needs_templating = True
break
is_static = C.DEFAULT_TASK_INCLUDES_STATIC or \
(use_handlers and C.DEFAULT_HANDLER_INCLUDES_STATIC) or \
(not needs_templating and ir.all_parents_static() and not ir.loop)
display.debug('Determined that if include_role static is %s' % str(is_static))
if is_static:
# uses compiled list from object
t = task_list.extend(ir.get_block_list(variable_manager=variable_manager, loader=loader))
else:
# passes task object itself for latter generation of list
t = task_list.append(ir)
else: else:
if use_handlers: if use_handlers:
t = Handler.load(task_ds, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader) t = Handler.load(task_ds, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader)

View file

@ -45,40 +45,73 @@ class IncludeRole(Task):
# ================================================================================= # =================================================================================
# ATTRIBUTES # ATTRIBUTES
_name = FieldAttribute(isa='string', default=None) # private as this is a 'module options' vs a task property
_tasks_from = FieldAttribute(isa='string', default=None) _static = FieldAttribute(isa='bool', default=None, private=True)
_private = FieldAttribute(isa='bool', default=None, private=True)
# these should not be changeable? def __init__(self, block=None, role=None, task_include=None):
_static = FieldAttribute(isa='bool', default=False)
_private = FieldAttribute(isa='bool', default=True) super(IncludeRole, self).__init__(block=block, role=role, task_include=task_include)
self._role_name = None
self.statically_loaded = False
self._from_files = {}
self._parent_role = role
def get_block_list(self, play=None, variable_manager=None, loader=None):
# only need play passed in when dynamic
if play is None:
myplay = self._parent._play
else:
myplay = play
ri = RoleInclude.load(self._role_name, play=myplay, variable_manager=variable_manager, loader=loader)
ri.vars.update(self.vars)
#ri._role_params.update(self.args) # jimi-c cant we avoid this?
#build role
actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files)
# compile role
blocks = actual_role.compile(play=myplay)
# set parent to ensure proper inheritance
for b in blocks:
b._parent = self._parent
# updated available handlers in play
myplay.handlers = myplay.handlers + actual_role.get_handler_blocks(play=myplay)
return blocks
@staticmethod @staticmethod
def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None):
r = IncludeRole().load_data(data, variable_manager=variable_manager, loader=loader) ir = IncludeRole(block, role, task_include=task_include).load_data(data, variable_manager=variable_manager, loader=loader)
args = r.preprocess_data(data).get('args', dict())
ri = RoleInclude.load(args.get('name'), play=block._play, variable_manager=variable_manager, loader=loader) #TODO: use more automated list: for builtin in r.get_attributes(): #jimi-c: doing this to avoid using role_params and conflating include_role specific opts with other tasks
ri.vars.update(r.vars) # set built in's
ir._role_name = ir.args.get('name')
for builtin in ['static', 'private']:
if ir.args.get(builtin):
setattr(ir, builtin, ir.args.get(builtin))
# build options for roles # build options for roles
from_files = {}
for key in ['tasks', 'vars', 'defaults']: for key in ['tasks', 'vars', 'defaults']:
from_key = key + '_from' from_key = key + '_from'
if args.get(from_key): if ir.args.get(from_key):
from_files[key] = basename(args.get(from_key)) ir._from_files[key] = basename(ir.args.get(from_key))
#build role return ir.load_data(data, variable_manager=variable_manager, loader=loader)
actual_role = Role.load(ri, block._play, parent_role=role, from_files=from_files)
# compile role def copy(self, exclude_parent=False, exclude_tasks=False):
blocks = actual_role.compile(play=block._play)
# set parent to ensure proper inheritance new_me = super(IncludeRole, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks)
for b in blocks: new_me.statically_loaded = self.statically_loaded
b._parent = block new_me._role_name = self._role_name
new_me._from_files = self._from_files.copy()
new_me._parent_role = self._parent_role
# updated available handlers in play return new_me
block._play.handlers = block._play.handlers + actual_role.get_handler_blocks(play=block._play)
return blocks

View file

@ -62,7 +62,7 @@ class CallbackModule(CallbackBase):
self._clean_results(result._result, result._task.action) self._clean_results(result._result, result._task.action)
delegated_vars = result._result.get('_ansible_delegated_vars', None) delegated_vars = result._result.get('_ansible_delegated_vars', None)
if result._task.action == 'include': if result._task.action in ('include', 'include_role'):
return return
elif result._result.get('changed', False): elif result._result.get('changed', False):
if delegated_vars: if delegated_vars:
@ -158,7 +158,7 @@ class CallbackModule(CallbackBase):
def v2_runner_item_on_ok(self, result): def v2_runner_item_on_ok(self, result):
delegated_vars = result._result.get('_ansible_delegated_vars', None) delegated_vars = result._result.get('_ansible_delegated_vars', None)
if result._task.action == 'include': if result._task.action in ('include', 'include_role'):
return return
elif result._result.get('changed', False): elif result._result.get('changed', False):
msg = 'changed' msg = 'changed'

View file

@ -376,7 +376,7 @@ class StrategyBase:
if self._diff: if self._diff:
self._tqm.send_callback('v2_on_file_diff', task_result) self._tqm.send_callback('v2_on_file_diff', task_result)
if original_task.action != 'include': if original_task.action in ['include', 'include_role']:
self._tqm._stats.increment('ok', original_host.name) self._tqm._stats.increment('ok', original_host.name)
if 'changed' in task_result._result and task_result._result['changed']: if 'changed' in task_result._result and task_result._result['changed']:
self._tqm._stats.increment('changed', original_host.name) self._tqm._stats.increment('changed', original_host.name)
@ -390,7 +390,7 @@ class StrategyBase:
# If this is a role task, mark the parent role as being run (if # If this is a role task, mark the parent role as being run (if
# the task was ok or failed, but not skipped or unreachable) # the task was ok or failed, but not skipped or unreachable)
if original_task._role is not None and role_ran and original_task.action != 'include_role': if original_task._role is not None and role_ran: #TODO: and original_task.action != 'include_role':?
# lookup the role in the ROLE_CACHE to make sure we're dealing # lookup the role in the ROLE_CACHE to make sure we're dealing
# with the correct object and mark it as executed # with the correct object and mark it as executed
for (entry, role_obj) in iteritems(iterator._play.ROLE_CACHE[original_task._role._role_name]): for (entry, role_obj) in iteritems(iterator._play.ROLE_CACHE[original_task._role._role_name]):

View file

@ -280,6 +280,36 @@ class StrategyModule(StrategyBase):
results += self._wait_on_pending_results(iterator) results += self._wait_on_pending_results(iterator)
host_results.extend(results) host_results.extend(results)
all_role_blocks = []
for hr in results:
# handle include_role
if hr._task.action == 'include_role':
loop_var = None
if hr._task.loop:
loop_var = 'item'
if hr._task.loop_control:
loop_var = hr._task.loop_control.loop_var or 'item'
include_results = hr._result['results']
else:
include_results = [ hr._result ]
for include_result in include_results:
if 'skipped' in include_result and include_result['skipped'] or 'failed' in include_result and include_result['failed']:
continue
role_vars = include_result.get('include_variables', dict())
if loop_var and loop_var in include_result:
role_vars[loop_var] = include_result[loop_var]
display.debug("generating all_blocks data for role")
new_ir = hr._task.copy()
new_ir.args.update(role_vars)
all_role_blocks.extend(new_ir.get_block_list(play=iterator._play, variable_manager=self._variable_manager, loader=self._loader))
if len(all_role_blocks) > 0:
for host in hosts_left:
iterator.add_tasks(host, all_role_blocks)
try: try:
included_files = IncludedFile.process_include_results( included_files = IncludedFile.process_include_results(
host_results, host_results,

View file

@ -367,13 +367,25 @@ class Templar:
else: else:
return variable return variable
def templatable(self, data):
'''
returns True if the data can be templated w/o errors
'''
templatable = True
try:
self.template(data)
except:
templatable = False
return templatable
def _contains_vars(self, data): def _contains_vars(self, data):
''' '''
returns True if the data contains a variable pattern returns True if the data contains a variable pattern
''' '''
for marker in [self.environment.block_start_string, self.environment.variable_start_string, self.environment.comment_start_string]: if isinstance(data, string_types):
if marker in data: for marker in [self.environment.block_start_string, self.environment.variable_start_string, self.environment.comment_start_string]:
return True if marker in data:
return True
return False return False
def _convert_bare_variable(self, variable, bare_deprecated): def _convert_bare_variable(self, variable, bare_deprecated):