예제 #1
0
    def _graph_task(self, task: Task, loop_counter: int, task_vars: Dict, graph: CustomDigraph, node_name_prefix: str,
                    color: str, parent_node_id: str, parent_node_name: str) -> bool:
        """
        Include the task in the graph.
        :return: True if the task has been included, false otherwise
        """

        self.display.vv(f"Adding {node_name_prefix.strip()}: '{task.get_name()}' to the graph")

        if not task.evaluate_tags(only_tags=self.tags, skip_tags=self.skip_tags, all_vars=task_vars):
            self.display.vv(f"The task '{task.get_name()}' is skipped due to the tags.")
            return False

        task_edge_label = str(loop_counter)
        if len(task.when) > 0:
            when = "".join(map(str, task.when))
            task_edge_label += "  [when: " + when + "]"

        task_name = clean_name(node_name_prefix + self.template(task.get_name(), task_vars))
        # get prefix id from node_name
        id_prefix = node_name_prefix.replace("[", "").replace("]", "").replace(" ", "_")
        task_id = id_prefix + generate_id()
        edge_id = "edge_" + generate_id()

        graph.node(task_id, label=task_name, shape="octagon", id=task_id, tooltip=task_name)
        graph.edge(parent_node_name, task_id, 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)

        return True
예제 #2
0
    def _add_task(self, task: Task, task_vars: Dict, node_type: str,
                  parent_node: CompositeNode) -> bool:
        """
        Include the task in the graph.
        :return: True if the task has been included, false otherwise
        """

        # Ansible-core 2.11 added an implicit meta tasks at the end of the role. So wee skip it here.
        if task.action == "meta" and task.implicit:
            return False

        if not task.evaluate_tags(only_tags=self.tags,
                                  skip_tags=self.skip_tags,
                                  all_vars=task_vars):
            display.vv(
                f"The task '{task.get_name()}' is skipped due to the tags.")
            return False

        display.vv(f"Adding {node_type} '{task.get_name()}' to the graph")

        task_name = clean_name(self.template(task.get_name(), task_vars))
        parent_node.add_node(
            target_composition=f"{node_type}s",
            node=TaskNode(
                task_name,
                generate_id(f"{node_type}_"),
                when=convert_when_to_str(task.when),
                raw_object=task,
                parent=parent_node,
            ),
        )

        return True
예제 #3
0
 def __init__(
     self, node_name: str, node_id: str = None, when: str = "", raw_object=None
 ):
     super().__init__(
         node_name,
         node_id or generate_id("playbook_"),
         when=when,
         raw_object=raw_object,
         supported_compositions=["plays"],
     )
예제 #4
0
 def __init__(
     self,
     node_name: str,
     node_id: str = None,
     when: str = "",
     raw_object=None,
     parent: "Node" = None,
 ):
     super().__init__(
         node_name,
         node_id or generate_id("block_"),
         when=when,
         raw_object=raw_object,
         parent=parent,
     )
예제 #5
0
    def __init__(
        self,
        node_name: str,
        node_id: str = None,
        when: str = "",
        raw_object=None,
        parent: "Node" = None,
    ):
        """

        :param node_name:
        :param node_id:
        :param raw_object:
        """
        super().__init__(
            node_name,
            node_id or generate_id("task_"),
            when=when,
            raw_object=raw_object,
            parent=parent,
        )
예제 #6
0
    def __init__(
        self,
        node_name: str,
        node_id: str = None,
        when: str = "",
        raw_object=None,
        parent: "Node" = None,
        include_role: bool = False,
    ):
        """

        :param node_name:
        :param node_id:
        :param raw_object:
        """
        self.include_role = include_role
        super().__init__(
            node_name,
            node_id or generate_id("role_"),
            when=when,
            raw_object=raw_object,
            parent=parent,
        )
