Example #1
0
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
Example #2
0
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)
Example #3
0
 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'))
Example #4
0
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
Example #5
0
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
Example #6
0
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
Example #7
0
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
Example #8
0
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
Example #9
0
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
Example #10
0
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
Example #11
0
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']
Example #12
0
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))
Example #13
0
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
Example #14
0
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)
Example #15
0
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
Example #16
0
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', []))
Example #17
0
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
Example #18
0
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
Example #19
0
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
Example #20
0
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 = []