mirror of
https://github.com/haidaraM/ansible-playbook-grapher
synced 2024-11-10 06:04:15 +00:00
Initial support for mermaidjs (#144)
Co-authored-by: haidaraM <haidaraM@users.noreply.github.com>
This commit is contained in:
parent
bc318c5814
commit
389e6ffda3
24 changed files with 1049 additions and 136 deletions
9
.github/workflows/testing.yaml
vendored
9
.github/workflows/testing.yaml
vendored
|
@ -17,6 +17,7 @@ jobs:
|
|||
name: Tests Py${{ matrix.python-version }} - Ansible ${{ matrix.ansible-version }}
|
||||
env:
|
||||
SVG_FILES_PATH: tests/generated-svgs
|
||||
MERMAID_FILES_PATH: tests/generated-mermaids
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -75,12 +76,8 @@ jobs:
|
|||
# branch: generated-svgs-${{ github.head_ref || github.ref }}-${{ env.MATRIX_JOB_IDENTIFIER }}
|
||||
# create_branch: true
|
||||
#
|
||||
# - name: Publish job summary
|
||||
# env:
|
||||
# SVG_COMMIT_SHA: ${{ steps.commit-svg-files.outputs.commit_hash }}
|
||||
# run: |
|
||||
# env
|
||||
# python tests/generate-job-summary.py >> $GITHUB_STEP_SUMMARY
|
||||
- name: Publish job summary
|
||||
run: python tests/generate-job-summary.py >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Test installation in virtualenv
|
||||
run: make test_install ANSIBLE_VERSION=${{ matrix.ansible-version }}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -109,4 +109,5 @@ venv.bak/
|
|||
.vagrant
|
||||
Vagrantfile
|
||||
generated-svgs
|
||||
generated-mermaids
|
||||
**/.DS_Store
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,3 +1,24 @@
|
|||
# 2.0.0 (unreleased)
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Add support for MermaidJS. See https://github.com/haidaraM/ansible-playbook-grapher/issues/137
|
||||
- Update various Dependencies: pytest, pytest-cov, ansible-core, pyquery etc...
|
||||
- ci: Add dependabot for github-actions
|
||||
- Rename some tests files
|
||||
- ...
|
||||
|
||||
## Breaking changes
|
||||
|
||||
This version contains the following breaking changes. Some of them may likely affect you if you were using the grapher
|
||||
as a library inside another project:
|
||||
|
||||
- Completely refactor the rendering part of the part by making it more extensible in order to support Mermaid.
|
||||
- Fill the plays, blocks and node with color to make them more visible in the output
|
||||
- Rename the file `graph.py` to `graph_model.py`
|
||||
- Use the concatenation of the playbook names as the output filename when graphing multiple playbooks instead of the
|
||||
first playbook.
|
||||
|
||||
# 1.2.0 (2022-08-21)
|
||||
|
||||
## What's Changed
|
||||
|
|
4
Makefile
4
Makefile
|
@ -23,10 +23,10 @@ test_install: build
|
|||
@./tests/test_install.sh $(VIRTUALENV_DIR) $(ANSIBLE_VERSION)
|
||||
|
||||
test:
|
||||
cd tests && pytest
|
||||
cd tests && pytest test_cli.py test_utils.py test_parser.py test_graph_model.py test_graphviz_postprocessor.py test_graphviz_renderer.py test_mermaid_renderer.py
|
||||
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
rm -rf ansible_playbook_grapher.egg-info build dist $(VIRTUALENV_DIR) tests/htmlcov tests/.pytest_cache .eggs tests/generated-svgs tests/.coverage
|
||||
rm -rf ansible_playbook_grapher.egg-info build dist $(VIRTUALENV_DIR) tests/htmlcov tests/.pytest_cache .eggs tests/generated-* tests/.coverage
|
||||
|
||||
.PHONY: clean test_install
|
341
README.md
341
README.md
|
@ -39,7 +39,7 @@ JavaScript:
|
|||
since `ansible` [package has been split](https://www.ansible.com/blog/ansible-3.0.0-qa)).
|
||||
- **Graphviz**: The tool used to generate the graph in SVG.
|
||||
```shell script
|
||||
$ sudo apt-get install graphviz # or yum install or brew install
|
||||
sudo apt-get install graphviz # or yum install or brew install
|
||||
```
|
||||
|
||||
I try to respect [Red Hat Ansible Engine Life Cycle](https://access.redhat.com/support/policy/updates/ansible-engine)
|
||||
|
@ -51,6 +51,19 @@ for the supported Ansible version.
|
|||
pip install ansible-playbook-grapher
|
||||
```
|
||||
|
||||
### Renderers
|
||||
|
||||
At the time of writing, two renderers are supported:
|
||||
|
||||
1. `graphviz` (default): Generate the graph in SVG. It has more features and is more tested: open protocol,
|
||||
highlight linked nodes...
|
||||
2. `mermaid-flowchart`: Generate the graph in [Mermaid](https://mermaid.js.org/syntax/flowchart.html) format. You can
|
||||
directly embed the graph in your markdown and GitHub (
|
||||
and [other integrations](https://mermaid.js.org/ecosystem/integrations.html)) will render it. **Early support**.
|
||||
|
||||
If you are interested to support more renderers, feel free to create an issue or raise a PR based on the existing
|
||||
renderers.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
|
@ -71,6 +84,305 @@ ansible-playbook-grapher tests/fixtures/with_block.yml
|
|||
|
||||
![Example](https://raw.githubusercontent.com/haidaraM/ansible-playbook-grapher/main/img/block.png)
|
||||
|
||||
```bash
|
||||
ansible-playbook-grapher --include-role-tasks --renderer mermaid-flowchart tests/fixtures/multi-plays.yml tests/fixtures/with_block.yml
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Ansible Playbook Grapher
|
||||
---
|
||||
%%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%%
|
||||
flowchart LR
|
||||
%% Start of the playbook 'tests/fixtures/multi-plays.yml'
|
||||
playbook_bd8798bc("tests/fixtures/multi-plays.yml")
|
||||
%% Start of the play 'Play: all (0)'
|
||||
play_fb11c1f8["Play: all (0)"]
|
||||
style play_fb11c1f8 fill:#4d8b41,color:#ffffff
|
||||
playbook_bd8798bc --> |"1"| play_fb11c1f8
|
||||
linkStyle 0 stroke:#4d8b41,color:#4d8b41
|
||||
pre_task_0aea2a6b["[pre_task] Pretask"]
|
||||
style pre_task_0aea2a6b stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"1"| pre_task_0aea2a6b
|
||||
linkStyle 1 stroke:#4d8b41,color:#4d8b41
|
||||
pre_task_e48d82b5["[pre_task] Pretask 2"]
|
||||
style pre_task_e48d82b5 stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"2"| pre_task_e48d82b5
|
||||
linkStyle 2 stroke:#4d8b41,color:#4d8b41
|
||||
%% Start of the role 'fake_role'
|
||||
role_d95e9d5f("[role] fake_role")
|
||||
style role_d95e9d5f fill:#4d8b41,color:#ffffff,stroke:#4d8b41
|
||||
play_fb11c1f8 --> |"3"| role_d95e9d5f
|
||||
linkStyle 3 stroke:#4d8b41,color:#4d8b41
|
||||
task_41f6dd12["fake_role : Debug 1"]
|
||||
style task_41f6dd12 stroke:#4d8b41,fill:#ffffff
|
||||
role_d95e9d5f --> |"1 [when: ansible_distribution == 'Debian']"| task_41f6dd12
|
||||
linkStyle 4 stroke:#4d8b41,color:#4d8b41
|
||||
task_9dcf29d3["fake_role : Debug 2"]
|
||||
style task_9dcf29d3 stroke:#4d8b41,fill:#ffffff
|
||||
role_d95e9d5f --> |"2 [when: ansible_distribution == 'Debian']"| task_9dcf29d3
|
||||
linkStyle 5 stroke:#4d8b41,color:#4d8b41
|
||||
task_dc3f4611["fake_role : Debug 3 with double quote "here" in the name"]
|
||||
style task_dc3f4611 stroke:#4d8b41,fill:#ffffff
|
||||
role_d95e9d5f --> |"3 [when: ansible_distribution == 'Debian']"| task_dc3f4611
|
||||
linkStyle 6 stroke:#4d8b41,color:#4d8b41
|
||||
%% End of the role 'fake_role'
|
||||
%% Start of the role 'display_some_facts'
|
||||
role_2b74c9f5("[role] display_some_facts")
|
||||
style role_2b74c9f5 fill:#4d8b41,color:#ffffff,stroke:#4d8b41
|
||||
play_fb11c1f8 --> |"4"| role_2b74c9f5
|
||||
linkStyle 7 stroke:#4d8b41,color:#4d8b41
|
||||
task_1c0691fb["display_some_facts : ansible_architecture"]
|
||||
style task_1c0691fb stroke:#4d8b41,fill:#ffffff
|
||||
role_2b74c9f5 --> |"1"| task_1c0691fb
|
||||
linkStyle 8 stroke:#4d8b41,color:#4d8b41
|
||||
task_c7d6574e["display_some_facts : ansible_date_time"]
|
||||
style task_c7d6574e stroke:#4d8b41,fill:#ffffff
|
||||
role_2b74c9f5 --> |"2"| task_c7d6574e
|
||||
linkStyle 9 stroke:#4d8b41,color:#4d8b41
|
||||
task_e26456cf["display_some_facts : Specific included task for Debian"]
|
||||
style task_e26456cf stroke:#4d8b41,fill:#ffffff
|
||||
role_2b74c9f5 --> |"3"| task_e26456cf
|
||||
linkStyle 10 stroke:#4d8b41,color:#4d8b41
|
||||
%% End of the role 'display_some_facts'
|
||||
task_f0ec5674["[task] Add backport {{backport}}"]
|
||||
style task_f0ec5674 stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"5"| task_f0ec5674
|
||||
linkStyle 11 stroke:#4d8b41,color:#4d8b41
|
||||
task_614fa7f3["[task] Install packages"]
|
||||
style task_614fa7f3 stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"6"| task_614fa7f3
|
||||
linkStyle 12 stroke:#4d8b41,color:#4d8b41
|
||||
post_task_bfd4a733["[post_task] Posttask"]
|
||||
style post_task_bfd4a733 stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"7"| post_task_bfd4a733
|
||||
linkStyle 13 stroke:#4d8b41,color:#4d8b41
|
||||
post_task_6728f20f["[post_task] Posttask 2"]
|
||||
style post_task_6728f20f stroke:#4d8b41,fill:#ffffff
|
||||
play_fb11c1f8 --> |"8"| post_task_6728f20f
|
||||
linkStyle 14 stroke:#4d8b41,color:#4d8b41
|
||||
%% End of the play 'Play: all (0)'
|
||||
%% Start of the play 'Play: database (0)'
|
||||
play_200a2cda["Play: database (0)"]
|
||||
style play_200a2cda fill:#195db3,color:#ffffff
|
||||
playbook_bd8798bc --> |"2"| play_200a2cda
|
||||
linkStyle 15 stroke:#195db3,color:#195db3
|
||||
%% Start of the role 'fake_role'
|
||||
role_584c099b("[role] fake_role")
|
||||
style role_584c099b fill:#195db3,color:#ffffff,stroke:#195db3
|
||||
play_200a2cda --> |"1"| role_584c099b
|
||||
linkStyle 16 stroke:#195db3,color:#195db3
|
||||
task_4363c0b7["fake_role : Debug 1"]
|
||||
style task_4363c0b7 stroke:#195db3,fill:#ffffff
|
||||
role_584c099b --> |"1 [when: ansible_distribution == 'Debian']"| task_4363c0b7
|
||||
linkStyle 17 stroke:#195db3,color:#195db3
|
||||
task_aff132aa["fake_role : Debug 2"]
|
||||
style task_aff132aa stroke:#195db3,fill:#ffffff
|
||||
role_584c099b --> |"2 [when: ansible_distribution == 'Debian']"| task_aff132aa
|
||||
linkStyle 18 stroke:#195db3,color:#195db3
|
||||
task_69120ebe["fake_role : Debug 3 with double quote "here" in the name"]
|
||||
style task_69120ebe stroke:#195db3,fill:#ffffff
|
||||
role_584c099b --> |"3 [when: ansible_distribution == 'Debian']"| task_69120ebe
|
||||
linkStyle 19 stroke:#195db3,color:#195db3
|
||||
%% End of the role 'fake_role'
|
||||
%% Start of the role 'display_some_facts'
|
||||
role_f46ed679("[role] display_some_facts")
|
||||
style role_f46ed679 fill:#195db3,color:#ffffff,stroke:#195db3
|
||||
play_200a2cda --> |"2"| role_f46ed679
|
||||
linkStyle 20 stroke:#195db3,color:#195db3
|
||||
task_5f4d00e6["display_some_facts : ansible_architecture"]
|
||||
style task_5f4d00e6 stroke:#195db3,fill:#ffffff
|
||||
role_f46ed679 --> |"1"| task_5f4d00e6
|
||||
linkStyle 21 stroke:#195db3,color:#195db3
|
||||
task_fa40d63b["display_some_facts : ansible_date_time"]
|
||||
style task_fa40d63b stroke:#195db3,fill:#ffffff
|
||||
role_f46ed679 --> |"2"| task_fa40d63b
|
||||
linkStyle 22 stroke:#195db3,color:#195db3
|
||||
task_3527696f["display_some_facts : Specific included task for Debian"]
|
||||
style task_3527696f stroke:#195db3,fill:#ffffff
|
||||
role_f46ed679 --> |"3"| task_3527696f
|
||||
linkStyle 23 stroke:#195db3,color:#195db3
|
||||
%% End of the role 'display_some_facts'
|
||||
%% End of the play 'Play: database (0)'
|
||||
%% Start of the play 'Play: webserver (0)'
|
||||
play_34799e65["Play: webserver (0)"]
|
||||
style play_34799e65 fill:#7f4d73,color:#ffffff
|
||||
playbook_bd8798bc --> |"3"| play_34799e65
|
||||
linkStyle 24 stroke:#7f4d73,color:#7f4d73
|
||||
%% Start of the role 'nested_include_role'
|
||||
role_1bfe7836("[role] nested_include_role")
|
||||
style role_1bfe7836 fill:#7f4d73,color:#ffffff,stroke:#7f4d73
|
||||
play_34799e65 --> |"1"| role_1bfe7836
|
||||
linkStyle 25 stroke:#7f4d73,color:#7f4d73
|
||||
task_40d7c36f["nested_include_role : Ensure postgresql is at the latest version"]
|
||||
style task_40d7c36f stroke:#7f4d73,fill:#ffffff
|
||||
role_1bfe7836 --> |"1"| task_40d7c36f
|
||||
linkStyle 26 stroke:#7f4d73,color:#7f4d73
|
||||
task_bc9f916c["nested_include_role : Ensure that postgresql is started"]
|
||||
style task_bc9f916c stroke:#7f4d73,fill:#ffffff
|
||||
role_1bfe7836 --> |"2"| task_bc9f916c
|
||||
linkStyle 27 stroke:#7f4d73,color:#7f4d73
|
||||
%% Start of the role 'display_some_facts'
|
||||
role_dfff70cd("[role] display_some_facts")
|
||||
style role_dfff70cd fill:#7f4d73,color:#ffffff,stroke:#7f4d73
|
||||
role_1bfe7836 --> |"3 [when: x is not defined]"| role_dfff70cd
|
||||
linkStyle 28 stroke:#7f4d73,color:#7f4d73
|
||||
task_2700f9a8["display_some_facts : ansible_architecture"]
|
||||
style task_2700f9a8 stroke:#7f4d73,fill:#ffffff
|
||||
role_dfff70cd --> |"1"| task_2700f9a8
|
||||
linkStyle 29 stroke:#7f4d73,color:#7f4d73
|
||||
task_84bd5d7e["display_some_facts : ansible_date_time"]
|
||||
style task_84bd5d7e stroke:#7f4d73,fill:#ffffff
|
||||
role_dfff70cd --> |"2"| task_84bd5d7e
|
||||
linkStyle 30 stroke:#7f4d73,color:#7f4d73
|
||||
task_1b02d165["display_some_facts : Specific included task for Debian"]
|
||||
style task_1b02d165 stroke:#7f4d73,fill:#ffffff
|
||||
role_dfff70cd --> |"3"| task_1b02d165
|
||||
linkStyle 31 stroke:#7f4d73,color:#7f4d73
|
||||
%% End of the role 'display_some_facts'
|
||||
%% Start of the role 'fake_role'
|
||||
role_6433854b("[role] fake_role")
|
||||
style role_6433854b fill:#7f4d73,color:#ffffff,stroke:#7f4d73
|
||||
role_1bfe7836 --> |"4"| role_6433854b
|
||||
linkStyle 32 stroke:#7f4d73,color:#7f4d73
|
||||
task_10479304["fake_role : Debug 1"]
|
||||
style task_10479304 stroke:#7f4d73,fill:#ffffff
|
||||
role_6433854b --> |"1"| task_10479304
|
||||
linkStyle 33 stroke:#7f4d73,color:#7f4d73
|
||||
task_a13ab280["fake_role : Debug 2"]
|
||||
style task_a13ab280 stroke:#7f4d73,fill:#ffffff
|
||||
role_6433854b --> |"2"| task_a13ab280
|
||||
linkStyle 34 stroke:#7f4d73,color:#7f4d73
|
||||
task_bffa1ed0["fake_role : Debug 3 with double quote "here" in the name"]
|
||||
style task_bffa1ed0 stroke:#7f4d73,fill:#ffffff
|
||||
role_6433854b --> |"3"| task_bffa1ed0
|
||||
linkStyle 35 stroke:#7f4d73,color:#7f4d73
|
||||
%% End of the role 'fake_role'
|
||||
%% End of the role 'nested_include_role'
|
||||
%% Start of the role 'display_some_facts'
|
||||
role_9188508e("[role] display_some_facts")
|
||||
style role_9188508e fill:#7f4d73,color:#ffffff,stroke:#7f4d73
|
||||
play_34799e65 --> |"2"| role_9188508e
|
||||
linkStyle 36 stroke:#7f4d73,color:#7f4d73
|
||||
task_b0401984["display_some_facts : ansible_architecture"]
|
||||
style task_b0401984 stroke:#7f4d73,fill:#ffffff
|
||||
role_9188508e --> |"1"| task_b0401984
|
||||
linkStyle 37 stroke:#7f4d73,color:#7f4d73
|
||||
task_417dd6b4["display_some_facts : ansible_date_time"]
|
||||
style task_417dd6b4 stroke:#7f4d73,fill:#ffffff
|
||||
role_9188508e --> |"2"| task_417dd6b4
|
||||
linkStyle 38 stroke:#7f4d73,color:#7f4d73
|
||||
task_60860d86["display_some_facts : Specific included task for Debian"]
|
||||
style task_60860d86 stroke:#7f4d73,fill:#ffffff
|
||||
role_9188508e --> |"3"| task_60860d86
|
||||
linkStyle 39 stroke:#7f4d73,color:#7f4d73
|
||||
%% End of the role 'display_some_facts'
|
||||
%% End of the play 'Play: webserver (0)'
|
||||
%% End of the playbook 'tests/fixtures/multi-plays.yml'
|
||||
|
||||
%% Start of the playbook 'tests/fixtures/with_block.yml'
|
||||
playbook_2e263466("tests/fixtures/with_block.yml")
|
||||
%% Start of the play 'Play: all (0)'
|
||||
play_74990123["Play: all (0)"]
|
||||
style play_74990123 fill:#a4288c,color:#ffffff
|
||||
playbook_2e263466 --> |"1"| play_74990123
|
||||
linkStyle 40 stroke:#a4288c,color:#a4288c
|
||||
%% Start of the role 'fake_role'
|
||||
role_5914bd45("[role] fake_role")
|
||||
style role_5914bd45 fill:#a4288c,color:#ffffff,stroke:#a4288c
|
||||
play_74990123 --> |"1"| role_5914bd45
|
||||
linkStyle 41 stroke:#a4288c,color:#a4288c
|
||||
pre_task_87f2b15f["fake_role : Debug 1"]
|
||||
style pre_task_87f2b15f stroke:#a4288c,fill:#ffffff
|
||||
role_5914bd45 --> |"1"| pre_task_87f2b15f
|
||||
linkStyle 42 stroke:#a4288c,color:#a4288c
|
||||
pre_task_64224a5c["fake_role : Debug 2"]
|
||||
style pre_task_64224a5c stroke:#a4288c,fill:#ffffff
|
||||
role_5914bd45 --> |"2"| pre_task_64224a5c
|
||||
linkStyle 43 stroke:#a4288c,color:#a4288c
|
||||
pre_task_eaf098d3["fake_role : Debug 3 with double quote "here" in the name"]
|
||||
style pre_task_eaf098d3 stroke:#a4288c,fill:#ffffff
|
||||
role_5914bd45 --> |"3"| pre_task_eaf098d3
|
||||
linkStyle 44 stroke:#a4288c,color:#a4288c
|
||||
%% End of the role 'fake_role'
|
||||
%% Start of the block 'Block in pre task'
|
||||
block_59e01f41["[block] Block in pre task"]
|
||||
style block_59e01f41 fill:#a4288c,color:#ffffff,stroke:#a4288c
|
||||
play_74990123 --> |"2"| block_59e01f41
|
||||
linkStyle 45 stroke:#a4288c,color:#a4288c
|
||||
subgraph subgraph_block_59e01f41["Block in pre task "]
|
||||
pre_task_d56eed17["debug"]
|
||||
style pre_task_d56eed17 stroke:#a4288c,fill:#ffffff
|
||||
block_59e01f41 --> |"1"| pre_task_d56eed17
|
||||
linkStyle 46 stroke:#a4288c,color:#a4288c
|
||||
end
|
||||
%% End of the block 'Block in pre task'
|
||||
task_13ae5b2d["[task] Install tree"]
|
||||
style task_13ae5b2d stroke:#a4288c,fill:#ffffff
|
||||
play_74990123 --> |"3"| task_13ae5b2d
|
||||
linkStyle 47 stroke:#a4288c,color:#a4288c
|
||||
%% Start of the block 'Install Apache'
|
||||
block_58464279["[block] Install Apache"]
|
||||
style block_58464279 fill:#a4288c,color:#ffffff,stroke:#a4288c
|
||||
play_74990123 --> |"4 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| block_58464279
|
||||
linkStyle 48 stroke:#a4288c,color:#a4288c
|
||||
subgraph subgraph_block_58464279["Install Apache "]
|
||||
task_3c6e5034["Install some packages"]
|
||||
style task_3c6e5034 stroke:#a4288c,fill:#ffffff
|
||||
block_58464279 --> |"1 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_3c6e5034
|
||||
linkStyle 49 stroke:#a4288c,color:#a4288c
|
||||
task_7f997d4e["template"]
|
||||
style task_7f997d4e stroke:#a4288c,fill:#ffffff
|
||||
block_58464279 --> |"2 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_7f997d4e
|
||||
linkStyle 50 stroke:#a4288c,color:#a4288c
|
||||
%% Start of the block ''
|
||||
block_58e72e2c["[block] "]
|
||||
style block_58e72e2c fill:#a4288c,color:#ffffff,stroke:#a4288c
|
||||
block_58464279 --> |"3 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| block_58e72e2c
|
||||
linkStyle 51 stroke:#a4288c,color:#a4288c
|
||||
subgraph subgraph_block_58e72e2c[" "]
|
||||
task_752a43fc["get_url"]
|
||||
style task_752a43fc stroke:#a4288c,fill:#ffffff
|
||||
block_58e72e2c --> |"1 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6') and True]"| task_752a43fc
|
||||
linkStyle 52 stroke:#a4288c,color:#a4288c
|
||||
task_b31070fc["command"]
|
||||
style task_b31070fc stroke:#a4288c,fill:#ffffff
|
||||
block_58e72e2c --> |"2 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_b31070fc
|
||||
linkStyle 53 stroke:#a4288c,color:#a4288c
|
||||
end
|
||||
%% End of the block ''
|
||||
task_4ff99c46["service"]
|
||||
style task_4ff99c46 stroke:#a4288c,fill:#ffffff
|
||||
block_58464279 --> |"4 [when: (ansible_facts['distribution'] == 'CentOS' and ansible_facts['distribution_major_version'] == '6')]"| task_4ff99c46
|
||||
linkStyle 54 stroke:#a4288c,color:#a4288c
|
||||
end
|
||||
%% End of the block 'Install Apache'
|
||||
task_27e5fe68["[task] Create a username for tomcat"]
|
||||
style task_27e5fe68 stroke:#a4288c,fill:#ffffff
|
||||
play_74990123 --> |"5"| task_27e5fe68
|
||||
linkStyle 55 stroke:#a4288c,color:#a4288c
|
||||
post_task_b09e48f3["[post_task] Debug"]
|
||||
style post_task_b09e48f3 stroke:#a4288c,fill:#ffffff
|
||||
play_74990123 --> |"6"| post_task_b09e48f3
|
||||
linkStyle 56 stroke:#a4288c,color:#a4288c
|
||||
%% Start of the block 'My post task block'
|
||||
block_644a319d["[block] My post task block"]
|
||||
style block_644a319d fill:#a4288c,color:#ffffff,stroke:#a4288c
|
||||
play_74990123 --> |"7"| block_644a319d
|
||||
linkStyle 57 stroke:#a4288c,color:#a4288c
|
||||
subgraph subgraph_block_644a319d["My post task block "]
|
||||
post_task_e6f19df5["template"]
|
||||
style post_task_e6f19df5 stroke:#a4288c,fill:#ffffff
|
||||
block_644a319d --> |"1"| post_task_e6f19df5
|
||||
linkStyle 58 stroke:#a4288c,color:#a4288c
|
||||
end
|
||||
%% End of the block 'My post task block'
|
||||
%% End of the play 'Play: all (0)'
|
||||
%% End of the playbook 'tests/fixtures/with_block.yml'
|
||||
|
||||
|
||||
```
|
||||
|
||||
Note on block: Since `block`s are logical group of tasks, the conditional `when` is not displayed on the edges pointing
|
||||
to them but on the tasks inside the block. This
|
||||
mimics [Ansible behavior](https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html#grouping-tasks-with-blocks)
|
||||
|
@ -86,8 +398,12 @@ usage: ansible-playbook-grapher [-h] [-v] [-i INVENTORY]
|
|||
[-o OUTPUT_FILENAME]
|
||||
[--open-protocol-handler {default,vscode,custom}]
|
||||
[--open-protocol-custom-formats OPEN_PROTOCOL_CUSTOM_FORMATS]
|
||||
[--group-roles-by-name] [--version] [-t TAGS]
|
||||
[--skip-tags SKIP_TAGS] [--vault-id VAULT_IDS]
|
||||
[--group-roles-by-name]
|
||||
[--renderer {graphviz,mermaid-flowchart}]
|
||||
[--renderer-mermaid-directive RENDERER_MERMAID_DIRECTIVE]
|
||||
[--renderer-mermaid-orientation {TD,RL,BT,RL,LR}]
|
||||
[--version] [-t TAGS] [--skip-tags SKIP_TAGS]
|
||||
[--vault-id VAULT_IDS]
|
||||
[--ask-vault-password | --vault-password-file VAULT_PASSWORD_FILES]
|
||||
[-e EXTRA_VARS]
|
||||
playbooks [playbooks ...]
|
||||
|
@ -97,12 +413,13 @@ Make graphs from your Ansible Playbooks.
|
|||
positional arguments:
|
||||
playbooks Playbook(s) to graph
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
--ask-vault-password, --ask-vault-pass
|
||||
ask for vault password
|
||||
--group-roles-by-name
|
||||
When rendering the graph, only a single role will be
|
||||
display for all roles having the same names.
|
||||
display for all roles having the same names. Default:
|
||||
False
|
||||
--include-role-tasks Include the tasks of the role in the graph.
|
||||
--open-protocol-custom-formats OPEN_PROTOCOL_CUSTOM_FORMATS
|
||||
The custom formats to use as URLs for the nodes in the
|
||||
|
@ -129,6 +446,16 @@ optional arguments:
|
|||
will be open with VSCode. For 'custom', you need to
|
||||
set a custom format with --open-protocol-custom-
|
||||
formats.
|
||||
--renderer {graphviz,mermaid-flowchart}
|
||||
The renderer to use to generate the graph. Default:
|
||||
graphviz
|
||||
--renderer-mermaid-directive RENDERER_MERMAID_DIRECTIVE
|
||||
The directive for the mermaid renderer. Can be used to
|
||||
customize the output: fonts, theme, curve etc. More
|
||||
info at https://mermaid.js.org/config/directives.html.
|
||||
Default: '%%{ init: { "flowchart": { "curve": "bumpX" } } }%%'
|
||||
--renderer-mermaid-orientation {TD,RL,BT,RL,LR}
|
||||
The orientation of the flow chart. Default: 'LR'
|
||||
--skip-tags SKIP_TAGS
|
||||
only run plays and tasks whose tags do not match these
|
||||
values
|
||||
|
@ -148,7 +475,7 @@ optional arguments:
|
|||
-o OUTPUT_FILENAME, --output-file-name OUTPUT_FILENAME
|
||||
Output filename without the '.svg' extension. Default:
|
||||
<playbook>.svg
|
||||
-s, --save-dot-file Save the dot file used to generate the graph.
|
||||
-s, --save-dot-file Save the graphviz dot file used to generate the graph.
|
||||
-t TAGS, --tags TAGS only run plays and tasks tagged with these values
|
||||
-v, --verbose Causes Ansible to print more debug messages. Adding
|
||||
multiple -v will increase the verbosity, the builtin
|
||||
|
@ -195,7 +522,7 @@ Run the tests and open the generated files in your system’s default viewer app
|
|||
|
||||
```shell script
|
||||
export TEST_VIEW_GENERATED_FILE=1
|
||||
$ make test # run all tests
|
||||
make test # run all tests
|
||||
```
|
||||
|
||||
The graphs are generated in the folder `tests/generated-svgs`. They are also generated as artefacts
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (C) 2022 Mohamed El Mouctar HAIDARA
|
||||
# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -13,18 +13,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Dict, List, Set
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
PlaybookNode,
|
||||
RoleNode,
|
||||
PlayNode,
|
||||
)
|
||||
from ansibleplaybookgrapher.parser import PlaybookParser
|
||||
from ansibleplaybookgrapher.utils import merge_dicts
|
||||
from .graph import PlaybookNode, PlayNode, TaskNode, RoleNode, BlockNode
|
||||
from .graph_model import PlaybookNode, PlayNode, TaskNode, RoleNode, BlockNode
|
||||
from .parser import PlaybookParser
|
||||
|
||||
__version__ = "2.0.0-dev"
|
||||
|
@ -40,16 +40,13 @@ class Grapher:
|
|||
"""
|
||||
self.playbook_filenames = playbook_filenames
|
||||
|
||||
# The usage of the roles in all playbooks
|
||||
self.roles_usage: Dict[RoleNode, Set[PlayNode]] = {}
|
||||
|
||||
def parse(
|
||||
self,
|
||||
include_role_tasks: bool = False,
|
||||
tags: List[str] = None,
|
||||
skip_tags: List[str] = None,
|
||||
group_roles_by_name: bool = False,
|
||||
) -> List[PlaybookNode]:
|
||||
) -> Tuple[List[PlaybookNode], Dict[RoleNode, Set[PlayNode]]]:
|
||||
"""
|
||||
Parses all the provided playbooks
|
||||
:param include_role_tasks: Should we include the role tasks
|
||||
|
@ -59,6 +56,8 @@ class Grapher:
|
|||
:return:
|
||||
"""
|
||||
playbook_nodes = []
|
||||
roles_usage: Dict[RoleNode, Set[PlayNode]] = {}
|
||||
|
||||
for playbook_file in self.playbook_filenames:
|
||||
display.display(f"Parsing playbook {playbook_file}")
|
||||
playbook_parser = PlaybookParser(
|
||||
|
@ -72,8 +71,6 @@ class Grapher:
|
|||
playbook_nodes.append(playbook_node)
|
||||
|
||||
# Update the usage of the roles
|
||||
self.roles_usage = merge_dicts(
|
||||
self.roles_usage, playbook_node.roles_usage()
|
||||
)
|
||||
roles_usage = merge_dicts(roles_usage, playbook_node.roles_usage())
|
||||
|
||||
return playbook_nodes
|
||||
return playbook_nodes, roles_usage
|
||||
|
|
|
@ -26,6 +26,11 @@ from ansible.utils.display import Display
|
|||
from ansibleplaybookgrapher import __prog__, __version__, Grapher
|
||||
from ansibleplaybookgrapher.renderer import OPEN_PROTOCOL_HANDLERS
|
||||
from ansibleplaybookgrapher.renderer.graphviz import GraphvizRenderer
|
||||
from ansibleplaybookgrapher.renderer.mermaid import (
|
||||
MermaidFlowChartRenderer,
|
||||
DEFAULT_DIRECTIVE as MERMAID_DEFAULT_DIRECTIVE,
|
||||
DEFAULT_ORIENTATION as MERMAID_DEFAULT_ORIENTATION,
|
||||
)
|
||||
|
||||
# The display is a singleton. This instruction will NOT return a new instance.
|
||||
# We explicitly set the verbosity after the init.
|
||||
|
@ -52,26 +57,41 @@ class PlaybookGrapherCLI(CLI):
|
|||
|
||||
display.verbosity = self.options.verbosity
|
||||
grapher = Grapher(self.options.playbook_filenames)
|
||||
playbook_nodes = grapher.parse(
|
||||
playbook_nodes, roles_usage = grapher.parse(
|
||||
include_role_tasks=self.options.include_role_tasks,
|
||||
tags=self.options.tags,
|
||||
skip_tags=self.options.skip_tags,
|
||||
group_roles_by_name=self.options.group_roles_by_name,
|
||||
)
|
||||
# TODO: add condition to choose the renderer
|
||||
renderer = GraphvizRenderer(
|
||||
playbook_nodes=playbook_nodes,
|
||||
roles_usage=grapher.roles_usage,
|
||||
)
|
||||
output_path = renderer.render(
|
||||
open_protocol_handler=self.options.open_protocol_handler,
|
||||
open_protocol_custom_formats=self.options.open_protocol_custom_formats,
|
||||
save_dot_file=self.options.save_dot_file,
|
||||
output_filename=self.options.output_filename,
|
||||
view=self.options.view,
|
||||
)
|
||||
|
||||
return output_path
|
||||
if self.options.renderer == "graphviz":
|
||||
renderer = GraphvizRenderer(
|
||||
playbook_nodes=playbook_nodes,
|
||||
roles_usage=roles_usage,
|
||||
)
|
||||
output_path = renderer.render(
|
||||
open_protocol_handler=self.options.open_protocol_handler,
|
||||
open_protocol_custom_formats=self.options.open_protocol_custom_formats,
|
||||
output_filename=self.options.output_filename,
|
||||
view=self.options.view,
|
||||
save_dot_file=self.options.save_dot_file,
|
||||
)
|
||||
|
||||
return output_path
|
||||
else:
|
||||
renderer = MermaidFlowChartRenderer(
|
||||
playbook_nodes=playbook_nodes,
|
||||
roles_usage=roles_usage,
|
||||
)
|
||||
output_path = renderer.render(
|
||||
open_protocol_handler=self.options.open_protocol_handler,
|
||||
open_protocol_custom_formats=self.options.open_protocol_custom_formats,
|
||||
output_filename=self.options.output_filename,
|
||||
view=self.options.view,
|
||||
directive=self.options.renderer_mermaid_directive,
|
||||
orientation=self.options.renderer_mermaid_orientation,
|
||||
)
|
||||
return output_path
|
||||
|
||||
def _add_my_options(self):
|
||||
"""
|
||||
|
@ -102,7 +122,7 @@ class PlaybookGrapherCLI(CLI):
|
|||
dest="save_dot_file",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Save the dot file used to generate the graph.",
|
||||
help="Save the graphviz dot file used to generate the graph.",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
|
@ -155,7 +175,27 @@ class PlaybookGrapherCLI(CLI):
|
|||
"--group-roles-by-name",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="When rendering the graph, only a single role will be display for all roles having the same names.",
|
||||
help="When rendering the graph, only a single role will be display for all roles having the same names. Default: %(default)s",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
"--renderer",
|
||||
choices=["graphviz", "mermaid-flowchart"],
|
||||
default="graphviz",
|
||||
help="The renderer to use to generate the graph. Default: %(default)s",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
"--renderer-mermaid-directive",
|
||||
default=MERMAID_DEFAULT_DIRECTIVE,
|
||||
help="The directive for the mermaid renderer. Can be used to customize the output: fonts, theme, curve etc. More info at https://mermaid.js.org/config/directives.html. Default: '%(default)s'",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
"--renderer-mermaid-orientation",
|
||||
default=MERMAID_DEFAULT_ORIENTATION,
|
||||
choices=["TD", "RL", "BT", "RL", "LR"],
|
||||
help="The orientation of the flow chart. Default: '%(default)s'",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
|
@ -192,10 +232,11 @@ class PlaybookGrapherCLI(CLI):
|
|||
self.options = options
|
||||
|
||||
if self.options.output_filename is None:
|
||||
# use the first playbook name (without the extension) as output filename
|
||||
self.options.output_filename = os.path.splitext(
|
||||
ntpath.basename(self.options.playbook_filenames[0])
|
||||
)[0]
|
||||
basenames = map(ntpath.basename, self.options.playbook_filenames)
|
||||
basenames_without_ext = "-".join(
|
||||
[os.path.splitext(basename)[0] for basename in basenames]
|
||||
)
|
||||
self.options.output_filename = basenames_without_ext
|
||||
|
||||
if self.options.open_protocol_handler == "custom":
|
||||
self.validate_open_protocol_custom_formats()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (C) 2022 Mohamed El Mouctar HAIDARA
|
||||
# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -14,7 +14,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Set, Type, Tuple
|
||||
from typing import Dict, List, Set, Type, Tuple, Optional
|
||||
|
||||
from ansibleplaybookgrapher.utils import generate_id, get_play_colors
|
||||
|
||||
|
@ -31,7 +31,7 @@ class Node:
|
|||
when: str = "",
|
||||
raw_object=None,
|
||||
parent: "Node" = None,
|
||||
index: int = None,
|
||||
index: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
|
||||
|
@ -53,7 +53,7 @@ class Node:
|
|||
self.set_position()
|
||||
|
||||
# The index of this node in the parent node if it has one (starting from 1)
|
||||
self.index: int = index
|
||||
self.index: Optional[int] = index
|
||||
|
||||
def set_position(self):
|
||||
"""
|
|
@ -27,7 +27,7 @@ from ansible.playbook.task_include import TaskInclude
|
|||
from ansible.template import Templar
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
TaskNode,
|
||||
PlaybookNode,
|
||||
RoleNode,
|
||||
|
@ -194,7 +194,9 @@ class PlaybookParser(BaseParser):
|
|||
|
||||
display.v(f"Parsing {play_name}")
|
||||
|
||||
play_node = PlayNode(play_name, hosts=play_hosts, raw_object=play)
|
||||
play_node = PlayNode(
|
||||
play_name, hosts=play_hosts, raw_object=play, parent=playbook_root_node
|
||||
)
|
||||
playbook_root_node.add_node("plays", play_node)
|
||||
|
||||
# loop through the pre_tasks
|
||||
|
@ -318,8 +320,7 @@ class PlaybookParser(BaseParser):
|
|||
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."
|
||||
"Looping on tasks or roles are not supported for the moment. Only the task having the loop argument will be added to the graph."
|
||||
)
|
||||
|
||||
if isinstance(task_or_block, Block):
|
||||
|
|
|
@ -17,7 +17,7 @@ from typing import Dict, Optional, Set
|
|||
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
PlaybookNode,
|
||||
PlayNode,
|
||||
RoleNode,
|
||||
|
@ -41,7 +41,42 @@ OPEN_PROTOCOL_HANDLERS = {
|
|||
}
|
||||
|
||||
|
||||
class Renderer(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
playbook_nodes: PlaybookNode,
|
||||
roles_usage: Dict[RoleNode, Set[PlayNode]],
|
||||
):
|
||||
self.playbook_nodes = playbook_nodes
|
||||
self.roles_usage = roles_usage
|
||||
|
||||
@abstractmethod
|
||||
def render(
|
||||
self,
|
||||
open_protocol_handler: str,
|
||||
open_protocol_custom_formats: Dict[str, str],
|
||||
output_filename: str,
|
||||
view: bool,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
Render the playbooks to a file.
|
||||
:param open_protocol_handler: The protocol handler name to use
|
||||
:param open_protocol_custom_formats: The custom formats to use when the protocol handler is set to custom
|
||||
:param output_filename: without any extension
|
||||
:param view: Whether to open the rendered file in the default viewer
|
||||
:param kwargs:
|
||||
:return: The filename of the rendered file
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlaybookBuilder(ABC):
|
||||
"""
|
||||
This the base class to inherit from by the renderer to build a single Playbook in the target format.
|
||||
It provides some methods that need to be implemented
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
playbook_node: PlaybookNode,
|
||||
|
@ -93,11 +128,11 @@ class PlaybookBuilder(ABC):
|
|||
)
|
||||
|
||||
@abstractmethod
|
||||
def build_playbook(self, **kwargs):
|
||||
def build_playbook(self, **kwargs) -> str:
|
||||
"""
|
||||
Build the whole playbook
|
||||
:param kwargs:
|
||||
:return:
|
||||
:return: The rendered playbook as a string
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -111,6 +146,53 @@ class PlaybookBuilder(ABC):
|
|||
"""
|
||||
pass
|
||||
|
||||
def traverse_play(self, play_node: PlayNode, **kwargs):
|
||||
"""
|
||||
Traverse a play to build the graph: pre_tasks, roles, tasks, post_tasks
|
||||
:param play_node:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
color, play_font_color = play_node.colors
|
||||
# pre_tasks
|
||||
for pre_task in play_node.pre_tasks:
|
||||
self.build_node(
|
||||
node=pre_task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[pre_task] ",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# roles
|
||||
for role in play_node.roles:
|
||||
self.build_role(
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
role_node=role,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# tasks
|
||||
for task in play_node.tasks:
|
||||
self.build_node(
|
||||
node=task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[task] ",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# post_tasks
|
||||
for post_task in play_node.post_tasks:
|
||||
self.build_node(
|
||||
node=post_task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[post_task] ",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def build_task(self, task_node: TaskNode, color: str, fontcolor: str, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -18,14 +18,14 @@ from typing import Dict, List, Set
|
|||
from ansible.utils.display import Display
|
||||
from graphviz import Digraph
|
||||
|
||||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
PlaybookNode,
|
||||
PlayNode,
|
||||
RoleNode,
|
||||
BlockNode,
|
||||
TaskNode,
|
||||
)
|
||||
from ansibleplaybookgrapher.renderer import PlaybookBuilder
|
||||
from ansibleplaybookgrapher.renderer import PlaybookBuilder, Renderer
|
||||
from ansibleplaybookgrapher.renderer.graphviz.postprocessor import GraphvizPostProcessor
|
||||
|
||||
display = Display()
|
||||
|
@ -39,7 +39,7 @@ DEFAULT_GRAPH_ATTR = {
|
|||
}
|
||||
|
||||
|
||||
class GraphvizRenderer:
|
||||
class GraphvizRenderer(Renderer):
|
||||
def __init__(
|
||||
self,
|
||||
playbook_nodes: List[PlaybookNode],
|
||||
|
@ -52,7 +52,6 @@ class GraphvizRenderer:
|
|||
self,
|
||||
open_protocol_handler: str,
|
||||
open_protocol_custom_formats: Dict[str, str],
|
||||
save_dot_file: bool,
|
||||
output_filename: str,
|
||||
view: bool,
|
||||
**kwargs,
|
||||
|
@ -60,6 +59,8 @@ class GraphvizRenderer:
|
|||
"""
|
||||
:return: The filename where the playbooks where rendered
|
||||
"""
|
||||
save_dot_file = kwargs.get("save_dot_file", False)
|
||||
|
||||
# Set of the roles that have been built so far for all the playbooks
|
||||
roles_built = set()
|
||||
digraph = Digraph(
|
||||
|
@ -219,6 +220,13 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
Render a role in the graph
|
||||
:return:
|
||||
"""
|
||||
|
||||
# check if we already built this role
|
||||
if role_node in self.roles_built:
|
||||
return
|
||||
|
||||
self.roles_built.add(role_node)
|
||||
|
||||
digraph = kwargs["digraph"]
|
||||
|
||||
if role_node.include_role: # For include_role, we point to a file
|
||||
|
@ -240,12 +248,6 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
labeltooltip=role_edge_label,
|
||||
)
|
||||
|
||||
# check if we already built this role
|
||||
if role_node in self.roles_built:
|
||||
return
|
||||
|
||||
self.roles_built.add(role_node)
|
||||
|
||||
plays_using_this_role = self.roles_usage[role_node]
|
||||
if len(plays_using_this_role) > 1:
|
||||
# If the role is used in multiple plays, we take black as the default color
|
||||
|
@ -274,10 +276,10 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
digraph=role_subgraph,
|
||||
)
|
||||
|
||||
def build_playbook(self, **kwargs):
|
||||
def build_playbook(self, **kwargs) -> str:
|
||||
"""
|
||||
Convert the PlaybookNode to the graphviz dot format
|
||||
:return:
|
||||
:return: The text representation of the graphviz dot format for the playbook
|
||||
"""
|
||||
display.vvv(f"Converting the graph to the dot format for graphviz")
|
||||
# root node
|
||||
|
@ -293,6 +295,8 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
with self.digraph.subgraph(name=play.name) as play_subgraph:
|
||||
self.build_play(play, digraph=play_subgraph, **kwargs)
|
||||
|
||||
return self.digraph.source
|
||||
|
||||
def build_play(self, play_node: PlayNode, **kwargs):
|
||||
"""
|
||||
|
||||
|
@ -320,7 +324,7 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
URL=self.get_node_url(play_node, "file"),
|
||||
)
|
||||
|
||||
# edge from root node to play
|
||||
# from playbook to play
|
||||
playbook_to_play_label = f"{play_node.index} {play_node.name}"
|
||||
self.digraph.edge(
|
||||
self.playbook_node.id,
|
||||
|
@ -333,41 +337,5 @@ class GraphvizPlaybookBuilder(PlaybookBuilder):
|
|||
labeltooltip=playbook_to_play_label,
|
||||
)
|
||||
|
||||
# pre_tasks
|
||||
for pre_task in play_node.pre_tasks:
|
||||
self.build_node(
|
||||
node=pre_task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[pre_task] ",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# roles
|
||||
for role in play_node.roles:
|
||||
self.build_role(
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
role_node=role,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# tasks
|
||||
for task in play_node.tasks:
|
||||
self.build_node(
|
||||
node=task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[task] ",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# post_tasks
|
||||
for post_task in play_node.post_tasks:
|
||||
self.build_node(
|
||||
node=post_task,
|
||||
color=color,
|
||||
fontcolor=play_font_color,
|
||||
node_label_prefix="[post_task] ",
|
||||
**kwargs,
|
||||
)
|
||||
# traverse the play
|
||||
self.traverse_play(play_node, **kwargs)
|
||||
|
|
|
@ -19,7 +19,7 @@ from ansible.utils.display import Display
|
|||
from lxml import etree
|
||||
from svg.path import parse_path
|
||||
|
||||
from ansibleplaybookgrapher.graph import PlaybookNode
|
||||
from ansibleplaybookgrapher.graph_model import PlaybookNode
|
||||
|
||||
display = Display()
|
||||
|
||||
|
|
337
ansibleplaybookgrapher/renderer/mermaid.py
Normal file
337
ansibleplaybookgrapher/renderer/mermaid.py
Normal file
|
@ -0,0 +1,337 @@
|
|||
# Copyright (C) 2023 Mohamed El Mouctar HAIDARA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from pathlib import Path
|
||||
from typing import Dict, Set, List
|
||||
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansibleplaybookgrapher import BlockNode, RoleNode, TaskNode, PlayNode, PlaybookNode
|
||||
from ansibleplaybookgrapher.renderer import PlaybookBuilder, Renderer
|
||||
|
||||
display = Display()
|
||||
|
||||
# Default directive when rendering the graph.
|
||||
# More info at
|
||||
# https://mermaid.js.org/config/directives.html
|
||||
#
|
||||
DEFAULT_DIRECTIVE = '%%{ init: { "flowchart": { "curve": "bumpX" } } }%%'
|
||||
DEFAULT_ORIENTATION = "LR" # Left to right
|
||||
|
||||
|
||||
class MermaidFlowChartRenderer(Renderer):
|
||||
def __init__(
|
||||
self,
|
||||
playbook_nodes: List[PlaybookNode],
|
||||
roles_usage: Dict["RoleNode", Set[PlayNode]],
|
||||
):
|
||||
self.playbook_nodes = playbook_nodes
|
||||
self.roles_usage = roles_usage
|
||||
|
||||
def render(
|
||||
self,
|
||||
open_protocol_handler: str,
|
||||
open_protocol_custom_formats: Dict[str, str],
|
||||
output_filename: str,
|
||||
view: bool,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
|
||||
:param open_protocol_handler:
|
||||
:param open_protocol_custom_formats:
|
||||
:param output_filename: without any extension
|
||||
:param view:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
# TODO: Add support for protocol handler
|
||||
# TODO: Add support for hover
|
||||
mermaid_code = "---\n"
|
||||
mermaid_code += "title: Ansible Playbook Grapher\n"
|
||||
mermaid_code += "---\n"
|
||||
|
||||
directive = kwargs.get("directive", DEFAULT_DIRECTIVE)
|
||||
orientation = kwargs.get("orientation", DEFAULT_ORIENTATION)
|
||||
|
||||
display.vvv(f"Using '{directive}' as directive for the mermaid chart")
|
||||
mermaid_code += f"{directive}\n"
|
||||
|
||||
mermaid_code += f"flowchart {orientation}\n"
|
||||
|
||||
# Mermaid only supports adding style to links by using the order of the link when it is created
|
||||
# https://mermaid.js.org/syntax/flowchart.html#styling-links
|
||||
link_order = 0
|
||||
|
||||
# Set of the roles that have been built so far for all the playbooks
|
||||
roles_built = set()
|
||||
for playbook_node in self.playbook_nodes:
|
||||
playbook_builder = MermaidFlowChartPlaybookBuilder(
|
||||
playbook_node=playbook_node,
|
||||
open_protocol_handler=open_protocol_handler,
|
||||
open_protocol_custom_formats=open_protocol_custom_formats,
|
||||
roles_usage=self.roles_usage,
|
||||
roles_built=roles_built,
|
||||
link_order=link_order,
|
||||
)
|
||||
|
||||
mermaid_code += playbook_builder.build_playbook()
|
||||
link_order = playbook_builder.link_order
|
||||
roles_built.update(playbook_builder.roles_built)
|
||||
|
||||
final_output_path_file = Path(f"{output_filename}.mmd")
|
||||
# Make the sure the parents directories exist
|
||||
final_output_path_file.parent.mkdir(exist_ok=True, parents=True)
|
||||
final_output_path_file.write_text(mermaid_code)
|
||||
|
||||
display.display(
|
||||
f"Mermaid code written to {final_output_path_file}", color="green"
|
||||
)
|
||||
|
||||
if view:
|
||||
# TODO: implement the view option
|
||||
# https://github.com/mermaidjs/mermaid-live-editor/issues/41 and https://mermaid.ink/
|
||||
display.warning(
|
||||
"The --view option is not supported yet by the mermaid renderer"
|
||||
)
|
||||
|
||||
return final_output_path_file
|
||||
|
||||
|
||||
class MermaidFlowChartPlaybookBuilder(PlaybookBuilder):
|
||||
def __init__(
|
||||
self,
|
||||
playbook_node: PlaybookNode,
|
||||
open_protocol_handler: str,
|
||||
open_protocol_custom_formats: Dict[str, str],
|
||||
roles_usage: Dict[RoleNode, Set[PlayNode]],
|
||||
roles_built: Set[RoleNode],
|
||||
link_order: int = 0,
|
||||
):
|
||||
super().__init__(
|
||||
playbook_node,
|
||||
open_protocol_handler,
|
||||
open_protocol_custom_formats,
|
||||
roles_usage,
|
||||
roles_built,
|
||||
)
|
||||
self.mermaid_code = ""
|
||||
# Used as an identifier for the links
|
||||
self.link_order = link_order
|
||||
# The current depth level of the nodes. Used for indentation
|
||||
self._identation_level = 1
|
||||
|
||||
def build_playbook(self, **kwargs) -> str:
|
||||
"""
|
||||
Build the playbook
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
display.vvv(
|
||||
f"Converting the playbook '{self.playbook_node.name}' to mermaid format"
|
||||
)
|
||||
|
||||
# Playbook node
|
||||
self.add_comment(f"Start of the playbook '{self.playbook_node.name}'")
|
||||
self.add_text(f'{self.playbook_node.id}("{self.playbook_node.name}")')
|
||||
|
||||
self._identation_level += 1
|
||||
for play_node in self.playbook_node.plays:
|
||||
self.build_play(play_node)
|
||||
self._identation_level -= 1
|
||||
|
||||
self.add_comment(f"End of the playbook '{self.playbook_node.name}'\n")
|
||||
|
||||
return self.mermaid_code
|
||||
|
||||
def build_play(self, play_node: PlayNode, **kwargs):
|
||||
"""
|
||||
|
||||
:param play_node:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
# Play node
|
||||
color, play_font_color = play_node.colors
|
||||
self.add_comment(f"Start of the play '{play_node.name}'")
|
||||
|
||||
self.add_text(f'{play_node.id}["{play_node.name}"]')
|
||||
self.add_text(f"style {play_node.id} fill:{color},color:{play_font_color}")
|
||||
|
||||
# From playbook to play
|
||||
self.add_link(
|
||||
source_id=play_node.parent.id,
|
||||
text=f"{play_node.index}",
|
||||
dest_id=play_node.id,
|
||||
style=f"stroke:{color},color:{color}",
|
||||
)
|
||||
|
||||
# traverse the play
|
||||
self._identation_level += 1
|
||||
self.traverse_play(play_node)
|
||||
self._identation_level -= 1
|
||||
|
||||
self.add_comment(f"End of the play '{play_node.name}'")
|
||||
|
||||
def build_task(self, task_node: TaskNode, color: str, fontcolor: str, **kwargs):
|
||||
"""
|
||||
|
||||
:param task_node:
|
||||
:param color:
|
||||
:param fontcolor:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
node_label_prefix = kwargs.get("node_label_prefix", "")
|
||||
# Task node
|
||||
self.add_text(f'{task_node.id}["{node_label_prefix}{task_node.name}"]')
|
||||
self.add_text(f"style {task_node.id} stroke:{color},fill:{fontcolor}")
|
||||
|
||||
# From parent to task
|
||||
self.add_link(
|
||||
source_id=task_node.parent.id,
|
||||
text=f"{task_node.index} {task_node.when}",
|
||||
dest_id=task_node.id,
|
||||
style=f"stroke:{color},color:{color}",
|
||||
)
|
||||
|
||||
def build_role(self, role_node: RoleNode, color: str, fontcolor: str, **kwargs):
|
||||
"""
|
||||
|
||||
:param role_node:
|
||||
:param color:
|
||||
:param fontcolor:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# check if we already built this role
|
||||
if role_node in self.roles_built:
|
||||
return
|
||||
self.roles_built.add(role_node)
|
||||
|
||||
# Role node
|
||||
self.add_comment(f"Start of the role '{role_node.name}'")
|
||||
self.add_text(f'{role_node.id}("[role] {role_node.name}")')
|
||||
self.add_text(
|
||||
f"style {role_node.id} fill:{color},color:{fontcolor},stroke:{color}"
|
||||
)
|
||||
|
||||
# from parent to role
|
||||
self.add_link(
|
||||
source_id=role_node.parent.id,
|
||||
text=f"{role_node.index} {role_node.when}",
|
||||
dest_id=role_node.id,
|
||||
style=f"stroke:{color},color:{color}",
|
||||
)
|
||||
|
||||
# role tasks
|
||||
self._identation_level += 1
|
||||
for role_task in role_node.tasks:
|
||||
self.build_node(
|
||||
node=role_task,
|
||||
color=color,
|
||||
fontcolor=fontcolor,
|
||||
)
|
||||
self._identation_level -= 1
|
||||
self.add_comment(f"End of the role '{role_node.name}'")
|
||||
|
||||
def build_block(self, block_node: BlockNode, color: str, fontcolor: str, **kwargs):
|
||||
"""
|
||||
|
||||
:param block_node:
|
||||
:param color:
|
||||
:param fontcolor:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Block node
|
||||
self.add_comment(f"Start of the block '{block_node.name}'")
|
||||
self.add_text(f'{block_node.id}["[block] {block_node.name}"]')
|
||||
self.add_text(
|
||||
f"style {block_node.id} fill:{color},color:{fontcolor},stroke:{color}"
|
||||
)
|
||||
|
||||
# from parent to block
|
||||
self.add_link(
|
||||
source_id=block_node.parent.id,
|
||||
text=f"{block_node.index} {block_node.when}",
|
||||
dest_id=block_node.id,
|
||||
style=f"stroke:{color},color:{color}",
|
||||
)
|
||||
|
||||
self.add_text(f'subgraph subgraph_{block_node.id}["{block_node.name} "]')
|
||||
|
||||
self._identation_level += 1
|
||||
for task in block_node.tasks:
|
||||
self.build_node(
|
||||
node=task,
|
||||
color=color,
|
||||
fontcolor=fontcolor,
|
||||
)
|
||||
self._identation_level -= 1
|
||||
|
||||
self.add_text("end") # End of the subgraph
|
||||
self.add_comment(f"End of the block '{block_node.name}'")
|
||||
|
||||
def add_link(
|
||||
self,
|
||||
source_id: str,
|
||||
text: str,
|
||||
dest_id: str,
|
||||
style: str = "",
|
||||
link_type: str = "--",
|
||||
):
|
||||
"""
|
||||
Add link between two nodes
|
||||
:param source_id: The link source
|
||||
:param text: The text on the link
|
||||
:param dest_id: The link destination
|
||||
:param style: The style to apply to the link
|
||||
:param link_type: Type of link to create. https://mermaid.js.org/syntax/flowchart.html#links-between-nodes
|
||||
:return:
|
||||
"""
|
||||
# Replace double quotes with single quotes. Mermaid doesn't like double quotes
|
||||
text = text.replace('"', "'").strip()
|
||||
self.add_text(f'{source_id} {link_type}> |"{text}"| {dest_id}')
|
||||
|
||||
if style != "" or style is not None:
|
||||
self.add_text(f"linkStyle {self.link_order} {style}")
|
||||
|
||||
self.link_order += 1
|
||||
|
||||
def add_comment(self, text: str):
|
||||
"""
|
||||
Add a comment to the mermaid code
|
||||
:param text: The text used as a comment
|
||||
:return:
|
||||
"""
|
||||
self.mermaid_code += f"{self.indentation}%% {text}\n"
|
||||
|
||||
def add_text(self, text: str):
|
||||
"""
|
||||
Add a text to the mermaid diagram
|
||||
:param text:
|
||||
:return:
|
||||
"""
|
||||
self.mermaid_code += f"{self.indentation}{text}\n"
|
||||
|
||||
@property
|
||||
def indentation(self) -> str:
|
||||
"""
|
||||
Return the current indentation level as tabulations
|
||||
:return:
|
||||
"""
|
||||
return "\t" * self._identation_level
|
BIN
img/block.png
BIN
img/block.png
Binary file not shown.
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 259 KiB |
BIN
img/example.png
BIN
img/example.png
Binary file not shown.
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 218 KiB |
Binary file not shown.
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 278 KiB |
|
@ -7,9 +7,9 @@ from jinja2 import Template
|
|||
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def list_svg_files(path_pattern: str) -> List[str]:
|
||||
def list_files(path_pattern: str) -> List[str]:
|
||||
"""
|
||||
|
||||
Return the list of files matching the pattern
|
||||
:param path_pattern:
|
||||
:return:
|
||||
"""
|
||||
|
@ -20,11 +20,10 @@ if __name__ == "__main__":
|
|||
with open(os.path.join(DIR_PATH, "job-summary.md.j2")) as template_file:
|
||||
template = Template(template_file.read())
|
||||
|
||||
svg_files = list_svg_files(f"{os.environ['SVG_FILES_PATH']}/*.svg")
|
||||
links = []
|
||||
for f in svg_files:
|
||||
links.append(
|
||||
f"https://raw.githubusercontent.com/{os.environ['GITHUB_REPOSITORY']}/{os.environ['SVG_COMMIT_SHA']}/{f}"
|
||||
)
|
||||
mermaid_files = list_files(f"{os.environ['MERMAID_FILES_PATH']}/*.mmd")
|
||||
matrix_job_identifier = os.environ["MATRIX_JOB_IDENTIFIER"]
|
||||
files = []
|
||||
for filename in mermaid_files:
|
||||
files.append({"name": filename, "content": open(filename).read()})
|
||||
|
||||
print(template.render(svg_files=links))
|
||||
print(template.render(files=files, matrix_job_identifier=matrix_job_identifier))
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
### Generated SVG files
|
||||
| Images |
|
||||
|:-------:|
|
||||
{% for svg_file in svg_files -%}
|
||||
| ![svg]({{ svg_file }}) |
|
||||
## Generated Mermaid files
|
||||
|
||||
This is the list of all the mermaid graph generated by the tests.
|
||||
|
||||
**Note:** It doesn't match the generated SVGs graphs yet in the artifacts.
|
||||
|
||||
{% for file in files -%}
|
||||
### {{ file.name }} - {{ matrix_job_identifier }}
|
||||
|
||||
```mermaid
|
||||
{{ file.content }}
|
||||
```
|
||||
|
||||
{% endfor %}
|
||||
|
|
|
@ -85,6 +85,20 @@ def test_cli_output_filename(output_filename_option, expected):
|
|||
assert cli.options.output_filename == expected
|
||||
|
||||
|
||||
def test_cli_output_filename_multiple_playbooks():
|
||||
"""
|
||||
Test for the output filename when using multiple playbooks
|
||||
:return:
|
||||
"""
|
||||
args = [__prog__] + ["playbook.yml", "second-playbook.yml", "third-playbook.yaml"]
|
||||
|
||||
cli = PlaybookGrapherCLI(args)
|
||||
|
||||
cli.parse()
|
||||
|
||||
assert cli.options.output_filename == "playbook-second-playbook-third-playbook"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"include_role_tasks_option, expected",
|
||||
[(["--"], False), (["--include-role-tasks"], True)],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
RoleNode,
|
||||
TaskNode,
|
||||
PlayNode,
|
|
@ -3,7 +3,7 @@ from _elementtree import Element
|
|||
import pytest
|
||||
from lxml import etree
|
||||
|
||||
from ansibleplaybookgrapher.graph import PlaybookNode, PlayNode, TaskNode
|
||||
from ansibleplaybookgrapher.graph_model import PlaybookNode, PlayNode, TaskNode
|
||||
from ansibleplaybookgrapher.renderer.graphviz.postprocessor import (
|
||||
GraphvizPostProcessor,
|
||||
SVG_NAMESPACE,
|
|
@ -16,7 +16,7 @@ DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
|||
|
||||
def run_grapher(
|
||||
playbook_files: List[str],
|
||||
output_filename: str = None,
|
||||
output_filename: str,
|
||||
additional_args: List[str] = None,
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
|
@ -24,7 +24,7 @@ def run_grapher(
|
|||
:param output_filename:
|
||||
:param additional_args:
|
||||
:param playbook_files:
|
||||
:return: SVG path and playbook absolute path
|
||||
:return: SVG path and playbooks absolute paths
|
||||
"""
|
||||
additional_args = additional_args or []
|
||||
# Explicitly add verbosity to the tests
|
||||
|
@ -56,10 +56,10 @@ def run_grapher(
|
|||
playbook_paths = [os.path.join(FIXTURES_DIR, p_file) for p_file in playbook_files]
|
||||
args = [__prog__]
|
||||
|
||||
if output_filename: # the default filename is the playbook file name minus .yml
|
||||
# put the generated svg in a dedicated folder
|
||||
output_filename = output_filename.replace("[", "-").replace("]", "")
|
||||
args.extend(["-o", os.path.join(DIR_PATH, "generated-svgs", output_filename)])
|
||||
# Clean the name a little bit
|
||||
output_filename = output_filename.replace("[", "-").replace("]", "")
|
||||
# put the generated file in a dedicated folder
|
||||
args.extend(["-o", os.path.join(DIR_PATH, "generated-svgs", output_filename)])
|
||||
|
||||
args.extend(additional_args)
|
||||
|
||||
|
@ -94,12 +94,12 @@ def _common_tests(
|
|||
:return: A dictionary with the different tasks, roles, pre_tasks as keys and a list of Elements (nodes) as values
|
||||
"""
|
||||
|
||||
pq = PyQuery(filename=svg_path)
|
||||
pq.remove_namespaces()
|
||||
|
||||
# test if the file exist. It will exist only if we write in it.
|
||||
assert os.path.isfile(svg_path), "The svg file should exist"
|
||||
|
||||
pq = PyQuery(filename=svg_path)
|
||||
pq.remove_namespaces()
|
||||
|
||||
playbooks = pq("g[id^='playbook_']")
|
||||
plays = pq("g[id^='play_']")
|
||||
tasks = pq("g[id^='task_']")
|
120
tests/test_mermaid_renderer.py
Normal file
120
tests/test_mermaid_renderer.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
import os
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from ansibleplaybookgrapher import __prog__
|
||||
from ansibleplaybookgrapher.cli import PlaybookGrapherCLI
|
||||
from tests import FIXTURES_DIR
|
||||
|
||||
# This file directory abspath
|
||||
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def run_grapher(
|
||||
playbook_files: List[str],
|
||||
output_filename: str = None,
|
||||
additional_args: List[str] = None,
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
Utility function to run the grapher
|
||||
:param output_filename:
|
||||
:param additional_args:
|
||||
:param playbook_files:
|
||||
:return: Mermaid file path and playbooks absolute paths
|
||||
"""
|
||||
additional_args = additional_args or []
|
||||
# Explicitly add verbosity to the tests
|
||||
additional_args.insert(0, "-vvv")
|
||||
|
||||
playbook_paths = [os.path.join(FIXTURES_DIR, p_file) for p_file in playbook_files]
|
||||
args = [__prog__]
|
||||
|
||||
# Clean the name a little bit
|
||||
output_filename = (
|
||||
output_filename.replace("[", "-").replace("]", "").replace(".yml", "")
|
||||
)
|
||||
# put the generated file in a dedicated folder
|
||||
args.extend(["-o", os.path.join(DIR_PATH, "generated-mermaids", output_filename)])
|
||||
|
||||
args.extend(additional_args)
|
||||
|
||||
args.extend(["--renderer", "mermaid-flowchart"])
|
||||
|
||||
args.extend(playbook_paths)
|
||||
|
||||
cli = PlaybookGrapherCLI(args)
|
||||
|
||||
return cli.run(), playbook_paths
|
||||
|
||||
|
||||
def _common_tests(mermaid_path: str, playbook_paths: List[str], **kwargs):
|
||||
"""
|
||||
|
||||
:param mermaid_path:
|
||||
:param playbook_paths:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# TODO: add proper tests on the mermaid code.
|
||||
# Need a parser to make sure the outputs contain all the playbooks, plays, tasks and roles
|
||||
# test if the file exist. It will exist only if we write in it.
|
||||
assert os.path.isfile(
|
||||
mermaid_path
|
||||
), f"The mermaid file should exist at '{mermaid_path}'"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"playbook_file",
|
||||
[
|
||||
# FIXME: Once we have proper tests, we need to split the parameters similar to what we do with graphviz
|
||||
"docker-mysql-galaxy.yml",
|
||||
"example.yml",
|
||||
"group-roles-by-name.yml",
|
||||
"import_playbook.yml",
|
||||
"import_role.yml",
|
||||
"import_tasks.yml",
|
||||
"include_role.yml",
|
||||
"include_tasks.yml",
|
||||
"multi-plays.yml",
|
||||
"nested_import_playbook.yml",
|
||||
"nested_include_tasks.yml",
|
||||
"relative_var_files.yml",
|
||||
"roles_dependencies.yml",
|
||||
"simple_playbook.yml",
|
||||
"tags.yml",
|
||||
"with_block.yml",
|
||||
"with_roles.yml",
|
||||
],
|
||||
)
|
||||
def test_playbook(request, playbook_file):
|
||||
"""
|
||||
Test the renderer with a single playbook
|
||||
"""
|
||||
mermaid_path, playbook_paths = run_grapher(
|
||||
[playbook_file],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[
|
||||
"-i",
|
||||
os.path.join(FIXTURES_DIR, "inventory"),
|
||||
"--include-role-tasks",
|
||||
],
|
||||
)
|
||||
_common_tests(mermaid_path, playbook_paths)
|
||||
|
||||
|
||||
def test_multiple_playbooks(request):
|
||||
"""
|
||||
Test the renderer with multiple playbooks in a single graph
|
||||
"""
|
||||
mermaid_path, playbook_paths = run_grapher(
|
||||
["multi-plays.yml", "relative_var_files.yml", "with_roles.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[
|
||||
"-i",
|
||||
os.path.join(FIXTURES_DIR, "inventory"),
|
||||
"--include-role-tasks",
|
||||
],
|
||||
)
|
||||
_common_tests(mermaid_path, playbook_paths)
|
|
@ -6,7 +6,7 @@ from ansible.utils.display import Display
|
|||
|
||||
from ansibleplaybookgrapher import PlaybookParser
|
||||
from ansibleplaybookgrapher.cli import PlaybookGrapherCLI
|
||||
from ansibleplaybookgrapher.graph import (
|
||||
from ansibleplaybookgrapher.graph_model import (
|
||||
TaskNode,
|
||||
BlockNode,
|
||||
RoleNode,
|
||||
|
|
Loading…
Reference in a new issue