From 393246fdd3ebd75eaa23de0f84efe71bfec5c305 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Fri, 20 Mar 2015 14:13:51 -0500 Subject: [PATCH] Make v2 playbook class attributes inheritable Also fixing some other become-related things --- v2/ansible/executor/connection_info.py | 16 +++---- v2/ansible/playbook/base.py | 29 +++++++++--- v2/ansible/playbook/become.py | 38 +++++++++++++++ v2/ansible/playbook/block.py | 46 ++++++++++++++----- v2/ansible/playbook/helpers.py | 2 + v2/ansible/playbook/play.py | 1 + v2/ansible/playbook/role/__init__.py | 19 ++++---- v2/ansible/playbook/role/definition.py | 8 +++- v2/ansible/playbook/task.py | 30 ++++++++---- v2/samples/roles/test_become_r1/meta/main.yml | 1 + .../roles/test_become_r1/tasks/main.yml | 2 + v2/samples/roles/test_become_r2/meta/main.yml | 3 ++ .../roles/test_become_r2/tasks/main.yml | 2 + v2/samples/test_become.yml | 6 +++ 14 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 v2/samples/roles/test_become_r1/meta/main.yml create mode 100644 v2/samples/roles/test_become_r1/tasks/main.yml create mode 100644 v2/samples/roles/test_become_r2/meta/main.yml create mode 100644 v2/samples/roles/test_become_r2/tasks/main.yml diff --git a/v2/ansible/executor/connection_info.py b/v2/ansible/executor/connection_info.py index 26a14a23f9..165cd1245f 100644 --- a/v2/ansible/executor/connection_info.py +++ b/v2/ansible/executor/connection_info.py @@ -157,13 +157,10 @@ class ConnectionInformation: new_info.copy(self) for attr in ('connection', 'remote_user', 'become', 'become_user', 'become_pass', 'become_method', 'environment', 'no_log'): - attr_val = None if hasattr(task, attr): attr_val = getattr(task, attr) - if task._block and hasattr(task._block, attr) and not attr_val: - attr_val = getattr(task._block, attr) - if attr_val: - setattr(new_info, attr, attr_val) + if attr_val: + setattr(new_info, attr, attr_val) return new_info @@ -184,6 +181,7 @@ class ConnectionInformation: executable = executable or '$SHELL' + success_cmd = pipes.quote('echo %s; %s' % (success_key, cmd)) if self.become: if self.become_method == 'sudo': # Rather than detect if sudo wants a password this time, -k makes sudo always ask for @@ -195,23 +193,23 @@ class ConnectionInformation: exe = become_settings.get('sudo_exe', C.DEFAULT_SUDO_EXE) flags = become_settings.get('sudo_flags', C.DEFAULT_SUDO_FLAGS) becomecmd = '%s -k && %s %s -S -p "%s" -u %s %s -c %s' % \ - (exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, self.become_user, executable, pipes.quote('echo %s; %s' % (success_key, cmd))) + (exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, self.become_user, executable, success_cmd) elif self.become_method == 'su': exe = become_settings.get('su_exe', C.DEFAULT_SU_EXE) flags = become_settings.get('su_flags', C.DEFAULT_SU_FLAGS) - becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, self.become_user, executable, pipes.quote('echo %s; %s' % (success_key, cmd))) + becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, self.become_user, executable, success_cmd) elif self.become_method == 'pbrun': exe = become_settings.get('pbrun_exe', 'pbrun') flags = become_settings.get('pbrun_flags', '') - becomecmd = '%s -b -l %s -u %s "%s"' % (exe, flags, user, pipes.quote('echo %s; %s' % (success_key, cmd))) + becomecmd = '%s -b -l %s -u %s "%s"' % (exe, flags, user, success_cmd) elif self.become_method == 'pfexec': exe = become_settings.get('pfexec_exe', 'pbrun') flags = become_settings.get('pfexec_flags', '') # No user as it uses it's own exec_attr to figure it out - becomecmd = '%s %s "%s"' % (exe, flags, pipes.quote('echo %s; %s' % (success_key, cmd))) + becomecmd = '%s %s "%s"' % (exe, flags, success_cmd) else: raise errors.AnsibleError("Privilege escalation method not found: %s" % method) diff --git a/v2/ansible/playbook/base.py b/v2/ansible/playbook/base.py index 949e6a09fd..e32da5d8c5 100644 --- a/v2/ansible/playbook/base.py +++ b/v2/ansible/playbook/base.py @@ -72,11 +72,20 @@ class Base: def munge(self, ds): ''' infrequently used method to do some pre-processing of legacy terms ''' - for base_class in self.__class__.__bases__: - method = getattr(self, ("_munge_%s" % base_class.__name__).lower(), None) - if method: - ds = method(ds) + def _get_base_classes_munge(target_class): + base_classes = list(target_class.__bases__[:]) + for base_class in target_class.__bases__: + base_classes.extend( _get_base_classes_munge(base_class)) + return base_classes + base_classes = list(self.__class__.__bases__[:]) + for base_class in self.__class__.__bases__: + base_classes.extend(_get_base_classes_munge(base_class)) + + for base_class in base_classes: + method = getattr(self, "_munge_%s" % base_class.__name__.lower(), None) + if method: + return method(ds) return ds def load_data(self, ds, variable_manager=None, loader=None): @@ -271,15 +280,21 @@ class Base: # optionally allowing masking by accessors if not needle.startswith("_"): - method = "get_%s" % needle - if method in self.__dict__: - return method(self) + method = "_get_attr_%s" % needle + if method in dir(self): + return getattr(self, method)() if needle in self._attributes: return self._attributes[needle] raise AttributeError("attribute not found in %s: %s" % (self.__class__.__name__, needle)) + def __setattr__(self, needle, value): + if hasattr(self, '_attributes') and needle in self._attributes: + self._attributes[needle] = value + else: + super(Base, self).__setattr__(needle, value) + def __getstate__(self): return self.serialize() diff --git a/v2/ansible/playbook/become.py b/v2/ansible/playbook/become.py index 0b0ad10176..67eb52b15e 100644 --- a/v2/ansible/playbook/become.py +++ b/v2/ansible/playbook/become.py @@ -95,3 +95,41 @@ class Become: ds['become_user'] = C.DEFAULT_BECOME_USER return ds + + def _get_attr_become(self): + ''' + Override for the 'become' getattr fetcher, used from Base. + ''' + if hasattr(self, '_get_parent_attribute'): + return self._get_parent_attribute('become') + else: + return self._attributes['become'] + + def _get_attr_become_method(self): + ''' + Override for the 'become_method' getattr fetcher, used from Base. + ''' + if hasattr(self, '_get_parent_attribute'): + return self._get_parent_attribute('become_method') + else: + return self._attributes['become_method'] + + def _get_attr_become_user(self): + ''' + Override for the 'become_user' getattr fetcher, used from Base. + ''' + if hasattr(self, '_get_parent_attribute'): + return self._get_parent_attribute('become_user') + else: + return self._attributes['become_user'] + + def _get_attr_become_password(self): + ''' + Override for the 'become_password' getattr fetcher, used from Base. + ''' + if hasattr(self, '_get_parent_attribute'): + return self._get_parent_attribute('become_password') + else: + return self._attributes['become_password'] + + diff --git a/v2/ansible/playbook/block.py b/v2/ansible/playbook/block.py index fa67b6ae1b..2946e83f5e 100644 --- a/v2/ansible/playbook/block.py +++ b/v2/ansible/playbook/block.py @@ -131,23 +131,24 @@ class Block(Base, Become, Conditional, Taggable): # use_handlers=self._use_handlers, # ) - def compile(self): - ''' - Returns the task list for this object - ''' - - task_list = [] - for task in self.block: - # FIXME: evaulate task tags/conditionals here - task_list.extend(task.compile()) - - return task_list - def copy(self): + def _dupe_task_list(task_list, new_block): + new_task_list = [] + for task in task_list: + new_task = task.copy(exclude_block=True) + new_task._block = new_block + new_task_list.append(new_task) + return new_task_list + new_me = super(Block, self).copy() new_me._use_handlers = self._use_handlers new_me._dep_chain = self._dep_chain[:] + 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) + print("new block tasks are: %s" % new_me.block) + new_me._parent_block = None if self._parent_block: new_me._parent_block = self._parent_block.copy() @@ -252,3 +253,24 @@ class Block(Base, Become, Conditional, Taggable): for dep in self._dep_chain: dep.set_loader(loader) + def _get_parent_attribute(self, attr): + ''' + Generic logic to get the attribute or parent attribute for a block value. + ''' + + value = self._attributes[attr] + if not value: + if self._parent_block: + value = getattr(self._block, attr) + elif self._role: + value = getattr(self._role, attr) + if not value and len(self._dep_chain): + reverse_dep_chain = self._dep_chain[:] + reverse_dep_chain.reverse() + for dep in reverse_dep_chain: + value = getattr(dep, attr) + if value: + break + + return value + diff --git a/v2/ansible/playbook/helpers.py b/v2/ansible/playbook/helpers.py index 0e14720557..3ea559d799 100644 --- a/v2/ansible/playbook/helpers.py +++ b/v2/ansible/playbook/helpers.py @@ -37,6 +37,7 @@ def load_list_of_blocks(ds, parent_block=None, role=None, task_include=None, use assert type(ds) in (list, NoneType) block_list = [] + print("in load list of blocks, ds is: %s" % ds) if ds: for block in ds: b = Block.load( @@ -50,6 +51,7 @@ def load_list_of_blocks(ds, parent_block=None, role=None, task_include=None, use ) block_list.append(b) + print("-> returning block list: %s" % block_list) return block_list diff --git a/v2/ansible/playbook/play.py b/v2/ansible/playbook/play.py index cbe4e03861..190189aa17 100644 --- a/v2/ansible/playbook/play.py +++ b/v2/ansible/playbook/play.py @@ -219,6 +219,7 @@ class Play(Base, Taggable, Become): block_list.extend(self.tasks) block_list.extend(self.post_tasks) + print("block list is: %s" % block_list) return block_list def get_vars(self): diff --git a/v2/ansible/playbook/role/__init__.py b/v2/ansible/playbook/role/__init__.py index dfb1f70add..21bcd21803 100644 --- a/v2/ansible/playbook/role/__init__.py +++ b/v2/ansible/playbook/role/__init__.py @@ -30,6 +30,7 @@ from ansible.errors import AnsibleError, AnsibleParserError from ansible.parsing import DataLoader from ansible.playbook.attribute import FieldAttribute from ansible.playbook.base import Base +from ansible.playbook.become import Become from ansible.playbook.conditional import Conditional from ansible.playbook.helpers import load_list_of_blocks, compile_block_list from ansible.playbook.role.include import RoleInclude @@ -69,7 +70,7 @@ def hash_params(params): ROLE_CACHE = dict() -class Role(Base, Conditional, Taggable): +class Role(Base, Become, Conditional, Taggable): def __init__(self): self._role_name = None @@ -136,6 +137,12 @@ class Role(Base, Conditional, Taggable): if parent_role: self.add_parent(parent_role) + # copy over all field attributes, except for when and tags, which + # are special cases and need to preserve pre-existing values + for (attr_name, _) in iteritems(self._get_base_attributes()): + if attr_name not in ('when', 'tags'): + setattr(self, attr_name, getattr(role_include, attr_name)) + current_when = getattr(self, 'when')[:] current_when.extend(role_include.when) setattr(self, 'when', current_when) @@ -144,10 +151,6 @@ class Role(Base, Conditional, Taggable): current_tags.extend(role_include.tags) setattr(self, 'tags', current_tags) - # save the current base directory for the loader and set it to the current role path - #cur_basedir = self._loader.get_basedir() - #self._loader.set_basedir(self._role_path) - # load the role's files, if they exist library = os.path.join(self._role_path, 'library') if os.path.isdir(library): @@ -179,9 +182,6 @@ class Role(Base, Conditional, Taggable): elif self._default_vars is None: self._default_vars = dict() - # and finally restore the previous base directory - #self._loader.set_basedir(cur_basedir) - def _load_role_yaml(self, subdir): file_path = os.path.join(self._role_path, subdir) if self._loader.path_exists(file_path) and self._loader.is_directory(file_path): @@ -313,9 +313,6 @@ class Role(Base, Conditional, Taggable): for dep in deps: dep_blocks = dep.compile(dep_chain=new_dep_chain) for dep_block in dep_blocks: - # since we're modifying the task, and need it to be unique, - # we make a copy of it here and assign the dependency chain - # to the copy, then append the copy to the task list. new_dep_block = dep_block.copy() new_dep_block._dep_chain = new_dep_chain block_list.append(new_dep_block) diff --git a/v2/ansible/playbook/role/definition.py b/v2/ansible/playbook/role/definition.py index d52c6795fb..bc1a0daacf 100644 --- a/v2/ansible/playbook/role/definition.py +++ b/v2/ansible/playbook/role/definition.py @@ -28,6 +28,7 @@ from ansible.errors import AnsibleError from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping from ansible.playbook.attribute import Attribute, FieldAttribute from ansible.playbook.base import Base +from ansible.playbook.become import Become from ansible.playbook.conditional import Conditional from ansible.playbook.taggable import Taggable from ansible.utils.path import unfrackpath @@ -36,7 +37,7 @@ from ansible.utils.path import unfrackpath __all__ = ['RoleDefinition'] -class RoleDefinition(Base, Conditional, Taggable): +class RoleDefinition(Base, Become, Conditional, Taggable): _role = FieldAttribute(isa='string') @@ -57,6 +58,9 @@ class RoleDefinition(Base, Conditional, Taggable): assert isinstance(ds, dict) or isinstance(ds, string_types) + if isinstance(ds, dict): + ds = super(RoleDefinition, self).munge(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 @@ -88,7 +92,7 @@ class RoleDefinition(Base, Conditional, Taggable): self._ds = ds # and return the cleaned-up data structure - return super(RoleDefinition, self).munge(new_ds) + return new_ds def _load_role_name(self, ds): ''' diff --git a/v2/ansible/playbook/task.py b/v2/ansible/playbook/task.py index 79ec2df340..ab66898242 100644 --- a/v2/ansible/playbook/task.py +++ b/v2/ansible/playbook/task.py @@ -210,20 +210,21 @@ class Task(Base, Conditional, Taggable, Become): del all_vars['when'] return all_vars - def compile(self): - ''' - For tasks, this is just a dummy method returning an array - with 'self' in it, so we don't have to care about task types - further up the chain. - ''' + # no longer used, as blocks are the lowest level of compilation now + #def compile(self): + # ''' + # For tasks, this is just a dummy method returning an array + # with 'self' in it, so we don't have to care about task types + # further up the chain. + # ''' + # + # return [self] - return [self] - - def copy(self): + def copy(self, exclude_block=False): new_me = super(Task, self).copy() new_me._block = None - if self._block: + if self._block and not exclude_block: new_me._block = self._block.copy() new_me._role = None @@ -309,3 +310,12 @@ class Task(Base, Conditional, Taggable, Become): if self._task_include: self._task_include.set_loader(loader) + def _get_parent_attribute(self, attr): + ''' + Generic logic to get the attribute or parent attribute for a task value. + ''' + value = self._attributes[attr] + if not value and self._block: + value = getattr(self._block, attr) + return value + diff --git a/v2/samples/roles/test_become_r1/meta/main.yml b/v2/samples/roles/test_become_r1/meta/main.yml new file mode 100644 index 0000000000..603a2d53a2 --- /dev/null +++ b/v2/samples/roles/test_become_r1/meta/main.yml @@ -0,0 +1 @@ +allow_duplicates: yes diff --git a/v2/samples/roles/test_become_r1/tasks/main.yml b/v2/samples/roles/test_become_r1/tasks/main.yml new file mode 100644 index 0000000000..9231d0af98 --- /dev/null +++ b/v2/samples/roles/test_become_r1/tasks/main.yml @@ -0,0 +1,2 @@ +- debug: msg="this is test_become_r1" +- command: whoami diff --git a/v2/samples/roles/test_become_r2/meta/main.yml b/v2/samples/roles/test_become_r2/meta/main.yml new file mode 100644 index 0000000000..9304df73a0 --- /dev/null +++ b/v2/samples/roles/test_become_r2/meta/main.yml @@ -0,0 +1,3 @@ +allow_duplicates: yes +dependencies: + - test_become_r1 diff --git a/v2/samples/roles/test_become_r2/tasks/main.yml b/v2/samples/roles/test_become_r2/tasks/main.yml new file mode 100644 index 0000000000..01d6d31385 --- /dev/null +++ b/v2/samples/roles/test_become_r2/tasks/main.yml @@ -0,0 +1,2 @@ +- debug: msg="this is test_become_r2" +- command: whoami diff --git a/v2/samples/test_become.yml b/v2/samples/test_become.yml index 4b02563ca7..eb527e5959 100644 --- a/v2/samples/test_become.yml +++ b/v2/samples/test_become.yml @@ -1,8 +1,14 @@ - hosts: all gather_facts: no + roles: + - { role: test_become_r2 } + - { role: test_become_r2, sudo_user: testing } tasks: + - command: whoami - command: whoami become_user: testing + - block: + - command: whoami - block: - command: whoami become_user: testing