예제 #7
0
 def __init__(
     self,
     node_name: str,
     node_id: str = None,
     when: str = "",
     raw_object=None,
     parent: "Node" = None,
     hosts: List[str] = None,
 ):
     """
     :param node_name:
     :param node_id:
     :param hosts: List of hosts attached to the play
     """
     super().__init__(
         node_name,
         node_id or generate_id("play_"),
         when=when,
         raw_object=raw_object,
         parent=parent,
         supported_compositions=["pre_tasks", "roles", "tasks", "post_tasks"],
     )
     self.hosts = hosts or []
예제 #8
0
    def make_graph(self, *args, **kwargs):
        """
        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:
        """

        # the root node
        self.graphiz_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(f"Loader basedir set to {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)
            play_id = "play_" + generate_id()

            self.display.banner("Graphing " + play_name)

            self.graph_representation.add_node(play_id)

            with self.graphiz_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_" + generate_id()
                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] ")

                # global_tasks_counter will hold the number of pre_tasks + tasks + and post_tasks
                global_tasks_counter = nb_pre_tasks

                self.display.v(f"{global_tasks_counter} pre_task(s) added to the graph.")
                # 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

                    # the role object doesn't inherit the tags from the play. So we add it manually.
                    role.tags = role.tags + play.tags
                    if not role.evaluate_tags(only_tags=self.tags, skip_tags=self.skip_tags,
                                              all_vars=play_vars):
                        self.display.vv(f"The role '{role.get_name()}' is skipped due to the tags.")
                        # Go to the next role
                        continue

                    role_number += 1
                    role_name = "[role] " + clean_name(role.get_name())

                    # edge from play to role
                    edge_id = "edge_" + generate_id()
                    play_subgraph.edge(play_name, role_name, label=str(role_number + global_tasks_counter), color=color,
                                       fontcolor=color, id=edge_id)
                    self.graph_representation.add_link(play_id, edge_id)

                    with self.graphiz_graph.subgraph(name=role_name, node_attr={}) as role_subgraph:

                        role_id = "role_" + generate_id()
                        role_subgraph.node(role_name, id=role_id)
                        self.graph_representation.add_link(edge_id, role_id)

                        # loop through the tasks of the roles
                        if self.include_role_tasks:
                            role_tasks_counter = 0  # the role tasks start a 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
                # end of roles loop
                self.display.v(f"{role_number} role(s) added to the graph")

                # loop through the tasks
                self.display.v("Graphing tasks...")
                for task_block in play.tasks:
                    global_tasks_counter = 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 + global_tasks_counter,
                                                                         play_vars=play_vars,
                                                                         node_name_prefix="[task] ")
                nb_tasks = global_tasks_counter - role_number - nb_pre_tasks
                self.display.v(f"{nb_tasks} task(s) added to the graph.")

                # loop through the post_tasks
                self.display.v("Graphing post_tasks...")
                for post_task_block in play.post_tasks:
                    global_tasks_counter = 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=global_tasks_counter,
                                                                         play_vars=play_vars,
                                                                         node_name_prefix="[post_task] ")
                nb_post_tasks = global_tasks_counter - nb_tasks - role_number - nb_pre_tasks
                self.display.v(f"{nb_post_tasks} post_task(s) added to the graph.")

            self.display.banner(f"Done graphing {play_name}")
            self.display.display("")  # just an empty line
