def test_post_processor_with_graph_representation(post_processor, tmpdir): """ Test the post processor a graph representation :param post_processor: :param tmpdir: :return: """ graph_represention = GraphRepresentation() svg_post_proccessed_path = tmpdir.join("simple_playbook_postproccess_graph.svg") play_id = "play_hostsall" # link from play to task edges graph_represention.add_link(play_id, "play_hostsallpost_taskPosttask1") graph_represention.add_link(play_id, "play_hostsallpost_taskPosttask2") post_processor.post_process(graph_represention) post_processor.write(output_filename=svg_post_proccessed_path.strpath) assert svg_post_proccessed_path.check(file=1) root = etree.parse(svg_post_proccessed_path.strpath).getroot() _assert_common_svg(root) assert len(root.xpath("ns:g/*[@id='%s']//ns:link" % play_id, namespaces={'ns': SVG_NAMESPACE})) == 2
def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, graph=None, output_filename=None): """ :param data_loader: :param inventory_manager: :param variable_manager: :param playbook_filename: :param graph: :param output_filename: The output filename without the extension """ self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.output_filename = output_filename self.graph_representation = GraphRepresentation() self.playbook = self.playbook = Playbook.load( self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg")
def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, options, graph=None): """ Main grapher responsible to parse the playbook and draw graph :param data_loader: :type data_loader: ansible.parsing.dataloader.DataLoader :param inventory_manager: :type inventory_manager: ansible.inventory.manager.InventoryManager :param variable_manager: :type variable_manager: ansible.vars.manager.VariableManager :param options Command line options :type options: optparse.Values :param playbook_filename: :type playbook_filename: str :param graph: :type graph: Digraph """ self.options = options self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.options.output_filename = self.options.output_filename self.rendered_file_path = None self.display = Display(verbosity=options.verbosity) if self.options.tags is None: self.options.tags = ["all"] if self.options.skip_tags is None: self.options.skip_tags = [] self.graph_representation = GraphRepresentation() self.playbook = Playbook.load(self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg")
def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, graph=None, output_filename=None): """ Main grapher responsible to parse the playbook and draw graph :param data_loader: :type data_loader: ansible.parsing.dataloader.DataLoader :param inventory_manager: :type inventory_manager: ansible.inventory.manager.InventoryManager :param variable_manager: :type variable_manager: ansible.vars.manager.VariableManager :param playbook_filename: :type playbook_filename: str :param graph: :type graph: Digraph :param output_filename: :type output_filename: str """ self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.output_filename = output_filename self.graph_representation = GraphRepresentation() self.playbook = Playbook.load(self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) # need playbook basedir. It's used to get tasks included with `include_tasks` # Ansible currently resets it to the CWD when the parsing is done self.data_loader.set_basedir(self.playbook._basedir) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg")
def __init__(self, data_loader: DataLoader, inventory_manager: InventoryManager, variable_manager: VariableManager, playbook_filename: str, display: Display, include_role_tasks=False, tags=None, skip_tags=None): """ Main grapher responsible to parse the playbook and draw graph :param data_loader: :param inventory_manager: :param variable_manager: :param include_role_tasks: If true, the tasks of the role will be included. :param playbook_filename: """ self.include_role_tasks = include_role_tasks self.playbook_filename = playbook_filename self.playbook = Playbook.load(playbook_filename, loader=data_loader, variable_manager=variable_manager) graphiz_graph = CustomDigraph(edge_attr=DEFAULT_EDGE_ATTR, graph_attr=DEFAULT_GRAPH_ATTR, format="svg", name=playbook_filename) super().__init__(data_loader=data_loader, inventory_manager=inventory_manager, graphiz_graph=graphiz_graph, variable_manager=variable_manager, graph_representation=GraphRepresentation(), tags=tags, skip_tags=skip_tags, display=display)
class Grapher(object): """ Main class to make the graph """ DEFAULT_GRAPH_ATTR = { 'ratio': "fill", "rankdir": "LR", 'concentrate': 'true', 'ordering': 'in' } DEFAULT_EDGE_ATTR = {'sep': "10", "esep": "5"} def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, graph=None, output_filename=None): """ Main grapher responsible to parse the playbook and draw graph :param data_loader: :type data_loader: ansible.parsing.dataloader.DataLoader :param inventory_manager: :type inventory_manager: ansible.inventory.manager.InventoryManager :param variable_manager: :type variable_manager: ansible.vars.manager.VariableManager :param playbook_filename: :type playbook_filename: str :param graph: :type graph: Digraph :param output_filename: :type output_filename: str """ self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.output_filename = output_filename self.graph_representation = GraphRepresentation() self.playbook = Playbook.load(self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) # need playbook basedir. It's used to get tasks included with `include_tasks` # Ansible currently resets it to the CWD when the parsing is done self.data_loader.set_basedir(self.playbook._basedir) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg") def template(self, data, variables, fail_on_undefined=False): """ Template the data using Jinja. Return data if an error occurs during the templating :param fail_on_undefined: :type fail_on_undefined: bool :param data: :type data: Union[str, ansible.parsing.yaml.objects.AnsibleUnicode] :param variables: :type variables: dict :return: """ try: templar = Templar(loader=self.data_loader, variables=variables) return templar.template(data, fail_on_undefined=fail_on_undefined) except AnsibleError as ansible_error: # Sometime we need to export if fail_on_undefined: raise display.warning(ansible_error) return data def make_graph(self, include_role_tasks=False, tags=None, skip_tags=None): """ Loop through the playbook and make the graph. The graph is drawn following this order (https://docs.ansible.com/ansible/2.4/playbooks_reuse_roles.html#using-roles) for each play: draw pre_tasks draw roles if include_role_tasks draw role_tasks draw tasks draw post_tasks :param include_role_tasks: :type include_role_tasks: bool :param tags: :type tags: list :param skip_tags: :type skip_tags: list :return: :rtype: """ if tags is None: tags = ['all'] if skip_tags is None: skip_tags = [] # the root node self.graph.node(self.playbook_filename, style="dotted", id="root_node") # loop through the plays for play_counter, play in enumerate(self.playbook.get_plays(), 1): play_vars = self.variable_manager.get_vars(play) # get only the hosts name for the moment play_hosts = [ h.get_name() for h in self.inventory_manager.get_hosts( self.template(play.hosts, play_vars)) ] nb_hosts = len(play_hosts) color, play_font_color = get_play_colors(play) play_name = "{} ({})".format(clean_name(str(play)), nb_hosts) play_name = self.template(play_name, play_vars) play_id = "play_" + clean_id(play_name) self.graph_representation.add_node(play_id) with self.graph.subgraph(name=play_name) as play_subgraph: # play node play_subgraph.node(play_name, id=play_id, style='filled', shape="box", color=color, fontcolor=play_font_color, tooltip=" ".join(play_hosts)) # edge from root node to plays play_edge_id = "edge_" + clean_id(self.playbook_filename + play_name + str(play_counter)) play_subgraph.edge(self.playbook_filename, play_name, id=play_edge_id, style="bold", label=str(play_counter), color=color, fontcolor=color) # loop through the pre_tasks nb_pre_tasks = 0 for pre_task_block in play.pre_tasks: nb_pre_tasks = self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=pre_task_block, color=color, current_counter=nb_pre_tasks, play_vars=play_vars, node_name_prefix='[pre_task] ', tags=tags, skip_tags=skip_tags) # loop through the roles for role_counter, role in enumerate(play.get_roles(), 1): role_name = '[role] ' + clean_name(str(role)) # the role object doesn't inherit the tags from the play. So we add it manually role.tags = role.tags + play.tags role_not_tagged = '' if not role.evaluate_tags(only_tags=tags, skip_tags=skip_tags, all_vars=play_vars): role_not_tagged = NOT_TAGGED with self.graph.subgraph(name=role_name, node_attr={}) as role_subgraph: current_counter = role_counter + nb_pre_tasks role_id = "role_" + clean_id(role_name + role_not_tagged) role_subgraph.node(role_name, id=role_id) when = "".join(role.when) play_to_node_label = str(current_counter) if len( when) == 0 else str( current_counter) + " [when: " + when + "]" edge_id = "edge_" + clean_id(play_id + role_id + role_not_tagged) role_subgraph.edge(play_name, role_name, label=play_to_node_label, color=color, fontcolor=color, id=edge_id) self.graph_representation.add_link(play_id, edge_id) self.graph_representation.add_link(edge_id, role_id) # loop through the tasks of the roles if include_role_tasks: role_tasks_counter = 0 for block in role.get_task_blocks(): role_tasks_counter = self._include_tasks_in_blocks( current_play=play, graph=role_subgraph, parent_node_name=role_name, parent_node_id=role_id, block=block, color=color, play_vars=play_vars, current_counter=role_tasks_counter, node_name_prefix='[task] ', tags=tags, skip_tags=skip_tags) role_tasks_counter += 1 nb_roles = len(play.get_roles()) # loop through the tasks nb_tasks = 0 for task_block in play.tasks: nb_tasks = self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=task_block, color=color, current_counter=nb_roles + nb_pre_tasks, play_vars=play_vars, node_name_prefix='[task] ', tags=tags, skip_tags=skip_tags) # loop through the post_tasks for post_task_block in play.post_tasks: self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=post_task_block, color=color, current_counter=nb_tasks, play_vars=play_vars, node_name_prefix='[post_task] ', tags=tags, skip_tags=skip_tags) def render_graph(self, output_filename=None, save_dot_file=False): """ Render the graph :param output_filename: :type output_filename: str :param save_dot_file: :type save_dot_file: bool :return: :rtype: """ if output_filename is None: output_filename = self.output_filename self.graph.render(cleanup=not save_dot_file, filename=output_filename) def post_process_svg(self, output_filename=None): """ Post process the rendered svg :param output_filename: The output filename without the extension :type output_filename: str :return: """ if output_filename is None: output_filename = self.output_filename + ".svg" post_processor = PostProcessor(svg_path=output_filename) post_processor.post_process( graph_representation=self.graph_representation) post_processor.write() return output_filename def _include_tasks_in_blocks(self, current_play, graph, parent_node_name, parent_node_id, block, color, current_counter, play_vars=None, node_name_prefix='', tags=None, skip_tags=None): """ Recursively read all the tasks of the block and add it to the graph :param current_play: :type current_play: ansible.playbook.play.Play :param graph: :type graph: :param parent_node_name: :type parent_node_name: str :param parent_node_id: :type parent_node_id: str :param block: :type block: Union[Block,TaskInclude] :param color: :type color: str :param current_counter: :type current_counter: int :param play_vars: :type play_vars: dict :param node_name_prefix: :type node_name_prefix: str :param tags: :type tags: list :param skip_tags: :type skip_tags: list :return: :rtype: """ if tags is None: tags = ['all'] if skip_tags is None: skip_tags = [] # get prefix id from node_name id_prefix = node_name_prefix.replace("[", "").replace("]", "").replace(" ", "_") loop_counter = current_counter # loop through the tasks for counter, task_or_block in enumerate(block.block, 1): if isinstance(task_or_block, Block): loop_counter = self._include_tasks_in_blocks( current_play=current_play, graph=graph, parent_node_name=parent_node_name, parent_node_id=parent_node_id, block=task_or_block, color=color, current_counter=loop_counter, play_vars=play_vars, node_name_prefix=node_name_prefix, tags=tags, skip_tags=skip_tags) elif isinstance(task_or_block, TaskInclude): # here we have an `include_tasks` which is dynamic. So we need to process it explicitly because Ansible # does it during th execution of the playbook include_target = self.template( task_or_block.args['_raw_params'], play_vars, fail_on_undefined=True) include_file = self.data_loader.path_dwim(include_target) data = self.data_loader.load_from_file(include_file) if data is None: display.warning( "file %s is empty and had no tasks to include" % include_file) continue elif not isinstance(data, list): raise AnsibleParserError( "included task files must contain a list of tasks", obj=data) # get the blocks from the include_tasks blocks = load_list_of_blocks( data, play=current_play, variable_manager=self.variable_manager) for b in blocks: # loop through the blocks inside the included tasks loop_counter = self._include_tasks_in_blocks( current_play=current_play, graph=graph, parent_node_name=parent_node_name, parent_node_id=parent_node_id, block=b, color=color, current_counter=loop_counter, play_vars=play_vars, node_name_prefix=node_name_prefix, tags=tags, skip_tags=skip_tags) else: # check if the task should be included tagged = '' if not task_or_block.evaluate_tags(only_tags=tags, skip_tags=skip_tags, all_vars=play_vars): tagged = NOT_TAGGED task_name = clean_name( node_name_prefix + self.template(task_or_block.get_name(), play_vars)) task_id = id_prefix + clean_id(task_name + tagged) graph.node(task_name, shape="octagon", id=task_id) edge_id = "edge_" + parent_node_id + task_id + str( loop_counter) + tagged graph.edge(parent_node_name, task_name, label=str(loop_counter + 1), color=color, fontcolor=color, style="bold", id=edge_id) self.graph_representation.add_link(parent_node_id, edge_id) self.graph_representation.add_link(edge_id, task_id) loop_counter += 1 return loop_counter
class Grapher(object): """ Main class to make the graph """ DEFAULT_GRAPH_ATTR = { "ratio": "fill", "rankdir": "LR", "concentrate": "true", "ordering": "in" } DEFAULT_EDGE_ATTR = {"sep": "10", "esep": "5"} def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, options, graph=None): """ Main grapher responsible to parse the playbook and draw graph :param data_loader: :type data_loader: ansible.parsing.dataloader.DataLoader :param inventory_manager: :type inventory_manager: ansible.inventory.manager.InventoryManager :param variable_manager: :type variable_manager: ansible.vars.manager.VariableManager :param options Command line options :type options: optparse.Values :param playbook_filename: :type playbook_filename: str :param graph: :type graph: Digraph """ self.options = options self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.options.output_filename = self.options.output_filename self.rendered_file_path = None self.display = Display(verbosity=options.verbosity) if self.options.tags is None: self.options.tags = ["all"] if self.options.skip_tags is None: self.options.skip_tags = [] self.graph_representation = GraphRepresentation() self.playbook = Playbook.load(self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg") def template(self, data, variables, fail_on_undefined=False): """ Template the data using Jinja. Return data if an error occurs during the templating :param fail_on_undefined: :type fail_on_undefined: bool :param data: :type data: Union[str, ansible.parsing.yaml.objects.AnsibleUnicode] :param variables: :type variables: dict :return: """ try: templar = Templar(loader=self.data_loader, variables=variables) return templar.template(data, fail_on_undefined=fail_on_undefined) except AnsibleError as ansible_error: # Sometime we need to export if fail_on_undefined: raise self.display.warning(ansible_error) return data def make_graph(self): """ Loop through the playbook and make the graph. The graph is drawn following this order (https://docs.ansible.com/ansible/2.4/playbooks_reuse_roles.html#using-roles) for each play: draw pre_tasks draw roles if include_role_tasks draw role_tasks draw tasks draw post_tasks :return: :rtype: """ # the root node self.graph.node(self.playbook_filename, style="dotted", id="root_node") # loop through the plays for play_counter, play in enumerate(self.playbook.get_plays(), 1): # the load basedir is relative to the playbook path if play._included_path is not None: self.data_loader.set_basedir(play._included_path) else: self.data_loader.set_basedir(self.playbook._basedir) self.display.vvv("Loader basedir set to {}".format( self.data_loader.get_basedir())) play_vars = self.variable_manager.get_vars(play) play_hosts = [ h.get_name() for h in self.inventory_manager.get_hosts( self.template(play.hosts, play_vars)) ] play_name = "Play #{}: {} ({})".format(play_counter, clean_name(play.get_name()), len(play_hosts)) play_name = self.template(play_name, play_vars) self.display.banner("Graphing " + play_name) play_id = "play_" + str(uuid.uuid4()) self.graph_representation.add_node(play_id) with self.graph.subgraph(name=play_name) as play_subgraph: color, play_font_color = get_play_colors(play) # play node play_subgraph.node(play_name, id=play_id, style="filled", shape="box", color=color, fontcolor=play_font_color, tooltip=" ".join(play_hosts)) # edge from root node to plays play_edge_id = "edge_" + str(uuid.uuid4()) play_subgraph.edge(self.playbook_filename, play_name, id=play_edge_id, style="bold", label=str(play_counter), color=color, fontcolor=color) # loop through the pre_tasks self.display.v("Graphing pre_tasks...") nb_pre_tasks = 0 for pre_task_block in play.pre_tasks: nb_pre_tasks = self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=pre_task_block, color=color, current_counter=nb_pre_tasks, play_vars=play_vars, node_name_prefix="[pre_task] ") # loop through the roles self.display.v("Graphing roles...") role_number = 0 for role in play.get_roles(): # Don't insert tasks from ``import/include_role``, preventing # duplicate graphing if role.from_include: continue role_number += 1 role_name = "[role] " + clean_name(role.get_name()) # the role object doesn't inherit the tags from the play. So we add it manually role.tags = role.tags + play.tags role_not_tagged = "" if not role.evaluate_tags(only_tags=self.options.tags, skip_tags=self.options.skip_tags, all_vars=play_vars): role_not_tagged = NOT_TAGGED with self.graph.subgraph(name=role_name, node_attr={}) as role_subgraph: current_counter = role_number + nb_pre_tasks role_id = "role_" + str(uuid.uuid4()) + role_not_tagged role_subgraph.node(role_name, id=role_id) edge_id = "edge_" + str(uuid.uuid4()) + role_not_tagged # edge from play to role role_subgraph.edge(play_name, role_name, label=str(current_counter), color=color, fontcolor=color, id=edge_id) self.graph_representation.add_link(play_id, edge_id) self.graph_representation.add_link(edge_id, role_id) # loop through the tasks of the roles if self.options.include_role_tasks: role_tasks_counter = 0 for block in role.compile(play): role_tasks_counter = self._include_tasks_in_blocks( current_play=play, graph=role_subgraph, parent_node_name=role_name, parent_node_id=role_id, block=block, color=color, play_vars=play_vars, current_counter=role_tasks_counter, node_name_prefix="[task] ") role_tasks_counter += 1 self.display.v( "{} roles added to the graph".format(role_number)) # loop through the tasks self.display.v("Graphing tasks...") nb_tasks = 0 for task_block in play.tasks: nb_tasks = self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=task_block, color=color, current_counter=role_number + nb_pre_tasks, play_vars=play_vars, node_name_prefix="[task] ") # loop through the post_tasks self.display.v("Graphing post_tasks...") for post_task_block in play.post_tasks: self._include_tasks_in_blocks( current_play=play, graph=play_subgraph, parent_node_name=play_name, parent_node_id=play_id, block=post_task_block, color=color, current_counter=nb_tasks, play_vars=play_vars, node_name_prefix="[post_task] ") self.display.banner("Done graphing {}".format(play_name)) self.display.display("") # just an empty line # moving to the next play def render_graph(self): """ Render the graph :return: The rendered file path :rtype: str """ self.rendered_file_path = self.graph.render( cleanup=not self.options.save_dot_file, filename=self.options.output_filename) if self.options.save_dot_file: # add .gv extension. The render doesn't add an extension final_name = self.options.output_filename + ".dot" os.rename(self.options.output_filename, final_name) self.display.display( "Graphviz dot file has been exported to {}".format(final_name)) return self.rendered_file_path def post_process_svg(self): """ Post process the rendered svg :return The post processed file path :rtype: str :return: """ post_processor = PostProcessor(svg_path=self.rendered_file_path) post_processor.post_process( graph_representation=self.graph_representation) post_processor.write() self.display.display("The graph has been exported to {}".format( self.rendered_file_path)) return self.rendered_file_path def _include_tasks_in_blocks(self, current_play, graph, parent_node_name, parent_node_id, block, color, current_counter, play_vars=None, node_name_prefix=""): """ Recursively read all the tasks of the block and add it to the graph FIXME: This function needs some refactoring. Thinking of a BlockGrapher to handle this :param current_play: :type current_play: ansible.playbook.play.Play :param graph: :type graph: :param parent_node_name: :type parent_node_name: str :param parent_node_id: :type parent_node_id: str :param block: :type block: Union[Block,TaskInclude] :param color: :type color: str :param current_counter: :type current_counter: int :param play_vars: :type play_vars: dict :param node_name_prefix: :type node_name_prefix: str :return: :rtype: """ loop_counter = current_counter # loop through the tasks for counter, task_or_block in enumerate(block.block, 1): if isinstance(task_or_block, Block): loop_counter = self._include_tasks_in_blocks( current_play=current_play, graph=graph, parent_node_name=parent_node_name, parent_node_id=parent_node_id, block=task_or_block, color=color, current_counter=loop_counter, play_vars=play_vars, node_name_prefix=node_name_prefix) elif isinstance( task_or_block, TaskInclude ): # include, include_tasks, include_role are dynamic # So we need to process it explicitly because Ansible does it during th execution of the playbook task_vars = self.variable_manager.get_vars(play=current_play, task=task_or_block) if isinstance(task_or_block, IncludeRole): self.display.v( "An 'include_role' found. Including tasks from '{}'". format(task_or_block.args["name"])) # here we have an include_role. The class IncludeRole is a subclass of TaskInclude. # We do this because the management of an include_role is different. # See :func:`~ansible.playbook.included_file.IncludedFile.process_include_results` from line 155 my_blocks, _ = task_or_block.get_block_list( play=current_play, loader=self.data_loader, variable_manager=self.variable_manager) else: self.display.v( "An 'include_tasks' found. Including tasks from '{}'". format(task_or_block.get_name())) templar = Templar(loader=self.data_loader, variables=task_vars) try: include_file = handle_include_path( original_task=task_or_block, loader=self.data_loader, templar=templar) except AnsibleUndefinedVariable as e: # TODO: mark this task with some special shape or color self.display.warning( "Unable to translate the include task '{}' due to an undefined variable: {}. " "Some variables are available only during the real execution." .format(task_or_block.get_name(), str(e))) loop_counter += 1 self._include_task(task_or_block, loop_counter, task_vars, graph, node_name_prefix, color, parent_node_id, parent_node_name) continue data = self.data_loader.load_from_file(include_file) if data is None: self.display.warning( "file %s is empty and had no tasks to include" % include_file) continue elif not isinstance(data, list): raise AnsibleParserError( "included task files must contain a list of tasks", obj=data) # get the blocks from the include_tasks my_blocks = load_list_of_blocks( data, play=current_play, variable_manager=self.variable_manager, role=task_or_block._role, loader=self.data_loader, parent_block=task_or_block) for b in my_blocks: # loop through the blocks inside the included tasks or role loop_counter = self._include_tasks_in_blocks( current_play=current_play, graph=graph, parent_node_name=parent_node_name, parent_node_id=parent_node_id, block=b, color=color, current_counter=loop_counter, play_vars=task_vars, node_name_prefix=node_name_prefix) else: # check if this task comes from a role and we dont want to include role's task if has_role_parent( task_or_block) and not self.options.include_role_tasks: # skip role's task self.display.vv( "The task '{}' has a role as parent and include_role_tasks is false. " "It will be skipped.".format(task_or_block.get_name())) continue self._include_task(task_or_block=task_or_block, loop_counter=loop_counter + 1, play_vars=play_vars, graph=graph, node_name_prefix=node_name_prefix, color=color, parent_node_id=parent_node_id, parent_node_name=parent_node_name) loop_counter += 1 return loop_counter def _include_task(self, task_or_block, loop_counter, play_vars, graph, node_name_prefix, color, parent_node_id, parent_node_name): """ Include the task in the graph :return: :rtype: """ self.display.vv("Adding the task '{}' to the graph".format( task_or_block.get_name())) # check if the task should be included tagged = '' if not task_or_block.evaluate_tags(only_tags=self.options.tags, skip_tags=self.options.skip_tags, all_vars=play_vars): self.display.vv( "The task '{}' should not be executed. It will be marked as NOT_TAGGED" .format(task_or_block.get_name())) tagged = NOT_TAGGED task_edge_label = str(loop_counter) if len(task_or_block.when) > 0: when = "".join(map(str, task_or_block.when)) task_edge_label += " [when: " + when + "]" task_name = clean_name( node_name_prefix + self.template(task_or_block.get_name(), play_vars)) # get prefix id from node_name id_prefix = node_name_prefix.replace("[", "").replace("]", "").replace(" ", "_") task_id = id_prefix + str(uuid.uuid4()) + tagged edge_id = "edge_" + str(uuid.uuid4()) + tagged graph.node(task_name, shape="octagon", id=task_id) graph.edge(parent_node_name, task_name, label=task_edge_label, color=color, fontcolor=color, style="bold", id=edge_id) self.graph_representation.add_link(parent_node_id, edge_id) self.graph_representation.add_link(edge_id, task_id)
class Grapher(object): """ Main class to make the graph """ DEFAULT_GRAPH_ATTR = { 'ratio': "fill", "rankdir": "LR", 'concentrate': 'true', 'ordering': 'in' } DEFAULT_EDGE_ATTR = {'sep': "10", "esep": "5"} def __init__(self, data_loader, inventory_manager, variable_manager, playbook_filename, graph=None, output_filename=None): """ :param data_loader: :param inventory_manager: :param variable_manager: :param playbook_filename: :param graph: :param output_filename: The output filename without the extension """ self.variable_manager = variable_manager self.inventory_manager = inventory_manager self.data_loader = data_loader self.playbook_filename = playbook_filename self.output_filename = output_filename self.graph_representation = GraphRepresentation() self.playbook = self.playbook = Playbook.load( self.playbook_filename, loader=self.data_loader, variable_manager=self.variable_manager) if graph is None: self.graph = CustomDigrah(edge_attr=self.DEFAULT_EDGE_ATTR, graph_attr=self.DEFAULT_GRAPH_ATTR, format="svg") def template(self, data, variables): """ Template the data using Jinja. Return data if an error occurs during the templating :param data: :param variables: :return: """ try: templar = Templar(loader=self.data_loader, variables=variables) return templar.template(data, fail_on_undefined=False) except AnsibleError as ansible_error: display.warning(ansible_error) return data def _colors_for_play(self, play): """ Return two colors (in hex) for a given play: the main color and the color to use as a font color :return: """ # TODO: Check the if the picked color is (almost) white. We can't see a white edge on the graph picked_color = Color(pick_for=play) play_font_color = "#000000" if picked_color.get_luminance( ) > 0.6 else "#ffffff" return picked_color.get_hex_l(), play_font_color def make_graph(self, include_role_tasks=False, tags=None, skip_tags=None): """ Loop through the playbook and make the graph. The graph is drawn following this order (https://docs.ansible.com/ansible/2.4/playbooks_reuse_roles.html#using-roles) for each play: draw pre_tasks draw roles if include_role_tasks draw role_tasks draw tasks draw post_tasks :return: """ if tags is None: tags = ['all'] if skip_tags is None: skip_tags = [] # the root node self.graph.node(self.playbook_filename, style="dotted") # loop through the plays for play_counter, play in enumerate(self.playbook.get_plays(), 1): play_vars = self.variable_manager.get_vars(play) # get only the hosts name for the moment play_hosts = [ h.get_name() for h in self.inventory_manager.get_hosts( self.template(play.hosts, play_vars)) ] nb_hosts = len(play_hosts) color, play_font_color = self._colors_for_play(play) play_name = "{} ({})".format(clean_name(str(play)), nb_hosts) play_name = self.template(play_name, play_vars) play_id = clean_id("play_" + play_name) self.graph_representation.add_node(play_id) with self.graph.subgraph(name=play_name) as play_subgraph: # play node play_subgraph.node(play_name, id=play_id, style='filled', shape="box", color=color, fontcolor=play_font_color, tooltip=" ".join(play_hosts)) # edge from root node to plays play_edge_id = clean_id(self.playbook_filename + play_name + str(play_counter)) play_subgraph.edge(self.playbook_filename, play_name, id=play_edge_id, style="bold", label=str(play_counter), color=color, fontcolor=color) # loop through the pre_tasks nb_pre_tasks = 0 for pre_task_block in play.pre_tasks: nb_pre_tasks = self._include_tasks_in_blocks( play_subgraph, play_name, play_id, pre_task_block, color, nb_pre_tasks, play_vars, '[pre_task] ', tags, skip_tags) # loop through the roles for role_counter, role in enumerate(play.get_roles(), 1): role_name = '[role] ' + clean_name(str(role)) # the role object doesn't inherit the tags from the play. So we add it manually role.tags = role.tags + play.tags role_not_tagged = '' if not role.evaluate_tags(only_tags=tags, skip_tags=skip_tags, all_vars=play_vars): role_not_tagged = NOT_TAGGED with self.graph.subgraph(name=role_name, node_attr={}) as role_subgraph: current_counter = role_counter + nb_pre_tasks role_id = clean_id("role_" + role_name + role_not_tagged) role_subgraph.node(role_name, id=role_id) when = "".join(role.when) play_to_node_label = str(current_counter) if len( when) == 0 else str( current_counter) + " [when: " + when + "]" edge_id = clean_id("edge_" + play_id + role_id + role_not_tagged) role_subgraph.edge(play_name, role_name, label=play_to_node_label, color=color, fontcolor=color, id=edge_id) self.graph_representation.add_link(play_id, edge_id) self.graph_representation.add_link(edge_id, role_id) # loop through the tasks of the roles if include_role_tasks: role_tasks_counter = 0 for block in role.get_task_blocks(): role_tasks_counter = self._include_tasks_in_blocks( role_subgraph, role_name, role_id, block, color, role_tasks_counter, play_vars, '[task] ', tags, skip_tags) role_tasks_counter += 1 nb_roles = len(play.get_roles()) # loop through the tasks nb_tasks = 0 for task_block in play.tasks: nb_tasks = self._include_tasks_in_blocks( play_subgraph, play_name, play_id, task_block, color, nb_roles + nb_pre_tasks, play_vars, '[task] ', tags, skip_tags) # loop through the post_tasks for post_task_block in play.post_tasks: self._include_tasks_in_blocks(play_subgraph, play_name, play_id, post_task_block, color, nb_tasks, play_vars, '[post_task] ', tags, skip_tags) def render_graph(self, output_filename=None, save_dot_file=False): """ Render the graph :param output_filename: :param save_dot_file: :return: """ if output_filename is None: output_filename = self.output_filename self.graph.render(cleanup=not save_dot_file, filename=output_filename) def post_process_svg(self, output_filename=None): """ Post process the rendered svg :param output_filename: The output filename without the extension :return: """ if output_filename is None: output_filename = self.output_filename + ".svg" post_processor = PostProcessor(svg_path=output_filename) post_processor.post_process( graph_representation=self.graph_representation) post_processor.write() def _include_tasks_in_blocks(self, graph, parent_node_name, parent_node_id, block, color, current_counter, variables=None, node_name_prefix='', tags=None, skip_tags=None): """ Recursively read all the tasks of the block and add it to the graph :param variables: :param tags: :param parent_node_id: :param graph_representation: :param node_name_prefix: :param color: :param current_counter: :param graph: :param parent_node_name: :param block: :return: """ if tags is None: tags = ['all'] if skip_tags is None: skip_tags = [] loop_counter = current_counter # loop through the tasks for counter, task_or_block in enumerate(block.block, 1): if isinstance(task_or_block, Block): loop_counter = self._include_tasks_in_blocks( graph, parent_node_name, parent_node_id, task_or_block, color, loop_counter, variables, node_name_prefix, tags, skip_tags) else: # check if the task should be included tagged = '' if not task_or_block.evaluate_tags(only_tags=tags, skip_tags=skip_tags, all_vars=variables): tagged = NOT_TAGGED task_name = clean_name( node_name_prefix + self.template(task_or_block.get_name(), variables)) task_id = clean_id(task_name + tagged) graph.node(task_name, shape="octagon", id=task_id) edge_id = "edge_" + parent_node_id + task_id + str( loop_counter) + tagged graph.edge(parent_node_name, task_name, label=str(loop_counter + 1), color=color, fontcolor=color, style="bold", id=edge_id) self.graph_representation.add_link(parent_node_id, edge_id) self.graph_representation.add_link(edge_id, task_id) loop_counter += 1 return loop_counter