From 9a401e73a697bf6dd45e3cad75c11c5884fb4894 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Thu, 15 Aug 2013 00:29:22 -0500 Subject: [PATCH] Adding support for role dependencies. Fixes #3686 Dependencies are enabled by adding a new directory/file named meta/main.yml to the role. The format of the dependencies are: dependencies: - { role: foo, x: 1, y: 2 } - { role: bar, x: 3, y: 4 } ... Dependencies inherit variables as they are seen at the time of the dependency inclusion. For example, if foo(x=1, y=2) has a dependency on bar(x=3,z=4), then bar will have variables (x=3,y=2,z=4). Different roles can have dependencies on the same role, and this variable inheritence allows for the reuse of generic roles quite easily. For example: Role 'car' has the following dependencies: dependencies: - { role: wheel, n: 1 } - { role: wheel, n: 2 } - { role: wheel, n: 3 } - { role: wheel, n: 4 } Role 'wheel' has the following dependencies: dependencies: - { role: tire } - { role: brake } The role 'car' is then used as follows: - { role: car, type: honda } And tasks/main.yml in each role simply contains the following: - name: {{ type }} whatever {{ n }} command: echo '' TASK: [honda tire 1] TASK: [honda brake 1] TASK: [honda wheel 1] TASK: [honda tire 2] TASK: [honda brake 2] TASK: [honda wheel 2] TASK: [honda tire 3] TASK: [honda brake 3] TASK: [honda wheel 3] TASK: [honda tire 4] TASK: [honda brake 4] TASK: [honda wheel 4] TASK: [I'm a honda] <- (this is in roles/car/tasks/main.yml) --- lib/ansible/playbook/play.py | 101 ++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index dc1c72fbb0c..e3258dc5841 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -119,9 +119,64 @@ class Play(object): # ************************************************* - def _load_roles(self, roles, ds): - + def _get_role_path(self, role): + """ + Returns the path on disk to the directory containing + the role directories like tasks, templates, etc. Also + returns any variables that were included with the role + """ + orig_path = template(self.basedir,role,self.vars) + + role_vars = {} + if type(orig_path) == dict: + # what, not a path? + role_name = orig_path.get('role', None) + if role_name is None: + raise errors.AnsibleError("expected a role name in dictionary: %s" % orig_path) + role_vars = orig_path + orig_path = role_name + + path = utils.path_dwim(self.basedir, os.path.join('roles', orig_path)) + if not os.path.isdir(path) and not orig_path.startswith(".") and not orig_path.startswith("/"): + path2 = utils.path_dwim(self.basedir, orig_path) + if not os.path.isdir(path2): + raise errors.AnsibleError("cannot find role in %s or %s" % (path, path2)) + path = path2 + elif not os.path.isdir(path): + raise errors.AnsibleError("cannot find role in %s" % (path)) + + return (path, role_vars) + + def _build_role_dependencies(self, roles, dep_stack, vars={}, level=0): + # this number is arbitrary, but it seems sane + if level > 20: + raise errors.AnsibleError("too many levels of recursion while resolving role dependencies") + for role in roles: + path,role_vars = self._get_role_path(role) + # the meta directory contains the yaml that should + # hold the list of dependencies (if any) + meta = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(path, 'meta'))) + if os.path.isfile(meta): + data = utils.parse_yaml_from_file(meta) + if data: + dependencies = data.get('dependencies',[]) + for dep in dependencies: + (dep_path,dep_vars) = self._get_role_path(dep) + dep_vars.update(role_vars) + for k in vars.keys(): + if not k in dep_vars: + dep_vars[k] = vars[k] + if 'role' in dep_vars: + del dep_vars['role'] + self._build_role_dependencies([dep], dep_stack, vars=dep_vars, level=level+1) + dep_stack.append([dep,dep_vars]) + # only add the current role when we're at the top level, + # otherwise we'll end up in a recursive loop + if level == 0: + dep_stack.append([role,role_vars]) + return dep_stack + def _load_roles(self, roles, ds): # a role is a name that auto-includes the following if they exist # /tasks/main.yml # /handlers/main.yml @@ -147,52 +202,37 @@ class Play(object): # flush handlers after pre_tasks new_tasks.append(dict(meta='flush_handlers')) - # variables if the role was parameterized (i.e. given as a hash) - has_dict = {} + roles = self._build_role_dependencies(roles, [], self.vars) - for role_path in roles: - orig_path = template(self.basedir,role_path,self.vars) - - if type(orig_path) == dict: - # what, not a path? - role_name = orig_path.get('role', None) - if role_name is None: - raise errors.AnsibleError("expected a role name in dictionary: %s" % orig_path) - has_dict = orig_path - orig_path = role_name + for role,role_vars in roles: + path,ignore = self._get_role_path(role) # special vars must be extracted from the dict to the included tasks special_keys = [ "sudo", "sudo_user", "when", "with_items" ] special_vars = {} for k in special_keys: - if k in has_dict: - special_vars[k] = has_dict[k] - - path = utils.path_dwim(self.basedir, os.path.join('roles', orig_path)) - if not os.path.isdir(path) and not orig_path.startswith(".") and not orig_path.startswith("/"): - path2 = utils.path_dwim(self.basedir, orig_path) - if not os.path.isdir(path2): - raise errors.AnsibleError("cannot find role in %s or %s" % (path, path2)) - path = path2 - elif not os.path.isdir(path): - raise errors.AnsibleError("cannot find role in %s" % (path)) + if k in role_vars: + special_vars[k] = role_vars[k] + task_basepath = utils.path_dwim(self.basedir, os.path.join(path, 'tasks')) handler_basepath = utils.path_dwim(self.basedir, os.path.join(path, 'handlers')) vars_basepath = utils.path_dwim(self.basedir, os.path.join(path, 'vars')) + task = self._resolve_main(task_basepath) handler = self._resolve_main(handler_basepath) vars_file = self._resolve_main(vars_basepath) library = utils.path_dwim(self.basedir, os.path.join(path, 'library')) + if not os.path.isfile(task) and not os.path.isfile(handler) and not os.path.isfile(vars_file) and not os.path.isdir(library): raise errors.AnsibleError("found role at %s, but cannot find %s or %s or %s or %s" % (path, task, handler, vars_file, library)) if os.path.isfile(task): - nt = dict(include=pipes.quote(task), vars=has_dict) + nt = dict(include=pipes.quote(task), vars=role_vars) for k in special_keys: if k in special_vars: nt[k] = special_vars[k] new_tasks.append(nt) if os.path.isfile(handler): - nt = dict(include=pipes.quote(handler), vars=has_dict) + nt = dict(include=pipes.quote(handler), vars=role_vars) for k in special_keys: if k in special_vars: nt[k] = special_vars[k] @@ -202,10 +242,9 @@ class Play(object): if os.path.isdir(library): utils.plugins.module_finder.add_directory(library) - tasks = ds.get('tasks', None) + tasks = ds.get('tasks', None) post_tasks = ds.get('post_tasks', None) - - handlers = ds.get('handlers', None) + handlers = ds.get('handlers', None) vars_files = ds.get('vars_files', None) if type(tasks) != list: @@ -223,8 +262,10 @@ class Play(object): new_tasks.extend(post_tasks) # flush handlers after post tasks new_tasks.append(dict(meta='flush_handlers')) + new_handlers.extend(handlers) new_vars_files.extend(vars_files) + ds['tasks'] = new_tasks ds['handlers'] = new_handlers ds['vars_files'] = new_vars_files