예제 #9
0
    def _include_tasks_in_blocks(
        self,
        current_play: Play,
        parent_nodes: List[CompositeNode],
        block: Union[Block, TaskInclude],
        node_type: str,
        play_vars: Dict,
    ):
        """
        Recursively read all the tasks of the block and add it to the graph
        :param parent_nodes: This a list of parent nodes. Each time, we see an include_role, the corresponding node is
        added to this list
        :param current_play:
        :param block:
        :param play_vars:
        :param node_type:
        :return:
        """

        if not block._implicit and block._role is None:
            # Here we have an explicit block. Ansible internally converts all normal tasks to Block
            block_node = BlockNode(
                str(block.name),
                when=convert_when_to_str(block.when),
                raw_object=block,
                parent=parent_nodes[-1],
            )
            parent_nodes[-1].add_node(f"{node_type}s", block_node)
            parent_nodes.append(block_node)

        # loop through the tasks
        for task_or_block in block.block:

            if hasattr(task_or_block, "loop") and task_or_block.loop:
                display.warning(
                    "Looping on tasks or roles are not supported for the moment. "
                    f"Only the task having the loop argument will be added to the graph."
                )

            if isinstance(task_or_block, Block):
                self._include_tasks_in_blocks(
                    current_play=current_play,
                    parent_nodes=parent_nodes,
                    block=task_or_block,
                    node_type=node_type,
                    play_vars=play_vars,
                )
            elif isinstance(
                    task_or_block, TaskInclude
            ):  # include, include_tasks, include_role are dynamic
                # So we need to process them explicitly because Ansible does it during the execution of the playbook

                task_vars = self.variable_manager.get_vars(play=current_play,
                                                           task=task_or_block)

                if isinstance(task_or_block, IncludeRole):
                    # 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
                    display.v(
                        f"An 'include_role' found: '{task_or_block.get_name()}'"
                    )

                    # Here we are using the role name instead of the task name to keep the same behavior  as a
                    #  traditional role
                    if self.group_roles_by_name:
                        # If we are grouping roles, we use the hash of role name as the node id
                        role_node_id = "role_" + hash_value(
                            task_or_block._role_name)
                    else:
                        # Otherwise, a random id is used
                        role_node_id = generate_id("role_")
                    role_node = RoleNode(
                        task_or_block._role_name,
                        node_id=role_node_id,
                        when=convert_when_to_str(task_or_block.when),
                        raw_object=task_or_block,
                        parent=parent_nodes[-1],
                        include_role=True,
                    )
                    parent_nodes[-1].add_node(
                        f"{node_type}s",
                        role_node,
                    )

                    if task_or_block.loop:  # Looping on include_role is not supported
                        continue  # Go the next task
                    else:
                        if self.include_role_tasks:
                            # If we have an include_role, and we want to include its tasks, the parent node now becomes
                            # the role.
                            parent_nodes.append(role_node)

                        block_list, _ = task_or_block.get_block_list(
                            play=current_play,
                            loader=self.data_loader,
                            variable_manager=self.variable_manager,
                        )
                else:
                    display.v(
                        f"An 'include_tasks' found. Including tasks from '{task_or_block.get_name()}'"
                    )

                    templar = Templar(loader=self.data_loader,
                                      variables=task_vars)
                    try:
                        included_file_path = 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
                        display.warning(
                            f"Unable to translate the include task '{task_or_block.get_name()}' due to an undefined variable: {str(e)}. "
                            "Some variables are available only during the execution of the playbook."
                        )
                        self._add_task(
                            task=task_or_block,
                            task_vars=task_vars,
                            node_type=node_type,
                            parent_node=parent_nodes[-1],
                        )
                        continue

                    data = self.data_loader.load_from_file(included_file_path)
                    if data is None:
                        display.warning(
                            f"The file '{included_file_path}' is empty and has no tasks to include"
                        )
                        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
                    block_list = 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 (
                        block_list
                ):  # loop through the blocks inside the included tasks or role
                    self._include_tasks_in_blocks(
                        current_play=current_play,
                        parent_nodes=parent_nodes,
                        block=b,
                        play_vars=task_vars,
                        node_type=node_type,
                    )
                if (self.include_role_tasks
                        and isinstance(task_or_block, IncludeRole)
                        and len(parent_nodes) > 1):
                    # We remove the parent node we have added if we included some tasks from a role
                    parent_nodes.pop()
            else:  # It's here that we add the task in the graph
                if (len(parent_nodes) > 1  # 1
                        and not has_role_parent(task_or_block)  # 2
                        and parent_nodes[-1].raw_object !=
                        task_or_block._parent  # 3
                    ):
                    # We remove a parent node :
                    # 1. When have at least two parents. Every node (except the playbook) should have a parent node
                    #   AND
                    # 2. The current node doesn't have a role as parent
                    #   AND
                    # 3. The last parent node is different from the current node parent. This means that we are
                    #   done with the child nodes of this parent node
                    parent_nodes.pop()

                # check if this task comes from a role, and we don't want to include tasks of the role
                if has_role_parent(
                        task_or_block) and not self.include_role_tasks:
                    # skip role's task
                    display.vv(
                        f"The task '{task_or_block.get_name()}' has a role as parent and include_role_tasks is false. "
                        "It will be skipped.")
                    # skipping
                    continue

                self._add_task(
                    task=task_or_block,
                    task_vars=play_vars,
                    node_type=node_type,
                    parent_node=parent_nodes[-1],
                )
