class CollectionSearch: # this needs to be populated before we can resolve tasks/roles/etc _collections = FieldAttribute(isa='list', listof=string_types, priority=100, default=_ensure_default_collection, always_post_validate=True, static=True) def _load_collections(self, attr, ds): # We are always a mixin with Base, so we can validate this untemplated # field early on to guarantee we are dealing with a list. ds = self.get_validated_value('collections', self._collections, ds, None) # this will only be called if someone specified a value; call the shared value _ensure_default_collection(collection_list=ds) if not ds: # don't return an empty collection list, just return None return None # This duplicates static attr checking logic from post_validate() # because if the user attempts to template a collection name, it may # error before it ever gets to the post_validate() warning (e.g. trying # to import a role from the collection). env = Environment() for collection_name in ds: if is_template(collection_name, env): display.warning( '"collections" is not templatable, but we found: %s, ' 'it will not be templated and will be used "as is".' % (collection_name)) return ds
class LoopControl(FieldAttributeBase): _loop_var = FieldAttribute(isa='str', default='item') _index_var = FieldAttribute(isa='str') _label = FieldAttribute(isa='str') _pause = FieldAttribute(isa='float', default=0) _extended = FieldAttribute(isa='bool') def __init__(self): super(LoopControl, self).__init__() @staticmethod def load(data, variable_manager=None, loader=None): t = LoopControl() return t.load_data(data, variable_manager=variable_manager, loader=loader)
def test_get_validated_value_string_rewrap_unsafe(self): attribute = FieldAttribute(isa='string') value = AssibleUnsafeText(u'bar') templar = Templar(None) bsc = self.ClassUnderTest() result = bsc.get_validated_value('foo', attribute, value, templar) self.assertIsInstance(result, AssibleUnsafeText) self.assertEqual(result, AssibleUnsafeText(u'bar'))
class ExampleParentBaseSubClass(base.Base): _test_attr_parent_string = FieldAttribute( isa='string', default='A string attr for a class that may be a parent for testing') def __init__(self): super(ExampleParentBaseSubClass, self).__init__() self._dep_chain = None def get_dep_chain(self): return self._dep_chain
class ExampleSubClass(base.Base): _test_attr_blip = FieldAttribute( isa='string', default='example sub class test_attr_blip', inherit=False, always_post_validate=True) def __init__(self): super(ExampleSubClass, self).__init__() def get_dep_chain(self): if self._parent: return self._parent.get_dep_chain() else: return None
class Handler(Task): _listen = FieldAttribute(isa='list', default=list, listof=string_types, static=True) def __init__(self, block=None, role=None, task_include=None): self.notified_hosts = [] self.cached_name = False super(Handler, self).__init__(block=block, role=role, task_include=task_include) def __repr__(self): ''' returns a human readable representation of the handler ''' return "HANDLER: %s" % self.get_name() @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): t = Handler(block=block, role=role, task_include=task_include) return t.load_data(data, variable_manager=variable_manager, loader=loader) def notify_host(self, host): if not self.is_host_notified(host): self.notified_hosts.append(host) return True return False def is_host_notified(self, host): return host in self.notified_hosts def serialize(self): result = super(Handler, self).serialize() result['is_handler'] = True return result
class PlaybookInclude(Base, Conditional, Taggable): _import_playbook = FieldAttribute(isa='string') _vars = FieldAttribute(isa='dict', default=dict) @staticmethod def load(data, basedir, variable_manager=None, loader=None): return PlaybookInclude().load_data(ds=data, basedir=basedir, variable_manager=variable_manager, loader=loader) def load_data(self, ds, basedir, variable_manager=None, loader=None): ''' Overrides the base load_data(), as we're actually going to return a new Playbook() object rather than a PlaybookInclude object ''' # import here to avoid a dependency loop from assible.playbook import Playbook from assible.playbook.play import Play # first, we use the original parent method to correctly load the object # via the load_data/preprocess_data system we normally use for other # playbook objects new_obj = super(PlaybookInclude, self).load_data(ds, variable_manager, loader) all_vars = self.vars.copy() if variable_manager: all_vars.update(variable_manager.get_vars()) templar = Templar(loader=loader, variables=all_vars) # then we use the object to load a Playbook pb = Playbook(loader=loader) file_name = templar.template(new_obj.import_playbook) if not os.path.isabs(file_name): file_name = os.path.join(basedir, file_name) pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager, vars=self.vars.copy()) # finally, update each loaded playbook entry with any variables specified # on the included playbook and/or any tags which may have been set for entry in pb._entries: # conditional includes on a playbook need a marker to skip gathering if new_obj.when and isinstance(entry, Play): entry._included_conditional = new_obj.when[:] temp_vars = entry.vars.copy() temp_vars.update(new_obj.vars) param_tags = temp_vars.pop('tags', None) if param_tags is not None: entry.tags.extend(param_tags.split(',')) entry.vars = temp_vars entry.tags = list(set(entry.tags).union(new_obj.tags)) if entry._included_path is None: entry._included_path = os.path.dirname(file_name) # Check to see if we need to forward the conditionals on to the included # plays. If so, we can take a shortcut here and simply prepend them to # those attached to each block (if any) if new_obj.when: for task_block in (entry.pre_tasks + entry.roles + entry.tasks + entry.post_tasks): task_block._attributes[ 'when'] = new_obj.when[:] + task_block.when[:] return pb def preprocess_data(self, ds): ''' Regorganizes the data for a PlaybookInclude datastructure to line up with what we expect the proper attributes to be ''' if not isinstance(ds, dict): raise AssibleAssertionError( 'ds (%s) should be a dict but was a %s' % (ds, type(ds))) # the new, cleaned datastructure, which will have legacy # items reduced to a standard structure new_ds = AssibleMapping() if isinstance(ds, AssibleBaseYAMLObject): new_ds.assible_pos = ds.assible_pos for (k, v) in iteritems(ds): if k in ('include', 'import_playbook'): self._preprocess_import(ds, new_ds, k, v) else: # some basic error checking, to make sure vars are properly # formatted and do not conflict with k=v parameters if k == 'vars': if 'vars' in new_ds: raise AssibleParserError( "import_playbook parameters cannot be mixed with 'vars' entries for import statements", obj=ds) elif not isinstance(v, dict): raise AssibleParserError( "vars for import_playbook statements must be specified as a dictionary", obj=ds) new_ds[k] = v return super(PlaybookInclude, self).preprocess_data(new_ds) def _preprocess_import(self, ds, new_ds, k, v): ''' Splits the playbook import line up into filename and parameters ''' if v is None: raise AssibleParserError("playbook import parameter is missing", obj=ds) elif not isinstance(v, string_types): raise AssibleParserError( "playbook import parameter must be a string indicating a file path, got %s instead" % type(v), obj=ds) # The import_playbook line must include at least one item, which is the filename # to import. Anything after that should be regarded as a parameter to the import items = split_args(v) if len(items) == 0: raise AssibleParserError( "import_playbook statements must specify the file name to import", obj=ds) else: new_ds['import_playbook'] = items[0].strip() if len(items) > 1: display.warning( 'Additional parameters in import_playbook statements are not supported. This will be an error in version 2.14' ) # rejoin the parameter portion of the arguments and # then use parse_kv() to get a dict of params back params = parse_kv(" ".join(items[1:])) if 'tags' in params: new_ds['tags'] = params.pop('tags') if 'vars' in new_ds: raise AssibleParserError( "import_playbook parameters cannot be mixed with 'vars' entries for import statements", obj=ds) new_ds['vars'] = params
class Taggable: untagged = frozenset(['untagged']) _tags = FieldAttribute(isa='list', default=list, listof=(string_types, int), extend=True) def _load_tags(self, attr, ds): if isinstance(ds, list): return ds elif isinstance(ds, string_types): value = ds.split(',') if isinstance(value, list): return [x.strip() for x in value] else: return [ds] else: raise AssibleError('tags must be specified as a list', obj=ds) def evaluate_tags(self, only_tags, skip_tags, all_vars): ''' this checks if the current item should be executed depending on tag options ''' if self.tags: templar = Templar(loader=self._loader, variables=all_vars) tags = templar.template(self.tags) _temp_tags = set() for tag in tags: if isinstance(tag, list): _temp_tags.update(tag) else: _temp_tags.add(tag) tags = _temp_tags self.tags = list(tags) else: # this makes isdisjoint work for untagged tags = self.untagged should_run = True # default, tasks to run if only_tags: if 'always' in tags: should_run = True elif ('all' in only_tags and 'never' not in tags): should_run = True elif not tags.isdisjoint(only_tags): should_run = True elif 'tagged' in only_tags and tags != self.untagged and 'never' not in tags: should_run = True else: should_run = False if should_run and skip_tags: # Check for tags that we need to skip if 'all' in skip_tags: if 'always' not in tags or 'always' in skip_tags: should_run = False elif not tags.isdisjoint(skip_tags): should_run = False elif 'tagged' in skip_tags and tags != self.untagged: should_run = False return should_run
class Play(Base, Taggable, CollectionSearch): """ A play is a language feature that represents a list of roles and/or task/handler blocks to execute on a given set of hosts. Usage: Play.load(datastructure) -> Play Play.something(...) """ # ================================================================================= _hosts = FieldAttribute(isa='list', required=True, listof=string_types, always_post_validate=True, priority=-1) # Facts _gather_facts = FieldAttribute(isa='bool', default=None, always_post_validate=True) _gather_subset = FieldAttribute(isa='list', default=(lambda: C.DEFAULT_GATHER_SUBSET), listof=string_types, always_post_validate=True) _gather_timeout = FieldAttribute(isa='int', default=C.DEFAULT_GATHER_TIMEOUT, always_post_validate=True) _fact_path = FieldAttribute(isa='string', default=C.DEFAULT_FACT_PATH) # Variable Attributes _vars_files = FieldAttribute(isa='list', default=list, priority=99) _vars_prompt = FieldAttribute(isa='list', default=list, always_post_validate=False) # Role Attributes _roles = FieldAttribute(isa='list', default=list, priority=90) # Block (Task) Lists Attributes _handlers = FieldAttribute(isa='list', default=list) _pre_tasks = FieldAttribute(isa='list', default=list) _post_tasks = FieldAttribute(isa='list', default=list) _tasks = FieldAttribute(isa='list', default=list) # Flag/Setting Attributes _force_handlers = FieldAttribute( isa='bool', default=context.cliargs_deferred_get('force_handlers'), always_post_validate=True) _max_fail_percentage = FieldAttribute(isa='percent', always_post_validate=True) _serial = FieldAttribute(isa='list', default=list, always_post_validate=True) _strategy = FieldAttribute(isa='string', default=C.DEFAULT_STRATEGY, always_post_validate=True) _order = FieldAttribute(isa='string', always_post_validate=True) # ================================================================================= def __init__(self): super(Play, self).__init__() self._included_conditional = None self._included_path = None self._removed_hosts = [] self.ROLE_CACHE = {} self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset( ('all', )) self.skip_tags = set(context.CLIARGS.get('skip_tags', [])) def __repr__(self): return self.get_name() def get_name(self): ''' return the name of the Play ''' return self.name @staticmethod def load(data, variable_manager=None, loader=None, vars=None): if ('name' not in data or data['name'] is None) and 'hosts' in data: if data['hosts'] is None or all(host is None for host in data['hosts']): raise AssibleParserError( "Hosts list cannot be empty - please check your playbook") if isinstance(data['hosts'], list): data['name'] = ','.join(data['hosts']) else: data['name'] = data['hosts'] p = Play() if vars: p.vars = vars.copy() return p.load_data(data, variable_manager=variable_manager, loader=loader) def preprocess_data(self, ds): ''' Adjusts play datastructure to cleanup old/legacy items ''' if not isinstance(ds, dict): raise AssibleAssertionError( 'while preprocessing data (%s), ds should be a dict but was a %s' % (ds, type(ds))) # The use of 'user' in the Play datastructure was deprecated to # line up with the same change for Tasks, due to the fact that # 'user' conflicted with the user module. if 'user' in ds: # this should never happen, but error out with a helpful message # to the user if it does... if 'remote_user' in ds: raise AssibleParserError( "both 'user' and 'remote_user' are set for %s. " "The use of 'user' is deprecated, and should be removed" % self.get_name(), obj=ds) ds['remote_user'] = ds['user'] del ds['user'] return super(Play, self).preprocess_data(ds) def _load_tasks(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' try: return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading tasks: %s" % to_native(e), obj=self._ds, orig_exc=e) def _load_pre_tasks(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' try: return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading pre_tasks", obj=self._ds, orig_exc=e) def _load_post_tasks(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' try: return load_list_of_blocks(ds=ds, play=self, variable_manager=self._variable_manager, loader=self._loader) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading post_tasks", obj=self._ds, orig_exc=e) def _load_handlers(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed handlers/blocks. Bare handlers outside of a block are given an implicit block. ''' try: return self._extend_value( self.handlers, load_list_of_blocks(ds=ds, play=self, use_handlers=True, variable_manager=self._variable_manager, loader=self._loader), prepend=True) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading handlers", obj=self._ds, orig_exc=e) def _load_roles(self, attr, ds): ''' Loads and returns a list of RoleInclude objects from the datastructure list of role definitions and creates the Role from those objects ''' if ds is None: ds = [] try: role_includes = load_list_of_roles( ds, play=self, variable_manager=self._variable_manager, loader=self._loader, collection_search_list=self.collections) except AssertionError as e: raise AssibleParserError( "A malformed role declaration was encountered.", obj=self._ds, orig_exc=e) roles = [] for ri in role_includes: roles.append(Role.load(ri, play=self)) self.roles[:0] = roles return self.roles def _load_vars_prompt(self, attr, ds): new_ds = preprocess_vars(ds) vars_prompts = [] if new_ds is not None: for prompt_data in new_ds: if 'name' not in prompt_data: raise AssibleParserError( "Invalid vars_prompt data structure, missing 'name' key", obj=ds) for key in prompt_data: if key not in ('name', 'prompt', 'default', 'private', 'confirm', 'encrypt', 'salt_size', 'salt', 'unsafe'): raise AssibleParserError( "Invalid vars_prompt data structure, found unsupported key '%s'" % key, obj=ds) vars_prompts.append(prompt_data) return vars_prompts def _compile_roles(self): ''' Handles the role compilation step, returning a flat list of tasks with the lowest level dependencies first. For example, if a role R has a dependency D1, which also has a dependency D2, the tasks from D2 are merged first, followed by D1, and lastly by the tasks from the parent role R last. This is done for all roles in the Play. ''' block_list = [] if len(self.roles) > 0: for r in self.roles: # Don't insert tasks from ``import/include_role``, preventing # duplicate execution at the wrong time if r.from_include: continue block_list.extend(r.compile(play=self)) return block_list def compile_roles_handlers(self): ''' Handles the role handler compilation step, returning a flat list of Handlers This is done for all roles in the Play. ''' block_list = [] if len(self.roles) > 0: for r in self.roles: if r.from_include: continue block_list.extend(r.get_handler_blocks(play=self)) return block_list def compile(self): ''' Compiles and returns the task list for this play, compiled from the roles (which are themselves compiled recursively) and/or the list of tasks specified in the play. ''' # create a block containing a single flush handlers meta # task, so we can be sure to run handlers at certain points # of the playbook execution flush_block = Block.load(data={'meta': 'flush_handlers'}, play=self, variable_manager=self._variable_manager, loader=self._loader) for task in flush_block.block: task.implicit = True block_list = [] block_list.extend(self.pre_tasks) block_list.append(flush_block) block_list.extend(self._compile_roles()) block_list.extend(self.tasks) block_list.append(flush_block) block_list.extend(self.post_tasks) block_list.append(flush_block) return block_list def get_vars(self): return self.vars.copy() def get_vars_files(self): if self.vars_files is None: return [] elif not isinstance(self.vars_files, list): return [self.vars_files] return self.vars_files def get_handlers(self): return self.handlers[:] def get_roles(self): return self.roles[:] def get_tasks(self): tasklist = [] for task in self.pre_tasks + self.tasks + self.post_tasks: if isinstance(task, Block): tasklist.append(task.block + task.rescue + task.always) else: tasklist.append(task) return tasklist def serialize(self): data = super(Play, self).serialize() roles = [] for role in self.get_roles(): roles.append(role.serialize()) data['roles'] = roles data['included_path'] = self._included_path return data def deserialize(self, data): super(Play, self).deserialize(data) self._included_path = data.get('included_path', None) if 'roles' in data: role_data = data.get('roles', []) roles = [] for role in role_data: r = Role() r.deserialize(role) roles.append(r) setattr(self, 'roles', roles) del data['roles'] def copy(self): new_me = super(Play, self).copy() new_me.ROLE_CACHE = self.ROLE_CACHE.copy() new_me._included_conditional = self._included_conditional new_me._included_path = self._included_path return new_me
class TaskInclude(Task): """ A task include is derived from a regular task to handle the special circumstances related to the `- include: ...` task. """ BASE = frozenset(('file', '_raw_params')) # directly assigned OTHER_ARGS = frozenset(('apply', )) # assigned to matching property VALID_ARGS = BASE.union(OTHER_ARGS) # all valid args VALID_INCLUDE_KEYWORDS = frozenset( ('action', 'args', 'collections', 'debugger', 'ignore_errors', 'loop', 'loop_control', 'loop_with', 'name', 'no_log', 'register', 'run_once', 'tags', 'vars', 'when')) # ================================================================================= # ATTRIBUTES _static = FieldAttribute(isa='bool', default=None) def __init__(self, block=None, role=None, task_include=None): super(TaskInclude, self).__init__(block=block, role=role, task_include=task_include) self.statically_loaded = False @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): ti = TaskInclude(block=block, role=role, task_include=task_include) task = ti.check_options( ti.load_data(data, variable_manager=variable_manager, loader=loader), data) return task def check_options(self, task, data): ''' Method for options validation to use in 'load_data' for TaskInclude and HandlerTaskInclude since they share the same validations. It is not named 'validate_options' on purpose to prevent confusion with '_validate_*" methods. Note that the task passed might be changed as a side-effect of this method. ''' my_arg_names = frozenset(task.args.keys()) # validate bad args, otherwise we silently ignore bad_opts = my_arg_names.difference(self.VALID_ARGS) if bad_opts and task.action in ('include_tasks', 'import_tasks'): raise AssibleParserError('Invalid options for %s: %s' % (task.action, ','.join(list(bad_opts))), obj=data) if not task.args.get('_raw_params'): task.args['_raw_params'] = task.args.pop('file', None) if not task.args['_raw_params']: raise AssibleParserError('No file specified for %s' % task.action) apply_attrs = task.args.get('apply', {}) if apply_attrs and task.action != 'include_tasks': raise AssibleParserError('Invalid options for %s: apply' % task.action, obj=data) elif not isinstance(apply_attrs, dict): raise AssibleParserError( 'Expected a dict for apply but got %s instead' % type(apply_attrs), obj=data) return task def preprocess_data(self, ds): ds = super(TaskInclude, self).preprocess_data(ds) diff = set(ds.keys()).difference(self.VALID_INCLUDE_KEYWORDS) for k in diff: # This check doesn't handle ``include`` as we have no idea at this point if it is static or not if ds[k] is not Sentinel and ds['action'] in ('include_tasks', 'include_role'): if C.INVALID_TASK_ATTRIBUTE_FAILED: raise AssibleParserError( "'%s' is not a valid attribute for a %s" % (k, self.__class__.__name__), obj=ds) else: display.warning("Ignoring invalid attribute: %s" % k) return ds def copy(self, exclude_parent=False, exclude_tasks=False): new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) new_me.statically_loaded = self.statically_loaded return new_me def get_vars(self): ''' We override the parent Task() classes get_vars here because we need to include the args of the include into the vars as they are params to the included tasks. But ONLY for 'include' ''' if self.action != 'include': all_vars = super(TaskInclude, self).get_vars() else: all_vars = dict() if self._parent: all_vars.update(self._parent.get_vars()) all_vars.update(self.vars) all_vars.update(self.args) if 'tags' in all_vars: del all_vars['tags'] if 'when' in all_vars: del all_vars['when'] return all_vars def build_parent_block(self): ''' This method is used to create the parent block for the included tasks when ``apply`` is specified ''' apply_attrs = self.args.pop('apply', {}) if apply_attrs: apply_attrs['block'] = [] p_block = Block.load( apply_attrs, play=self._parent._play, task_include=self, role=self._role, variable_manager=self._variable_manager, loader=self._loader, ) else: p_block = self return p_block
class PlayContext(Base): ''' This class is used to consolidate the connection information for hosts in a play and child tasks, where the task may override some connection/authentication information. ''' # base _module_compression = FieldAttribute(isa='string', default=C.DEFAULT_MODULE_COMPRESSION) _shell = FieldAttribute(isa='string') _executable = FieldAttribute(isa='string', default=C.DEFAULT_EXECUTABLE) # connection fields, some are inherited from Base: # (connection, port, remote_user, environment, no_log) _remote_addr = FieldAttribute(isa='string') _password = FieldAttribute(isa='string') _timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT) _connection_user = FieldAttribute(isa='string') _private_key_file = FieldAttribute(isa='string', default=C.DEFAULT_PRIVATE_KEY_FILE) _pipelining = FieldAttribute(isa='bool', default=C.ASSIBLE_PIPELINING) # networking modules _network_os = FieldAttribute(isa='string') # docker FIXME: remove these _docker_extra_args = FieldAttribute(isa='string') # ssh # FIXME: remove these _ssh_executable = FieldAttribute(isa='string', default=C.ASSIBLE_SSH_EXECUTABLE) _ssh_args = FieldAttribute(isa='string', default=C.ASSIBLE_SSH_ARGS) _ssh_common_args = FieldAttribute(isa='string') _sftp_extra_args = FieldAttribute(isa='string') _scp_extra_args = FieldAttribute(isa='string') _ssh_extra_args = FieldAttribute(isa='string') _ssh_transfer_method = FieldAttribute( isa='string', default=C.DEFAULT_SSH_TRANSFER_METHOD) # ??? _connection_lockfd = FieldAttribute(isa='int') # privilege escalation fields _become = FieldAttribute(isa='bool') _become_method = FieldAttribute(isa='string') _become_user = FieldAttribute(isa='string') _become_pass = FieldAttribute(isa='string') _become_exe = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_EXE) _become_flags = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_FLAGS) _prompt = FieldAttribute(isa='string') # general flags _verbosity = FieldAttribute(isa='int', default=0) _only_tags = FieldAttribute(isa='set', default=set) _skip_tags = FieldAttribute(isa='set', default=set) _start_at_task = FieldAttribute(isa='string') _step = FieldAttribute(isa='bool', default=False) # "PlayContext.force_handlers should not be used, the calling code should be using play itself instead" _force_handlers = FieldAttribute(isa='bool', default=False) def __init__(self, play=None, passwords=None, connection_lockfd=None): # Note: play is really not optional. The only time it could be omitted is when we create # a PlayContext just so we can invoke its deserialize method to load it from a serialized # data source. super(PlayContext, self).__init__() if passwords is None: passwords = {} self.password = passwords.get('conn_pass', '') self.become_pass = passwords.get('become_pass', '') self._become_plugin = None self.prompt = '' self.success_key = '' # a file descriptor to be used during locking operations self.connection_lockfd = connection_lockfd # set options before play to allow play to override them if context.CLIARGS: self.set_attributes_from_cli() if play: self.set_attributes_from_play(play) def set_attributes_from_plugin(self, plugin): # generic derived from connection plugin, temporary for backwards compat, in the end we should not set play_context properties # get options for plugins options = C.config.get_configuration_definitions( get_plugin_class(plugin), plugin._load_name) for option in options: if option: flag = options[option].get('name') if flag: setattr(self, flag, self.connection.get_option(flag)) def set_attributes_from_play(self, play): self.force_handlers = play.force_handlers def set_attributes_from_cli(self): ''' Configures this connection information instance with data from options specified by the user on the command line. These have a lower precedence than those set on the play or host. ''' if context.CLIARGS.get('timeout', False): self.timeout = int(context.CLIARGS['timeout']) # From the command line. These should probably be used directly by plugins instead # For now, they are likely to be moved to FieldAttribute defaults self.private_key_file = context.CLIARGS.get( 'private_key_file') # Else default self.verbosity = context.CLIARGS.get('verbosity') # Else default self.ssh_common_args = context.CLIARGS.get( 'ssh_common_args') # Else default self.ssh_extra_args = context.CLIARGS.get( 'ssh_extra_args') # Else default self.sftp_extra_args = context.CLIARGS.get( 'sftp_extra_args') # Else default self.scp_extra_args = context.CLIARGS.get( 'scp_extra_args') # Else default # Not every cli that uses PlayContext has these command line args so have a default self.start_at_task = context.CLIARGS.get('start_at_task', None) # Else default def set_task_and_variable_override(self, task, variables, templar): ''' Sets attributes from the task if they are set, which will override those from the play. :arg task: the task object with the parameters that were set on it :arg variables: variables from inventory :arg templar: templar instance if templating variables is needed ''' new_info = self.copy() # loop through a subset of attributes on the task object and set # connection fields based on their values for attr in TASK_ATTRIBUTE_OVERRIDES: if hasattr(task, attr): attr_val = getattr(task, attr) if attr_val is not None: setattr(new_info, attr, attr_val) # next, use the MAGIC_VARIABLE_MAPPING dictionary to update this # connection info object with 'magic' variables from the variable list. # If the value 'assible_delegated_vars' is in the variables, it means # we have a delegated-to host, so we check there first before looking # at the variables in general if task.delegate_to is not None: # In the case of a loop, the delegated_to host may have been # templated based on the loop variable, so we try and locate # the host name in the delegated variable dictionary here delegated_host_name = templar.template(task.delegate_to) delegated_vars = variables.get('assible_delegated_vars', dict()).get(delegated_host_name, dict()) delegated_transport = C.DEFAULT_TRANSPORT for transport_var in C.MAGIC_VARIABLE_MAPPING.get('connection'): if transport_var in delegated_vars: delegated_transport = delegated_vars[transport_var] break # make sure this delegated_to host has something set for its remote # address, otherwise we default to connecting to it by name. This # may happen when users put an IP entry into their inventory, or if # they rely on DNS for a non-inventory hostname for address_var in ( 'assible_%s_host' % delegated_transport, ) + C.MAGIC_VARIABLE_MAPPING.get('remote_addr'): if address_var in delegated_vars: break else: display.debug( "no remote address found for delegated host %s\nusing its name, so success depends on DNS resolution" % delegated_host_name) delegated_vars['assible_host'] = delegated_host_name # reset the port back to the default if none was specified, to prevent # the delegated host from inheriting the original host's setting for port_var in ('assible_%s_port' % delegated_transport, ) + C.MAGIC_VARIABLE_MAPPING.get('port'): if port_var in delegated_vars: break else: if delegated_transport == 'winrm': delegated_vars['assible_port'] = 5986 else: delegated_vars['assible_port'] = C.DEFAULT_REMOTE_PORT # and likewise for the remote user for user_var in ('assible_%s_user' % delegated_transport, ) + C.MAGIC_VARIABLE_MAPPING.get('remote_user'): if user_var in delegated_vars and delegated_vars[user_var]: break else: delegated_vars[ 'assible_user'] = task.remote_user or self.remote_user else: delegated_vars = dict() # setup shell for exe_var in C.MAGIC_VARIABLE_MAPPING.get('executable'): if exe_var in variables: setattr(new_info, 'executable', variables.get(exe_var)) attrs_considered = [] for (attr, variable_names) in iteritems(C.MAGIC_VARIABLE_MAPPING): for variable_name in variable_names: if attr in attrs_considered: continue # if delegation task ONLY use delegated host vars, avoid delegated FOR host vars if task.delegate_to is not None: if isinstance(delegated_vars, dict) and variable_name in delegated_vars: setattr(new_info, attr, delegated_vars[variable_name]) attrs_considered.append(attr) elif variable_name in variables: setattr(new_info, attr, variables[variable_name]) attrs_considered.append(attr) # no else, as no other vars should be considered # become legacy updates -- from inventory file (inventory overrides # commandline) for become_pass_name in C.MAGIC_VARIABLE_MAPPING.get('become_pass'): if become_pass_name in variables: break # make sure we get port defaults if needed if new_info.port is None and C.DEFAULT_REMOTE_PORT is not None: new_info.port = int(C.DEFAULT_REMOTE_PORT) # special overrides for the connection setting if len(delegated_vars) > 0: # in the event that we were using local before make sure to reset the # connection type to the default transport for the delegated-to host, # if not otherwise specified for connection_type in C.MAGIC_VARIABLE_MAPPING.get('connection'): if connection_type in delegated_vars: break else: remote_addr_local = new_info.remote_addr in C.LOCALHOST inv_hostname_local = delegated_vars.get( 'inventory_hostname') in C.LOCALHOST if remote_addr_local and inv_hostname_local: setattr(new_info, 'connection', 'local') elif getattr(new_info, 'connection', None) == 'local' and (not remote_addr_local or not inv_hostname_local): setattr(new_info, 'connection', C.DEFAULT_TRANSPORT) # if the final connection type is local, reset the remote_user value to that of the currently logged in user # this ensures any become settings are obeyed correctly # we store original in 'connection_user' for use of network/other modules that fallback to it as login user # connection_user to be deprecated once connection=local is removed for # network modules if new_info.connection == 'local': if not new_info.connection_user: new_info.connection_user = new_info.remote_user new_info.remote_user = pwd.getpwuid(os.getuid()).pw_name # set no_log to default if it was not previously set if new_info.no_log is None: new_info.no_log = C.DEFAULT_NO_LOG if task.check_mode is not None: new_info.check_mode = task.check_mode if task.diff is not None: new_info.diff = task.diff return new_info def set_become_plugin(self, plugin): self._become_plugin = plugin def make_become_cmd(self, cmd, executable=None): """ helper function to create privilege escalation commands """ display.deprecated( "PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead", version="2.12", collection_name='assible.builtin') if not cmd or not self.become: return cmd become_method = self.become_method # load/call become plugins here plugin = self._become_plugin if plugin: options = { 'become_exe': self.become_exe or become_method, 'become_flags': self.become_flags or '', 'become_user': self.become_user, 'become_pass': self.become_pass } plugin.set_options(direct=options) if not executable: executable = self.executable shell = get_shell_plugin(executable=executable) cmd = plugin.build_become_command(cmd, shell) # for backwards compat: if self.become_pass: self.prompt = plugin.prompt else: raise AssibleError("Privilege escalation method not found: %s" % become_method) return cmd def update_vars(self, variables): ''' Adds 'magic' variables relating to connections to the variable dictionary provided. In case users need to access from the play, this is a legacy from runner. ''' for prop, var_list in C.MAGIC_VARIABLE_MAPPING.items(): try: if 'become' in prop: continue var_val = getattr(self, prop) for var_opt in var_list: if var_opt not in variables and var_val is not None: variables[var_opt] = var_val except AttributeError: continue def _get_attr_connection(self): ''' connections are special, this takes care of responding correctly ''' conn_type = None if self._attributes['connection'] == 'smart': conn_type = 'ssh' # see if SSH can support ControlPersist if not use paramiko if not check_for_controlpersist( self.ssh_executable) and paramiko is not None: conn_type = "paramiko" # if someone did `connection: persistent`, default it to using a persistent paramiko connection to avoid problems elif self._attributes[ 'connection'] == 'persistent' and paramiko is not None: conn_type = 'paramiko' if conn_type: self.connection = conn_type return self._attributes['connection']
class Conditional: ''' This is a mix-in class, to be used with Base to allow the object to be run conditionally when a condition is met or skipped. ''' _when = FieldAttribute(isa='list', default=list, extend=True, prepend=True) def __init__(self, loader=None): # when used directly, this class needs a loader, but we want to # make sure we don't trample on the existing one if this class # is used as a mix-in with a playbook base class if not hasattr(self, '_loader'): if loader is None: raise AssibleError( "a loader must be specified when using Conditional() directly" ) else: self._loader = loader super(Conditional, self).__init__() def _validate_when(self, attr, name, value): if not isinstance(value, list): setattr(self, name, [value]) def extract_defined_undefined(self, conditional): results = [] cond = conditional m = DEFINED_REGEX.search(cond) while m: results.append(m.groups()) cond = cond[m.end():] m = DEFINED_REGEX.search(cond) return results def evaluate_conditional(self, templar, all_vars): ''' Loops through the conditionals set on this object, returning False if any of them evaluate as such. ''' # since this is a mix-in, it may not have an underlying datastructure # associated with it, so we pull it out now in case we need it for # error reporting below ds = None if hasattr(self, '_ds'): ds = getattr(self, '_ds') result = True try: for conditional in self.when: # do evaluation if conditional is None or conditional == '': res = True elif isinstance(conditional, bool): res = conditional else: res = self._check_conditional(conditional, templar, all_vars) # only update if still true, preserve false if result: result = res display.debug("Evaluated conditional (%s): %s " % (conditional, res)) if not result: break except Exception as e: raise AssibleError( "The conditional check '%s' failed. The error was: %s" % (to_native(conditional), to_native(e)), obj=ds) return result def _check_conditional(self, conditional, templar, all_vars): ''' This method does the low-level evaluation of each conditional set on this object, using jinja2 to wrap the conditionals for evaluation. ''' original = conditional if templar.is_template(conditional): display.warning('conditional statements should not include jinja2 ' 'templating delimiters such as {{ }} or {%% %%}. ' 'Found: %s' % conditional) bare_vars_warning = False if C.CONDITIONAL_BARE_VARS: if conditional in all_vars and VALID_VAR_REGEX.match(conditional): conditional = all_vars[conditional] bare_vars_warning = True # make sure the templar is using the variables specified with this method templar.available_variables = all_vars try: # if the conditional is "unsafe", disable lookups disable_lookups = hasattr(conditional, '__UNSAFE__') conditional = templar.template(conditional, disable_lookups=disable_lookups) if bare_vars_warning and not isinstance(conditional, bool): display.deprecated( 'evaluating %r as a bare variable, this behaviour will go away and you might need to add " | bool"' ' (if you would like to evaluate input string from prompt) or " is truthy"' ' (if you would like to apply Python\'s evaluation method) to the expression in the future. ' 'Also see CONDITIONAL_BARE_VARS configuration toggle' % original, version="2.12", collection_name='assible.builtin') if not isinstance(conditional, text_type) or conditional == "": return conditional # update the lookups flag, as the string returned above may now be unsafe # and we don't want future templating calls to do unsafe things disable_lookups |= hasattr(conditional, '__UNSAFE__') # First, we do some low-level jinja2 parsing involving the AST format of the # statement to ensure we don't do anything unsafe (using the disable_lookup flag above) class CleansingNodeVisitor(ast.NodeVisitor): def generic_visit(self, node, inside_call=False, inside_yield=False): if isinstance(node, ast.Call): inside_call = True elif isinstance(node, ast.Yield): inside_yield = True elif isinstance(node, ast.Str): if disable_lookups: if inside_call and node.s.startswith("__"): # calling things with a dunder is generally bad at this point... raise AssibleError( "Invalid access found in the conditional: '%s'" % conditional) elif inside_yield: # we're inside a yield, so recursively parse and traverse the AST # of the result to catch forbidden syntax from executing parsed = ast.parse(node.s, mode='exec') cnv = CleansingNodeVisitor() cnv.visit(parsed) # iterate over all child nodes for child_node in ast.iter_child_nodes(node): self.generic_visit(child_node, inside_call=inside_call, inside_yield=inside_yield) try: e = templar.environment.overlay() e.filters.update(templar.environment.filters) e.tests.update(templar.environment.tests) res = e._parse(conditional, None, None) res = generate(res, e, None, None) parsed = ast.parse(res, mode='exec') cnv = CleansingNodeVisitor() cnv.visit(parsed) except Exception as e: raise AssibleError("Invalid conditional detected: %s" % to_native(e)) # and finally we generate and template the presented string and look at the resulting string presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional val = templar.template(presented, disable_lookups=disable_lookups).strip() if val == "True": return True elif val == "False": return False else: raise AssibleError("unable to evaluate conditional: %s" % original) except (AssibleUndefinedVariable, UndefinedError) as e: # the templating failed, meaning most likely a variable was undefined. If we happened # to be looking for an undefined variable, return True, otherwise fail try: # first we extract the variable name from the error message var_name = re.compile( r"'(hostvars\[.+\]|[\w_]+)' is undefined").search( str(e)).groups()[0] # next we extract all defined/undefined tests from the conditional string def_undef = self.extract_defined_undefined(conditional) # then we loop through these, comparing the error variable name against # each def/undef test we found above. If there is a match, we determine # whether the logic/state mean the variable should exist or not and return # the corresponding True/False for (du_var, logic, state) in def_undef: # when we compare the var names, normalize quotes because something # like hostvars['foo'] may be tested against hostvars["foo"] if var_name.replace("'", '"') == du_var.replace("'", '"'): # the should exist is a xor test between a negation in the logic portion # against the state (defined or undefined) should_exist = ('not' in logic) != (state == 'defined') if should_exist: return False else: return True # as nothing above matched the failed var name, re-raise here to # trigger the AssibleUndefinedVariable exception again below raise except Exception: raise AssibleUndefinedVariable( "error while evaluating conditional (%s): %s" % (original, e))
class Block(Base, Conditional, CollectionSearch, Taggable): # main block fields containing the task lists _block = FieldAttribute(isa='list', default=list, inherit=False) _rescue = FieldAttribute(isa='list', default=list, inherit=False) _always = FieldAttribute(isa='list', default=list, inherit=False) # other fields _delegate_to = FieldAttribute(isa='string') _delegate_facts = FieldAttribute(isa='bool') # for future consideration? this would be functionally # similar to the 'else' clause for exceptions # _otherwise = FieldAttribute(isa='list') def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, implicit=False): self._play = play self._role = role self._parent = None self._dep_chain = None self._use_handlers = use_handlers self._implicit = implicit # end of role flag self._eor = False if task_include: self._parent = task_include elif parent_block: self._parent = parent_block super(Block, self).__init__() def __repr__(self): return "BLOCK(uuid=%s)(id=%s)(parent=%s)" % (self._uuid, id(self), self._parent) def __eq__(self, other): '''object comparison based on _uuid''' return self._uuid == other._uuid def __ne__(self, other): '''object comparison based on _uuid''' return self._uuid != other._uuid def get_vars(self): ''' Blocks do not store variables directly, however they may be a member of a role or task include which does, so return those if present. ''' all_vars = self.vars.copy() if self._parent: all_vars.update(self._parent.get_vars()) return all_vars @staticmethod def load(data, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None): implicit = not Block.is_block(data) b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers, implicit=implicit) return b.load_data(data, variable_manager=variable_manager, loader=loader) @staticmethod def is_block(ds): is_block = False if isinstance(ds, dict): for attr in ('block', 'rescue', 'always'): if attr in ds: is_block = True break return is_block def preprocess_data(self, ds): ''' If a simple task is given, an implicit block for that single task is created, which goes in the main portion of the block ''' if not Block.is_block(ds): if isinstance(ds, list): return super(Block, self).preprocess_data(dict(block=ds)) else: return super(Block, self).preprocess_data(dict(block=[ds])) return super(Block, self).preprocess_data(ds) def _load_block(self, attr, ds): try: return load_list_of_tasks( ds, play=self._play, block=self, role=self._role, task_include=None, variable_manager=self._variable_manager, loader=self._loader, use_handlers=self._use_handlers, ) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading a block", obj=self._ds, orig_exc=e) def _load_rescue(self, attr, ds): try: return load_list_of_tasks( ds, play=self._play, block=self, role=self._role, task_include=None, variable_manager=self._variable_manager, loader=self._loader, use_handlers=self._use_handlers, ) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading rescue.", obj=self._ds, orig_exc=e) def _load_always(self, attr, ds): try: return load_list_of_tasks( ds, play=self._play, block=self, role=self._role, task_include=None, variable_manager=self._variable_manager, loader=self._loader, use_handlers=self._use_handlers, ) except AssertionError as e: raise AssibleParserError( "A malformed block was encountered while loading always", obj=self._ds, orig_exc=e) def _validate_always(self, attr, name, value): if value and not self.block: raise AssibleParserError( "'%s' keyword cannot be used without 'block'" % name, obj=self._ds) _validate_rescue = _validate_always def get_dep_chain(self): if self._dep_chain is None: if self._parent: return self._parent.get_dep_chain() else: return None else: return self._dep_chain[:] def copy(self, exclude_parent=False, exclude_tasks=False): def _dupe_task_list(task_list, new_block): new_task_list = [] for task in task_list: new_task = task.copy(exclude_parent=True) if task._parent: new_task._parent = task._parent.copy(exclude_tasks=True) if task._parent == new_block: # If task._parent is the same as new_block, just replace it new_task._parent = new_block else: # task may not be a direct child of new_block, search for the correct place to insert new_block cur_obj = new_task._parent while cur_obj._parent and cur_obj._parent != new_block: cur_obj = cur_obj._parent cur_obj._parent = new_block else: new_task._parent = new_block new_task_list.append(new_task) return new_task_list new_me = super(Block, self).copy() new_me._play = self._play new_me._use_handlers = self._use_handlers new_me._eor = self._eor if self._dep_chain is not None: new_me._dep_chain = self._dep_chain[:] new_me._parent = None if self._parent and not exclude_parent: new_me._parent = self._parent.copy(exclude_tasks=True) if not exclude_tasks: new_me.block = _dupe_task_list(self.block or [], new_me) new_me.rescue = _dupe_task_list(self.rescue or [], new_me) new_me.always = _dupe_task_list(self.always or [], new_me) new_me._role = None if self._role: new_me._role = self._role new_me.validate() return new_me def serialize(self): ''' Override of the default serialize method, since when we're serializing a task we don't want to include the attribute list of tasks. ''' data = dict() for attr in self._valid_attrs: if attr not in ('block', 'rescue', 'always'): data[attr] = getattr(self, attr) data['dep_chain'] = self.get_dep_chain() data['eor'] = self._eor if self._role is not None: data['role'] = self._role.serialize() if self._parent is not None: data['parent'] = self._parent.copy(exclude_tasks=True).serialize() data['parent_type'] = self._parent.__class__.__name__ return data def deserialize(self, data): ''' Override of the default deserialize method, to match the above overridden serialize method ''' # import is here to avoid import loops from assible.playbook.task_include import TaskInclude from assible.playbook.handler_task_include import HandlerTaskInclude # we don't want the full set of attributes (the task lists), as that # would lead to a serialize/deserialize loop for attr in self._valid_attrs: if attr in data and attr not in ('block', 'rescue', 'always'): setattr(self, attr, data.get(attr)) self._dep_chain = data.get('dep_chain', None) self._eor = data.get('eor', False) # if there was a serialized role, unpack it too role_data = data.get('role') if role_data: r = Role() r.deserialize(role_data) self._role = r parent_data = data.get('parent') if parent_data: parent_type = data.get('parent_type') if parent_type == 'Block': p = Block() elif parent_type == 'TaskInclude': p = TaskInclude() elif parent_type == 'HandlerTaskInclude': p = HandlerTaskInclude() p.deserialize(parent_data) self._parent = p self._dep_chain = self._parent.get_dep_chain() def set_loader(self, loader): self._loader = loader if self._parent: self._parent.set_loader(loader) elif self._role: self._role.set_loader(loader) dep_chain = self.get_dep_chain() if dep_chain: for dep in dep_chain: dep.set_loader(loader) def _get_parent_attribute(self, attr, extend=False, prepend=False): ''' Generic logic to get the attribute or parent attribute for a block value. ''' extend = self._valid_attrs[attr].extend prepend = self._valid_attrs[attr].prepend try: value = self._attributes[attr] # If parent is static, we can grab attrs from the parent # otherwise, defer to the grandparent if getattr(self._parent, 'statically_loaded', True): _parent = self._parent else: _parent = self._parent._parent if _parent and (value is Sentinel or extend): try: if getattr(_parent, 'statically_loaded', True): if hasattr(_parent, '_get_parent_attribute'): parent_value = _parent._get_parent_attribute(attr) else: parent_value = _parent._attributes.get( attr, Sentinel) if extend: value = self._extend_value(value, parent_value, prepend) else: value = parent_value except AttributeError: pass if self._role and (value is Sentinel or extend): try: parent_value = self._role._attributes.get(attr, Sentinel) if extend: value = self._extend_value(value, parent_value, prepend) else: value = parent_value dep_chain = self.get_dep_chain() if dep_chain and (value is Sentinel or extend): dep_chain.reverse() for dep in dep_chain: dep_value = dep._attributes.get(attr, Sentinel) if extend: value = self._extend_value( value, dep_value, prepend) else: value = dep_value if value is not Sentinel and not extend: break except AttributeError: pass if self._play and (value is Sentinel or extend): try: play_value = self._play._attributes.get(attr, Sentinel) if play_value is not Sentinel: if extend: value = self._extend_value(value, play_value, prepend) else: value = play_value except AttributeError: pass except KeyError: pass return value def filter_tagged_tasks(self, all_vars): ''' Creates a new block, with task lists filtered based on the tags. ''' def evaluate_and_append_task(target): tmp_list = [] for task in target: if isinstance(task, Block): filtered_block = evaluate_block(task) if filtered_block.has_tasks(): tmp_list.append(filtered_block) elif ((task.action == 'meta' and task.implicit) or (task.action == 'include' and task.evaluate_tags( [], self._play.skip_tags, all_vars=all_vars)) or task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars)): tmp_list.append(task) return tmp_list def evaluate_block(block): new_block = block.copy(exclude_parent=True, exclude_tasks=True) new_block._parent = block._parent new_block.block = evaluate_and_append_task(block.block) new_block.rescue = evaluate_and_append_task(block.rescue) new_block.always = evaluate_and_append_task(block.always) return new_block return evaluate_block(self) def has_tasks(self): return len(self.block) > 0 or len(self.rescue) > 0 or len( self.always) > 0 def get_include_params(self): if self._parent: return self._parent.get_include_params() else: return dict() def all_parents_static(self): ''' Determine if all of the parents of this block were statically loaded or not. Since Task/TaskInclude objects may be in the chain, they simply call their parents all_parents_static() method. Only Block objects in the chain check the statically_loaded value of the parent. ''' from assible.playbook.task_include import TaskInclude if self._parent: if isinstance(self._parent, TaskInclude) and not self._parent.statically_loaded: return False return self._parent.all_parents_static() return True def get_first_parent_include(self): from assible.playbook.task_include import TaskInclude if self._parent: if isinstance(self._parent, TaskInclude): return self._parent return self._parent.get_first_parent_include() return None
class Role(Base, Conditional, Taggable, CollectionSearch): _delegate_to = FieldAttribute(isa='string') _delegate_facts = FieldAttribute(isa='bool') def __init__(self, play=None, from_files=None, from_include=False): self._role_name = None self._role_path = None self._role_collection = None self._role_params = dict() self._loader = None self._metadata = None self._play = play self._parents = [] self._dependencies = [] self._task_blocks = [] self._handler_blocks = [] self._compiled_handler_blocks = None self._default_vars = dict() self._role_vars = dict() self._had_task_run = dict() self._completed = dict() if from_files is None: from_files = {} self._from_files = from_files # Indicates whether this role was included via include/import_role self.from_include = from_include super(Role, self).__init__() def __repr__(self): return self.get_name() def get_name(self, include_role_fqcn=True): if include_role_fqcn: return '.'.join(x for x in (self._role_collection, self._role_name) if x) return self._role_name @staticmethod def load(role_include, play, parent_role=None, from_files=None, from_include=False): if from_files is None: from_files = {} try: # The ROLE_CACHE is a dictionary of role names, with each entry # containing another dictionary corresponding to a set of parameters # specified for a role as the key and the Role() object itself. # We use frozenset to make the dictionary hashable. params = role_include.get_role_params() if role_include.when is not None: params['when'] = role_include.when if role_include.tags is not None: params['tags'] = role_include.tags if from_files is not None: params['from_files'] = from_files if role_include.vars: params['vars'] = role_include.vars params['from_include'] = from_include hashed_params = hash_params(params) if role_include.get_name() in play.ROLE_CACHE: for (entry, role_obj) in iteritems( play.ROLE_CACHE[role_include.get_name()]): if hashed_params == entry: if parent_role: role_obj.add_parent(parent_role) return role_obj # TODO: need to fix cycle detection in role load (maybe use an empty dict # for the in-flight in role cache as a sentinel that we're already trying to load # that role?) # see https://github.com/assible/assible/issues/61527 r = Role(play=play, from_files=from_files, from_include=from_include) r._load_role_data(role_include, parent_role=parent_role) if role_include.get_name() not in play.ROLE_CACHE: play.ROLE_CACHE[role_include.get_name()] = dict() # FIXME: how to handle cache keys for collection-based roles, since they're technically adjustable per task? play.ROLE_CACHE[role_include.get_name()][hashed_params] = r return r except RuntimeError: raise AssibleError( "A recursion loop was detected with the roles specified. Make sure child roles do not have dependencies on parent roles", obj=role_include._ds) def _load_role_data(self, role_include, parent_role=None): self._role_name = role_include.role self._role_path = role_include.get_role_path() self._role_collection = role_include._role_collection self._role_params = role_include.get_role_params() self._variable_manager = role_include.get_variable_manager() self._loader = role_include.get_loader() if parent_role: self.add_parent(parent_role) # copy over all field attributes from the RoleInclude # update self._attributes directly, to avoid squashing for (attr_name, _) in iteritems(self._valid_attrs): if attr_name in ('when', 'tags'): self._attributes[attr_name] = self._extend_value( self._attributes[attr_name], role_include._attributes[attr_name], ) else: self._attributes[attr_name] = role_include._attributes[ attr_name] # vars and default vars are regular dictionaries self._role_vars = self._load_role_yaml( 'vars', main=self._from_files.get('vars'), allow_dir=True) if self._role_vars is None: self._role_vars = dict() elif not isinstance(self._role_vars, dict): raise AssibleParserError( "The vars/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) self._default_vars = self._load_role_yaml( 'defaults', main=self._from_files.get('defaults'), allow_dir=True) if self._default_vars is None: self._default_vars = dict() elif not isinstance(self._default_vars, dict): raise AssibleParserError( "The defaults/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) # load the role's other files, if they exist metadata = self._load_role_yaml('meta') if metadata: self._metadata = RoleMetadata.load( metadata, owner=self, variable_manager=self._variable_manager, loader=self._loader) self._dependencies = self._load_dependencies() else: self._metadata = RoleMetadata() # reset collections list; roles do not inherit collections from parents, just use the defaults # FUTURE: use a private config default for this so we can allow it to be overridden later self.collections = [] # configure plugin/collection loading; either prepend the current role's collection or configure legacy plugin loading # FIXME: need exception for explicit assible.legacy? if self._role_collection: # this is a collection-hosted role self.collections.insert(0, self._role_collection) else: # this is a legacy role, but set the default collection if there is one default_collection = AssibleCollectionConfig.default_collection if default_collection: self.collections.insert(0, default_collection) # legacy role, ensure all plugin dirs under the role are added to plugin search path add_all_plugin_dirs(self._role_path) # collections can be specified in metadata for legacy or collection-hosted roles if self._metadata.collections: self.collections.extend((c for c in self._metadata.collections if c not in self.collections)) # if any collections were specified, ensure that core or legacy synthetic collections are always included if self.collections: # default append collection is core for collection-hosted roles, legacy for others default_append_collection = 'assible.builtin' if self._role_collection else 'assible.legacy' if 'assible.builtin' not in self.collections and 'assible.legacy' not in self.collections: self.collections.append(default_append_collection) task_data = self._load_role_yaml('tasks', main=self._from_files.get('tasks')) if task_data: try: self._task_blocks = load_list_of_blocks( task_data, play=self._play, role=self, loader=self._loader, variable_manager=self._variable_manager) except AssertionError as e: raise AssibleParserError( "The tasks/main.yml file for role '%s' must contain a list of tasks" % self._role_name, obj=task_data, orig_exc=e) handler_data = self._load_role_yaml( 'handlers', main=self._from_files.get('handlers')) if handler_data: try: self._handler_blocks = load_list_of_blocks( handler_data, play=self._play, role=self, use_handlers=True, loader=self._loader, variable_manager=self._variable_manager) except AssertionError as e: raise AssibleParserError( "The handlers/main.yml file for role '%s' must contain a list of tasks" % self._role_name, obj=handler_data, orig_exc=e) def _load_role_yaml(self, subdir, main=None, allow_dir=False): file_path = os.path.join(self._role_path, subdir) if self._loader.path_exists(file_path) and self._loader.is_directory( file_path): # Valid extensions and ordering for roles is hard-coded to maintain # role portability extensions = ['.yml', '.yaml', '.json'] # If no <main> is specified by the user, look for files with # extensions before bare name. Otherwise, look for bare name first. if main is None: _main = 'main' extensions.append('') else: _main = main extensions.insert(0, '') found_files = self._loader.find_vars_files(file_path, _main, extensions, allow_dir) if found_files: data = {} for found in found_files: new_data = self._loader.load_from_file(found) if new_data and allow_dir: data = combine_vars(data, new_data) else: data = new_data return data elif main is not None: raise AssibleParserError( "Could not find specified file in role: %s/%s" % (subdir, main)) return None def _load_dependencies(self): ''' Recursively loads role dependencies from the metadata list of dependencies, if it exists ''' deps = [] if self._metadata: for role_include in self._metadata.dependencies: r = Role.load(role_include, play=self._play, parent_role=self) deps.append(r) return deps # other functions def add_parent(self, parent_role): ''' adds a role to the list of this roles parents ''' if not isinstance(parent_role, Role): raise AssibleAssertionError() if parent_role not in self._parents: self._parents.append(parent_role) def get_parents(self): return self._parents def get_default_vars(self, dep_chain=None): dep_chain = [] if dep_chain is None else dep_chain default_vars = dict() for dep in self.get_all_dependencies(): default_vars = combine_vars(default_vars, dep.get_default_vars()) if dep_chain: for parent in dep_chain: default_vars = combine_vars(default_vars, parent._default_vars) default_vars = combine_vars(default_vars, self._default_vars) return default_vars def get_inherited_vars(self, dep_chain=None): dep_chain = [] if dep_chain is None else dep_chain inherited_vars = dict() if dep_chain: for parent in dep_chain: inherited_vars = combine_vars(inherited_vars, parent._role_vars) return inherited_vars def get_role_params(self, dep_chain=None): dep_chain = [] if dep_chain is None else dep_chain params = {} if dep_chain: for parent in dep_chain: params = combine_vars(params, parent._role_params) params = combine_vars(params, self._role_params) return params def get_vars(self, dep_chain=None, include_params=True): dep_chain = [] if dep_chain is None else dep_chain all_vars = self.get_inherited_vars(dep_chain) for dep in self.get_all_dependencies(): all_vars = combine_vars( all_vars, dep.get_vars(include_params=include_params)) all_vars = combine_vars(all_vars, self.vars) all_vars = combine_vars(all_vars, self._role_vars) if include_params: all_vars = combine_vars(all_vars, self.get_role_params(dep_chain=dep_chain)) return all_vars def get_direct_dependencies(self): return self._dependencies[:] def get_all_dependencies(self): ''' Returns a list of all deps, built recursively from all child dependencies, in the proper order in which they should be executed or evaluated. ''' child_deps = [] for dep in self.get_direct_dependencies(): for child_dep in dep.get_all_dependencies(): child_deps.append(child_dep) child_deps.append(dep) return child_deps def get_task_blocks(self): return self._task_blocks[:] def get_handler_blocks(self, play, dep_chain=None): # Do not recreate this list each time ``get_handler_blocks`` is called. # Cache the results so that we don't potentially overwrite with copied duplicates # # ``get_handler_blocks`` may be called when handling ``import_role`` during parsing # as well as with ``Play.compile_roles_handlers`` from ``TaskExecutor`` if self._compiled_handler_blocks: return self._compiled_handler_blocks self._compiled_handler_blocks = block_list = [] # update the dependency chain here if dep_chain is None: dep_chain = [] new_dep_chain = dep_chain + [self] for dep in self.get_direct_dependencies(): dep_blocks = dep.get_handler_blocks(play=play, dep_chain=new_dep_chain) block_list.extend(dep_blocks) for task_block in self._handler_blocks: new_task_block = task_block.copy() new_task_block._dep_chain = new_dep_chain new_task_block._play = play block_list.append(new_task_block) return block_list def has_run(self, host): ''' Returns true if this role has been iterated over completely and at least one task was run ''' return host.name in self._completed and not self._metadata.allow_duplicates def compile(self, play, dep_chain=None): ''' Returns the task list for this role, which is created by first recursively compiling the tasks for all direct dependencies, and then adding on the tasks for this role. The role compile() also remembers and saves the dependency chain with each task, so tasks know by which route they were found, and can correctly take their parent's tags/conditionals into account. ''' block_list = [] # update the dependency chain here if dep_chain is None: dep_chain = [] new_dep_chain = dep_chain + [self] deps = self.get_direct_dependencies() for dep in deps: dep_blocks = dep.compile(play=play, dep_chain=new_dep_chain) block_list.extend(dep_blocks) for idx, task_block in enumerate(self._task_blocks): new_task_block = task_block.copy() new_task_block._dep_chain = new_dep_chain new_task_block._play = play if idx == len(self._task_blocks) - 1: new_task_block._eor = True block_list.append(new_task_block) return block_list def serialize(self, include_deps=True): res = super(Role, self).serialize() res['_role_name'] = self._role_name res['_role_path'] = self._role_path res['_role_vars'] = self._role_vars res['_role_params'] = self._role_params res['_default_vars'] = self._default_vars res['_had_task_run'] = self._had_task_run.copy() res['_completed'] = self._completed.copy() if self._metadata: res['_metadata'] = self._metadata.serialize() if include_deps: deps = [] for role in self.get_direct_dependencies(): deps.append(role.serialize()) res['_dependencies'] = deps parents = [] for parent in self._parents: parents.append(parent.serialize(include_deps=False)) res['_parents'] = parents return res def deserialize(self, data, include_deps=True): self._role_name = data.get('_role_name', '') self._role_path = data.get('_role_path', '') self._role_vars = data.get('_role_vars', dict()) self._role_params = data.get('_role_params', dict()) self._default_vars = data.get('_default_vars', dict()) self._had_task_run = data.get('_had_task_run', dict()) self._completed = data.get('_completed', dict()) if include_deps: deps = [] for dep in data.get('_dependencies', []): r = Role() r.deserialize(dep) deps.append(r) setattr(self, '_dependencies', deps) parent_data = data.get('_parents', []) parents = [] for parent in parent_data: r = Role() r.deserialize(parent, include_deps=False) parents.append(r) setattr(self, '_parents', parents) metadata_data = data.get('_metadata') if metadata_data: m = RoleMetadata() m.deserialize(metadata_data) self._metadata = m super(Role, self).deserialize(data) def set_loader(self, loader): self._loader = loader for parent in self._parents: parent.set_loader(loader) for dep in self.get_direct_dependencies(): dep.set_loader(loader)
class IncludeRole(TaskInclude): """ A Role include is derived from a regular role to handle the special circumstances related to the `- include_role: ...` """ BASE = ('name', 'role') # directly assigned FROM_ARGS = ('tasks_from', 'vars_from', 'defaults_from', 'handlers_from' ) # used to populate from dict in role OTHER_ARGS = ('apply', 'public', 'allow_duplicates' ) # assigned to matching property VALID_ARGS = tuple(frozenset(BASE + FROM_ARGS + OTHER_ARGS)) # all valid args # ================================================================================= # ATTRIBUTES # private as this is a 'module options' vs a task property _allow_duplicates = FieldAttribute(isa='bool', default=True, private=True) _public = FieldAttribute(isa='bool', default=False, private=True) def __init__(self, block=None, role=None, task_include=None): super(IncludeRole, self).__init__(block=block, role=role, task_include=task_include) self._from_files = {} self._parent_role = role self._role_name = None self._role_path = None def get_name(self): ''' return the name of the task ''' return self.name or "%s : %s" % (self.action, self._role_name) 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, collection_list=self.collections) ri.vars.update(self.vars) # build role actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files, from_include=True) actual_role._metadata.allow_duplicates = self.allow_duplicates if self.statically_loaded or self.public: myplay.roles.append(actual_role) # save this for later use self._role_path = actual_role._role_path # compile role with parent roles as dependencies to ensure they inherit # variables if not self._parent_role: dep_chain = [] else: dep_chain = list(self._parent_role._parents) dep_chain.append(self._parent_role) p_block = self.build_parent_block() # collections value is not inherited; override with the value we calculated during role setup p_block.collections = actual_role.collections blocks = actual_role.compile(play=myplay, dep_chain=dep_chain) for b in blocks: b._parent = p_block # HACK: parent inheritance doesn't seem to have a way to handle this intermediate override until squashed/finalized b.collections = actual_role.collections # updated available handlers in play handlers = actual_role.get_handler_blocks(play=myplay) for h in handlers: h._parent = p_block myplay.handlers = myplay.handlers + handlers return blocks, handlers @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): ir = IncludeRole(block, role, task_include=task_include).load_data( data, variable_manager=variable_manager, loader=loader) # Validate options my_arg_names = frozenset(ir.args.keys()) # name is needed, or use role as alias ir._role_name = ir.args.get('name', ir.args.get('role')) if ir._role_name is None: raise AssibleParserError("'name' is a required field for %s." % ir.action, obj=data) if 'public' in ir.args and ir.action != 'include_role': raise AssibleParserError('Invalid options for %s: public' % ir.action, obj=data) # validate bad args, otherwise we silently ignore bad_opts = my_arg_names.difference(IncludeRole.VALID_ARGS) if bad_opts: raise AssibleParserError('Invalid options for %s: %s' % (ir.action, ','.join(list(bad_opts))), obj=data) # build options for role includes for key in my_arg_names.intersection(IncludeRole.FROM_ARGS): from_key = key.replace('_from', '') args_value = ir.args.get(key) if not isinstance(args_value, string_types): raise AssibleParserError( 'Expected a string for %s but got %s instead' % (key, type(args_value))) ir._from_files[from_key] = basename(args_value) apply_attrs = ir.args.get('apply', {}) if apply_attrs and ir.action != 'include_role': raise AssibleParserError('Invalid options for %s: apply' % ir.action, obj=data) elif not isinstance(apply_attrs, dict): raise AssibleParserError( 'Expected a dict for apply but got %s instead' % type(apply_attrs), obj=data) # manual list as otherwise the options would set other task parameters we don't want. for option in my_arg_names.intersection(IncludeRole.OTHER_ARGS): setattr(ir, option, ir.args.get(option)) return ir def copy(self, exclude_parent=False, exclude_tasks=False): new_me = super(IncludeRole, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) new_me.statically_loaded = self.statically_loaded new_me._from_files = self._from_files.copy() new_me._parent_role = self._parent_role new_me._role_name = self._role_name new_me._role_path = self._role_path return new_me def get_include_params(self): v = super(IncludeRole, self).get_include_params() if self._parent_role: v.update(self._parent_role.get_role_params()) v.setdefault('assible_parent_role_names', []).insert(0, self._parent_role.get_name()) v.setdefault('assible_parent_role_paths', []).insert(0, self._parent_role._role_path) return v
class RoleMetadata(Base, CollectionSearch): ''' This class wraps the parsing and validation of the optional metadata within each Role (meta/main.yml). ''' _allow_duplicates = FieldAttribute(isa='bool', default=False) _dependencies = FieldAttribute(isa='list', default=list) _galaxy_info = FieldAttribute(isa='GalaxyInfo') def __init__(self, owner=None): self._owner = owner super(RoleMetadata, self).__init__() @staticmethod def load(data, owner, variable_manager=None, loader=None): ''' Returns a new RoleMetadata object based on the datastructure passed in. ''' if not isinstance(data, dict): raise AssibleParserError( "the 'meta/main.yml' for role %s is not a dictionary" % owner.get_name()) m = RoleMetadata(owner=owner).load_data( data, variable_manager=variable_manager, loader=loader) return m def _load_dependencies(self, attr, ds): ''' This is a helper loading function for the dependencies list, which returns a list of RoleInclude objects ''' roles = [] if ds: if not isinstance(ds, list): raise AssibleParserError( "Expected role dependencies to be a list.", obj=self._ds) for role_def in ds: if isinstance(role_def, string_types ) or 'role' in role_def or 'name' in role_def: roles.append(role_def) continue try: # role_def is new style: { src: 'galaxy.role,version,name', other_vars: "here" } def_parsed = RoleRequirement.role_yaml_parse(role_def) if def_parsed.get('name'): role_def['name'] = def_parsed['name'] roles.append(role_def) except AssibleError as exc: raise AssibleParserError(to_native(exc), obj=role_def, orig_exc=exc) current_role_path = None collection_search_list = None if self._owner: current_role_path = os.path.dirname(self._owner._role_path) # if the calling role has a collections search path defined, consult it collection_search_list = self._owner.collections[:] or [] # if the calling role is a collection role, ensure that its containing collection is searched first owner_collection = self._owner._role_collection if owner_collection: collection_search_list = [ c for c in collection_search_list if c != owner_collection ] collection_search_list.insert(0, owner_collection) # ensure fallback role search works if 'assible.legacy' not in collection_search_list: collection_search_list.append('assible.legacy') try: return load_list_of_roles( roles, play=self._owner._play, current_role_path=current_role_path, variable_manager=self._variable_manager, loader=self._loader, collection_search_list=collection_search_list) except AssertionError as e: raise AssibleParserError( "A malformed list of role dependencies was encountered.", obj=self._ds, orig_exc=e) def _load_galaxy_info(self, attr, ds): ''' This is a helper loading function for the galaxy info entry in the metadata, which returns a GalaxyInfo object rather than a simple dictionary. ''' return ds def serialize(self): return dict(allow_duplicates=self._allow_duplicates, dependencies=self._dependencies) def deserialize(self, data): setattr(self, 'allow_duplicates', data.get('allow_duplicates', False)) setattr(self, 'dependencies', data.get('dependencies', []))
class BaseSubClass(base.Base): _name = FieldAttribute(isa='string', default='', always_post_validate=True) _test_attr_bool = FieldAttribute(isa='bool', always_post_validate=True) _test_attr_int = FieldAttribute(isa='int', always_post_validate=True) _test_attr_float = FieldAttribute(isa='float', default=3.14159, always_post_validate=True) _test_attr_list = FieldAttribute(isa='list', listof=string_types, always_post_validate=True) _test_attr_list_no_listof = FieldAttribute(isa='list', always_post_validate=True) _test_attr_list_required = FieldAttribute(isa='list', listof=string_types, required=True, default=list, always_post_validate=True) _test_attr_string = FieldAttribute( isa='string', default='the_test_attr_string_default_value') _test_attr_string_required = FieldAttribute( isa='string', required=True, default='the_test_attr_string_default_value') _test_attr_percent = FieldAttribute(isa='percent', always_post_validate=True) _test_attr_set = FieldAttribute(isa='set', default=set, always_post_validate=True) _test_attr_dict = FieldAttribute(isa='dict', default=lambda: {'a_key': 'a_value'}, always_post_validate=True) _test_attr_class = FieldAttribute(isa='class', class_type=ExampleSubClass) _test_attr_class_post_validate = FieldAttribute(isa='class', class_type=ExampleSubClass, always_post_validate=True) _test_attr_unknown_isa = FieldAttribute(isa='not_a_real_isa', always_post_validate=True) _test_attr_example = FieldAttribute(isa='string', default='the_default', always_post_validate=True) _test_attr_none = FieldAttribute(isa='string', always_post_validate=True) _test_attr_preprocess = FieldAttribute( isa='string', default='the default for preprocess') _test_attr_method = FieldAttribute(isa='string', default='some attr with a getter', always_post_validate=True) _test_attr_method_missing = FieldAttribute( isa='string', default='some attr with a missing getter', always_post_validate=True) def _get_attr_test_attr_method(self): return 'foo bar' def _validate_test_attr_example(self, attr, name, value): if not isinstance(value, str): raise ExampleException( '_test_attr_example is not a string: %s type=%s' % (value, type(value))) def _post_validate_test_attr_example(self, attr, value, templar): after_template_value = templar.template(value) return after_template_value def _post_validate_test_attr_none(self, attr, value, templar): return None def _get_parent_attribute(self, attr, extend=False, prepend=False): value = None try: value = self._attributes[attr] if self._parent and (value is None or extend): parent_value = getattr(self._parent, attr, None) if extend: value = self._extend_value(value, parent_value, prepend) else: value = parent_value except KeyError: pass return value
class Task(Base, Conditional, Taggable, CollectionSearch): """ A task is a language feature that represents a call to a module, with given arguments and other parameters. A handler is a subclass of a task. Usage: Task.load(datastructure) -> Task Task.something(...) """ # ================================================================================= # ATTRIBUTES # load_<attribute_name> and # validate_<attribute_name> # will be used if defined # might be possible to define others # NOTE: ONLY set defaults on task attributes that are not inheritable, # inheritance is only triggered if the 'current value' is None, # default can be set at play/top level object and inheritance will take it's course. _args = FieldAttribute(isa='dict', default=dict) _action = FieldAttribute(isa='string') _async_val = FieldAttribute(isa='int', default=0, alias='async') _changed_when = FieldAttribute(isa='list', default=list) _delay = FieldAttribute(isa='int', default=5) _delegate_to = FieldAttribute(isa='string') _delegate_facts = FieldAttribute(isa='bool') _failed_when = FieldAttribute(isa='list', default=list) _loop = FieldAttribute() _loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False) _notify = FieldAttribute(isa='list') _poll = FieldAttribute(isa='int', default=C.DEFAULT_POLL_INTERVAL) _register = FieldAttribute(isa='string', static=True) _retries = FieldAttribute(isa='int', default=3) _until = FieldAttribute(isa='list', default=list) # deprecated, used to be loop and loop_args but loop has been repurposed _loop_with = FieldAttribute(isa='string', private=True, inherit=False) def __init__(self, block=None, role=None, task_include=None): ''' constructors a task, without the Task.load classmethod, it will be pretty blank ''' # This is a reference of all the candidate action names for transparent execution of module_defaults with redirected content # This isn't a FieldAttribute to prevent it from being set via the playbook self._assible_internal_redirect_list = [] self._role = role self._parent = None self.implicit = False if task_include: self._parent = task_include else: self._parent = block super(Task, self).__init__() def get_path(self): ''' return the absolute path of the task with its line number ''' path = "" if hasattr(self, '_ds') and hasattr(self._ds, '_data_source') and hasattr(self._ds, '_line_number'): path = "%s:%s" % (self._ds._data_source, self._ds._line_number) elif hasattr(self._parent._play, '_ds') and hasattr(self._parent._play._ds, '_data_source') and hasattr(self._parent._play._ds, '_line_number'): path = "%s:%s" % (self._parent._play._ds._data_source, self._parent._play._ds._line_number) return path def get_name(self, include_role_fqcn=True): ''' return the name of the task ''' if self._role: role_name = self._role.get_name(include_role_fqcn=include_role_fqcn) if self._role and self.name and role_name not in self.name: return "%s : %s" % (role_name, self.name) elif self.name: return self.name else: if self._role: return "%s : %s" % (role_name, self.action) else: return "%s" % (self.action,) def _merge_kv(self, ds): if ds is None: return "" elif isinstance(ds, string_types): return ds elif isinstance(ds, dict): buf = "" for (k, v) in iteritems(ds): if k.startswith('_'): continue buf = buf + "%s=%s " % (k, v) buf = buf.strip() return buf @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): t = Task(block=block, role=role, task_include=task_include) return t.load_data(data, variable_manager=variable_manager, loader=loader) def __repr__(self): ''' returns a human readable representation of the task ''' if self.get_name() == 'meta': return "TASK: meta (%s)" % self.args['_raw_params'] else: return "TASK: %s" % self.get_name() def _preprocess_with_loop(self, ds, new_ds, k, v): ''' take a lookup plugin name and store it correctly ''' loop_name = k.replace("with_", "") if new_ds.get('loop') is not None or new_ds.get('loop_with') is not None: raise AssibleError("duplicate loop in task: %s" % loop_name, obj=ds) if v is None: raise AssibleError("you must specify a value when using %s" % k, obj=ds) new_ds['loop_with'] = loop_name new_ds['loop'] = v # display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead", # version="2.10", collection_name='assible.builtin') def preprocess_data(self, ds): ''' tasks are especially complex arguments so need pre-processing. keep it short. ''' if not isinstance(ds, dict): raise AssibleAssertionError('ds (%s) should be a dict but was a %s' % (ds, type(ds))) # the new, cleaned datastructure, which will have legacy # items reduced to a standard structure suitable for the # attributes of the task class new_ds = AssibleMapping() if isinstance(ds, AssibleBaseYAMLObject): new_ds.assible_pos = ds.assible_pos # since this affects the task action parsing, we have to resolve in preprocess instead of in typical validator default_collection = AssibleCollectionConfig.default_collection collections_list = ds.get('collections') if collections_list is None: # use the parent value if our ds doesn't define it collections_list = self.collections else: # Validate this untemplated field early on to guarantee we are dealing with a list. # This is also done in CollectionSearch._load_collections() but this runs before that call. collections_list = self.get_validated_value('collections', self._collections, collections_list, None) if default_collection and not self._role: # FIXME: and not a collections role if collections_list: if default_collection not in collections_list: collections_list.insert(0, default_collection) else: collections_list = [default_collection] if collections_list and 'assible.builtin' not in collections_list and 'assible.legacy' not in collections_list: collections_list.append('assible.legacy') if collections_list: ds['collections'] = collections_list # use the args parsing class to determine the action, args, # and the delegate_to value from the various possible forms # supported as legacy args_parser = ModuleArgsParser(task_ds=ds, collection_list=collections_list) try: (action, args, delegate_to) = args_parser.parse() except AssibleParserError as e: # if the raises exception was created with obj=ds args, then it includes the detail # so we dont need to add it so we can just re raise. if e._obj: raise # But if it wasn't, we can add the yaml object now to get more detail raise AssibleParserError(to_native(e), obj=ds, orig_exc=e) else: self._assible_internal_redirect_list = args_parser.internal_redirect_list[:] # the command/shell/script modules used to support the `cmd` arg, # which corresponds to what we now call _raw_params, so move that # value over to _raw_params (assuming it is empty) if action in ('command', 'shell', 'script'): if 'cmd' in args: if args.get('_raw_params', '') != '': raise AssibleError("The 'cmd' argument cannot be used when other raw parameters are specified." " Please put everything in one or the other place.", obj=ds) args['_raw_params'] = args.pop('cmd') new_ds['action'] = action new_ds['args'] = args new_ds['delegate_to'] = delegate_to # we handle any 'vars' specified in the ds here, as we may # be adding things to them below (special handling for includes). # When that deprecated feature is removed, this can be too. if 'vars' in ds: # _load_vars is defined in Base, and is used to load a dictionary # or list of dictionaries in a standard way new_ds['vars'] = self._load_vars(None, ds.get('vars')) else: new_ds['vars'] = dict() for (k, v) in iteritems(ds): if k in ('action', 'local_action', 'args', 'delegate_to') or k == action or k == 'shell': # we don't want to re-assign these values, which were determined by the ModuleArgsParser() above continue elif k.startswith('with_') and k.replace("with_", "") in lookup_loader: # transform into loop property self._preprocess_with_loop(ds, new_ds, k, v) else: # pre-2.0 syntax allowed variables for include statements at the top level of the task, # so we move those into the 'vars' dictionary here, and show a deprecation message # as we will remove this at some point in the future. if action in ('include',) and k not in self._valid_attrs and k not in self.DEPRECATED_ATTRIBUTES: display.deprecated("Specifying include variables at the top-level of the task is deprecated." " Please see:\nhttps://docs.assible.com/assible/playbooks_roles.html#task-include-files-and-encouraging-reuse\n\n" " for currently supported syntax regarding included files and variables", version="2.12", collection_name='assible.builtin') new_ds['vars'][k] = v elif C.INVALID_TASK_ATTRIBUTE_FAILED or k in self._valid_attrs: new_ds[k] = v else: display.warning("Ignoring invalid attribute: %s" % k) return super(Task, self).preprocess_data(new_ds) def _load_loop_control(self, attr, ds): if not isinstance(ds, dict): raise AssibleParserError( "the `loop_control` value must be specified as a dictionary and cannot " "be a variable itself (though it can contain variables)", obj=ds, ) return LoopControl.load(data=ds, variable_manager=self._variable_manager, loader=self._loader) def _validate_attributes(self, ds): try: super(Task, self)._validate_attributes(ds) except AssibleParserError as e: e.message += '\nThis error can be suppressed as a warning using the "invalid_task_attribute_failed" configuration' raise e def post_validate(self, templar): ''' Override of base class post_validate, to also do final validation on the block and task include (if any) to which this task belongs. ''' if self._parent: self._parent.post_validate(templar) if AssibleCollectionConfig.default_collection: pass super(Task, self).post_validate(templar) def _post_validate_loop(self, attr, value, templar): ''' Override post validation for the loop field, which is templated specially in the TaskExecutor class when evaluating loops. ''' return value def _post_validate_environment(self, attr, value, templar): ''' Override post validation of vars on the play, as we don't want to template these too early. ''' env = {} if value is not None: def _parse_env_kv(k, v): try: env[k] = templar.template(v, convert_bare=False) except AssibleUndefinedVariable as e: error = to_native(e) if self.action in ('setup', 'gather_facts') and 'assible_facts.env' in error or 'assible_env' in error: # ignore as fact gathering is required for 'env' facts return raise if isinstance(value, list): for env_item in value: if isinstance(env_item, dict): for k in env_item: _parse_env_kv(k, env_item[k]) else: isdict = templar.template(env_item, convert_bare=False) if isinstance(isdict, dict): env.update(isdict) else: display.warning("could not parse environment value, skipping: %s" % value) elif isinstance(value, dict): # should not really happen env = dict() for env_item in value: _parse_env_kv(env_item, value[env_item]) else: # at this point it should be a simple string, also should not happen env = templar.template(value, convert_bare=False) return env def _post_validate_changed_when(self, attr, value, templar): ''' changed_when is evaluated after the execution of the task is complete, and should not be templated during the regular post_validate step. ''' return value def _post_validate_failed_when(self, attr, value, templar): ''' failed_when is evaluated after the execution of the task is complete, and should not be templated during the regular post_validate step. ''' return value def _post_validate_until(self, attr, value, templar): ''' until is evaluated after the execution of the task is complete, and should not be templated during the regular post_validate step. ''' return value def get_vars(self): all_vars = dict() if self._parent: all_vars.update(self._parent.get_vars()) all_vars.update(self.vars) if 'tags' in all_vars: del all_vars['tags'] if 'when' in all_vars: del all_vars['when'] return all_vars def get_include_params(self): all_vars = dict() if self._parent: all_vars.update(self._parent.get_include_params()) if self.action in ('include', 'include_tasks', 'include_role'): all_vars.update(self.vars) return all_vars def copy(self, exclude_parent=False, exclude_tasks=False): new_me = super(Task, self).copy() # if the task has an associated list of candidate names, copy it to the new object too new_me._assible_internal_redirect_list = self._assible_internal_redirect_list[:] new_me._parent = None if self._parent and not exclude_parent: new_me._parent = self._parent.copy(exclude_tasks=exclude_tasks) new_me._role = None if self._role: new_me._role = self._role new_me.implicit = self.implicit return new_me def serialize(self): data = super(Task, self).serialize() if not self._squashed and not self._finalized: if self._parent: data['parent'] = self._parent.serialize() data['parent_type'] = self._parent.__class__.__name__ if self._role: data['role'] = self._role.serialize() if self._assible_internal_redirect_list: data['_assible_internal_redirect_list'] = self._assible_internal_redirect_list[:] data['implicit'] = self.implicit return data def deserialize(self, data): # import is here to avoid import loops from assible.playbook.task_include import TaskInclude from assible.playbook.handler_task_include import HandlerTaskInclude parent_data = data.get('parent', None) if parent_data: parent_type = data.get('parent_type') if parent_type == 'Block': p = Block() elif parent_type == 'TaskInclude': p = TaskInclude() elif parent_type == 'HandlerTaskInclude': p = HandlerTaskInclude() p.deserialize(parent_data) self._parent = p del data['parent'] role_data = data.get('role') if role_data: r = Role() r.deserialize(role_data) self._role = r del data['role'] self._assible_internal_redirect_list = data.get('_assible_internal_redirect_list', []) self.implicit = data.get('implicit', False) super(Task, self).deserialize(data) def set_loader(self, loader): ''' Sets the loader on this object and recursively on parent, child objects. This is used primarily after the Task has been serialized/deserialized, which does not preserve the loader. ''' self._loader = loader if self._parent: self._parent.set_loader(loader) def _get_parent_attribute(self, attr, extend=False, prepend=False): ''' Generic logic to get the attribute or parent attribute for a task value. ''' extend = self._valid_attrs[attr].extend prepend = self._valid_attrs[attr].prepend try: value = self._attributes[attr] # If parent is static, we can grab attrs from the parent # otherwise, defer to the grandparent if getattr(self._parent, 'statically_loaded', True): _parent = self._parent else: _parent = self._parent._parent if _parent and (value is Sentinel or extend): if getattr(_parent, 'statically_loaded', True): # vars are always inheritable, other attributes might not be for the parent but still should be for other ancestors if attr != 'vars' and hasattr(_parent, '_get_parent_attribute'): parent_value = _parent._get_parent_attribute(attr) else: parent_value = _parent._attributes.get(attr, Sentinel) if extend: value = self._extend_value(value, parent_value, prepend) else: value = parent_value except KeyError: pass return value def get_dep_chain(self): if self._parent: return self._parent.get_dep_chain() else: return None def get_search_path(self): ''' Return the list of paths you should search for files, in order. This follows role/playbook dependency chain. ''' path_stack = [] dep_chain = self.get_dep_chain() # inside role: add the dependency chain from current to dependent if dep_chain: path_stack.extend(reversed([x._role_path for x in dep_chain])) # add path of task itself, unless it is already in the list task_dir = os.path.dirname(self.get_path()) if task_dir not in path_stack: path_stack.append(task_dir) return path_stack def all_parents_static(self): if self._parent: return self._parent.all_parents_static() return True def get_first_parent_include(self): from assible.playbook.task_include import TaskInclude if self._parent: if isinstance(self._parent, TaskInclude): return self._parent return self._parent.get_first_parent_include() return None
class RoleDefinition(Base, Conditional, Taggable, CollectionSearch): _role = FieldAttribute(isa='string') def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None, collection_list=None): super(RoleDefinition, self).__init__() self._play = play self._variable_manager = variable_manager self._loader = loader self._role_path = None self._role_collection = None self._role_basedir = role_basedir self._role_params = dict() self._collection_list = collection_list # def __repr__(self): # return 'ROLEDEF: ' + self._attributes.get('role', '<no name set>') @staticmethod def load(data, variable_manager=None, loader=None): raise AssibleError("not implemented") def preprocess_data(self, ds): # role names that are simply numbers can be parsed by PyYAML # as integers even when quoted, so turn it into a string type if isinstance(ds, int): ds = "%s" % ds if not isinstance(ds, dict) and not isinstance( ds, string_types) and not isinstance(ds, AssibleBaseYAMLObject): raise AssibleAssertionError() if isinstance(ds, dict): ds = super(RoleDefinition, self).preprocess_data(ds) # save the original ds for use later self._ds = ds # we create a new data structure here, using the same # object used internally by the YAML parsing code so we # can preserve file:line:column information if it exists new_ds = AssibleMapping() if isinstance(ds, AssibleBaseYAMLObject): new_ds.assible_pos = ds.assible_pos # first we pull the role name out of the data structure, # and then use that to determine the role path (which may # result in a new role name, if it was a file path) role_name = self._load_role_name(ds) (role_name, role_path) = self._load_role_path(role_name) # next, we split the role params out from the valid role # attributes and update the new datastructure with that # result and the role name if isinstance(ds, dict): (new_role_def, role_params) = self._split_role_params(ds) new_ds.update(new_role_def) self._role_params = role_params # set the role name in the new ds new_ds['role'] = role_name # we store the role path internally self._role_path = role_path # and return the cleaned-up data structure return new_ds def _load_role_name(self, ds): ''' Returns the role name (either the role: or name: field) from the role definition, or (when the role definition is a simple string), just that string ''' if isinstance(ds, string_types): return ds role_name = ds.get('role', ds.get('name')) if not role_name or not isinstance(role_name, string_types): raise AssibleError('role definitions must contain a role name', obj=ds) # if we have the required datastructures, and if the role_name # contains a variable, try and template it now if self._variable_manager: all_vars = self._variable_manager.get_vars(play=self._play) templar = Templar(loader=self._loader, variables=all_vars) role_name = templar.template(role_name) return role_name def _load_role_path(self, role_name): ''' the 'role', as specified in the ds (or as a bare string), can either be a simple name or a full path. If it is a full path, we use the basename as the role name, otherwise we take the name as-given and append it to the default role path ''' # create a templar class to template the dependency names, in # case they contain variables if self._variable_manager is not None: all_vars = self._variable_manager.get_vars(play=self._play) else: all_vars = dict() templar = Templar(loader=self._loader, variables=all_vars) role_name = templar.template(role_name) role_tuple = None # try to load as a collection-based role first if self._collection_list or AssibleCollectionRef.is_valid_fqcr( role_name): role_tuple = _get_collection_role_path(role_name, self._collection_list) if role_tuple: # we found it, stash collection data and return the name/path tuple self._role_collection = role_tuple[2] return role_tuple[0:2] # We didn't find a collection role, look in defined role paths # FUTURE: refactor this to be callable from internal so we can properly order # assible.legacy searches with the collections keyword # we always start the search for roles in the base directory of the playbook role_search_paths = [ os.path.join(self._loader.get_basedir(), u'roles'), ] # also search in the configured roles path if C.DEFAULT_ROLES_PATH: role_search_paths.extend(C.DEFAULT_ROLES_PATH) # next, append the roles basedir, if it was set, so we can # search relative to that directory for dependent roles if self._role_basedir: role_search_paths.append(self._role_basedir) # finally as a last resort we look in the current basedir as set # in the loader (which should be the playbook dir itself) but without # the roles/ dir appended role_search_paths.append(self._loader.get_basedir()) # now iterate through the possible paths and return the first one we find for path in role_search_paths: path = templar.template(path) role_path = unfrackpath(os.path.join(path, role_name)) if self._loader.path_exists(role_path): return (role_name, role_path) # if not found elsewhere try to extract path from name role_path = unfrackpath(role_name) if self._loader.path_exists(role_path): role_name = os.path.basename(role_name) return (role_name, role_path) searches = (self._collection_list or []) + role_search_paths raise AssibleError("the role '%s' was not found in %s" % (role_name, ":".join(searches)), obj=self._ds) def _split_role_params(self, ds): ''' Splits any random role params off from the role spec and store them in a dictionary of params for parsing later ''' role_def = dict() role_params = dict() base_attribute_names = frozenset(self._valid_attrs.keys()) for (key, value) in iteritems(ds): # use the list of FieldAttribute values to determine what is and is not # an extra parameter for this role (or sub-class of this role) # FIXME: hard-coded list of exception key names here corresponds to the # connection fields in the Base class. There may need to be some # other mechanism where we exclude certain kinds of field attributes, # or make this list more automatic in some way so we don't have to # remember to update it manually. if key not in base_attribute_names: # this key does not match a field attribute, so it must be a role param role_params[key] = value else: # this is a field attribute, so copy it over directly role_def[key] = value return (role_def, role_params) def get_role_params(self): return self._role_params.copy() def get_role_path(self): return self._role_path def get_name(self, include_role_fqcn=True): if include_role_fqcn: return '.'.join(x for x in (self._role_collection, self.role) if x) return self.role
class Base(FieldAttributeBase): _name = FieldAttribute(isa='string', default='', always_post_validate=True, inherit=False) # connection/transport _connection = FieldAttribute(isa='string', default=context.cliargs_deferred_get('connection')) _port = FieldAttribute(isa='int') _remote_user = FieldAttribute(isa='string', default=context.cliargs_deferred_get('remote_user')) # variables _vars = FieldAttribute(isa='dict', priority=100, inherit=False, static=True) # module default params _module_defaults = FieldAttribute(isa='list', extend=True, prepend=True) # flags and misc. settings _environment = FieldAttribute(isa='list', extend=True, prepend=True) _no_log = FieldAttribute(isa='bool') _run_once = FieldAttribute(isa='bool') _ignore_errors = FieldAttribute(isa='bool') _ignore_unreachable = FieldAttribute(isa='bool') _check_mode = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('check')) _diff = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('diff')) _any_errors_fatal = FieldAttribute(isa='bool', default=C.ANY_ERRORS_FATAL) _throttle = FieldAttribute(isa='int', default=0) _timeout = FieldAttribute(isa='int', default=C.TASK_TIMEOUT) # explicitly invoke a debugger on tasks _debugger = FieldAttribute(isa='string') # Privilege escalation _become = FieldAttribute(isa='bool', default=context.cliargs_deferred_get('become')) _become_method = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_method')) _become_user = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_user')) _become_flags = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_flags')) _become_exe = FieldAttribute(isa='string', default=context.cliargs_deferred_get('become_exe')) # used to hold sudo/su stuff DEPRECATED_ATTRIBUTES = []