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
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)
Esempio n. 3
0
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
Esempio n. 4
0
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