예제 #10
0
    def parse(self, *args, **kwargs) -> PlaybookNode:
        """
        Loop through the playbook and generate 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:
            add pre_tasks
            add roles
                if  include_role_tasks
                    add role_tasks
            add tasks
            add post_tasks
        :return:
        """

        playbook = Playbook.load(
            self.playbook_filename,
            loader=self.data_loader,
            variable_manager=self.variable_manager,
        )
        # the root node
        playbook_root_node = PlaybookNode(self.playbook_filename,
                                          raw_object=playbook)
        # loop through the plays
        for play in playbook.get_plays():

            # 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(playbook._basedir)
            display.vvv(
                f"Loader basedir set to {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 = f"Play: {clean_name(play.get_name())} ({len(play_hosts)})"
            play_name = self.template(play_name, play_vars)

            display.v(f"Parsing {play_name}")

            play_node = PlayNode(play_name, hosts=play_hosts, raw_object=play)
            playbook_root_node.add_node("plays", play_node)

            # loop through the pre_tasks
            display.v("Parsing pre_tasks...")
            for pre_task_block in play.pre_tasks:
                self._include_tasks_in_blocks(
                    current_play=play,
                    parent_nodes=[play_node],
                    block=pre_task_block,
                    play_vars=play_vars,
                    node_type="pre_task",
                )

            # loop through the roles
            display.v("Parsing roles...")

            for role in play.get_roles():
                # Don't insert tasks from ``import/include_role``, preventing duplicate graphing
                if role.from_include:
                    continue

                # the role object doesn't inherit the tags from the play. So we add it manually.
                role.tags = role.tags + play.tags
                if not role.evaluate_tags(only_tags=self.tags,
                                          skip_tags=self.skip_tags,
                                          all_vars=play_vars):
                    display.vv(
                        f"The role '{role.get_name()}' is skipped due to the tags."
                    )
                    # Go to the next role
                    continue

                if self.group_roles_by_name:
                    # If we are grouping roles, we use the hash of role name as the node id
                    role_node_id = "role_" + hash_value(role.get_name())
                else:
                    # Otherwise, a random id is used
                    role_node_id = generate_id("role_")
                role_node = RoleNode(
                    clean_name(role.get_name()),
                    node_id=role_node_id,
                    raw_object=role,
                    parent=play_node,
                )
                # edge from play to role
                play_node.add_node("roles", role_node)

                if self.include_role_tasks:
                    # loop through the tasks of the roles
                    for block in role.compile(play):
                        self._include_tasks_in_blocks(
                            current_play=play,
                            parent_nodes=[role_node],
                            block=block,
                            play_vars=play_vars,
                            node_type="task",
                        )
                    # end of roles loop

            # loop through the tasks
            display.v("Parsing tasks...")
            for task_block in play.tasks:
                self._include_tasks_in_blocks(
                    current_play=play,
                    parent_nodes=[play_node],
                    block=task_block,
                    play_vars=play_vars,
                    node_type="task",
                )

            # loop through the post_tasks
            display.v("Parsing post_tasks...")
            for post_task_block in play.post_tasks:
                self._include_tasks_in_blocks(
                    current_play=play,
                    parent_nodes=[play_node],
                    block=post_task_block,
                    play_vars=play_vars,
                    node_type="post_task",
                )
            # Summary
            display.v(
                f"{len(play_node.pre_tasks)} pre_task(s) added to the graph.")
            display.v(f"{len(play_node.roles)} role(s) added to the play")
            display.v(f"{len(play_node.tasks)} task(s) added to the play")
            display.v(
                f"{len(play_node.post_tasks)} post_task(s) added to the play")
            # moving to the next play

        return playbook_root_node