├── .editorconfig ├── .github └── workflows │ ├── python-publish.yml │ └── python_application.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pyup.yml ├── .readthedocs.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── README.rst ├── _static │ └── vro-package-diff-sample.png ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── make.bat ├── modules.rst └── vro_package_diff.rst ├── requirements-dev.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── data │ ├── __init__.py │ ├── package_v1.0.package │ ├── package_v1.1.package │ └── test_vro_package_diff.py ├── tox.ini └── vro_package_diff ├── __init__.py ├── __main__.py ├── config.py └── vro_element.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.8' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build 21 | run: | 22 | python setup.py sdist bdist_wheel 23 | - name: Publish 24 | env: 25 | TWINE_USERNAME: __token__ 26 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 27 | run: | 28 | twine upload dist/* -------------------------------------------------------------------------------- /.github/workflows/python_application.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Set up Python 3.9 11 | uses: actions/setup-python@v1 12 | with: 13 | python-version: 3.9 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install -r requirements_dev.txt 18 | pip install . 19 | - name: Check endpoint 20 | run: | 21 | which vro-diff 22 | 23 | test: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | max-parallel: 4 27 | matrix: 28 | os: 29 | - ubuntu-latest 30 | - macos-latest 31 | - windows-2016 32 | - windows-latest 33 | python-version: [3.7, 3.8, 3.9] 34 | steps: 35 | - uses: actions/checkout@v1 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v1 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install -r requirements_dev.txt 44 | pip install tox-gh-actions 45 | pip install . 46 | - name: Test with tox 47 | env: 48 | PYTHONIOENCODING: utf-8 49 | run: tox 50 | 51 | documentation: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v1 55 | - name: Set up Python 3.9 56 | uses: actions/setup-python@v1 57 | with: 58 | python-version: 3.9 59 | - name: Install dependencies 60 | run: | 61 | python -m pip install --upgrade pip 62 | pip install -r requirements_dev.txt 63 | pip install . 64 | - name: Build sphinx documentation 65 | run: | 66 | make docs 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE 105 | .vscode/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://gitlab.com/pycqa/flake8 3 | rev: 3.7.9 4 | hooks: 5 | - id: flake8 6 | 7 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # configure updates globally 2 | # default: all 3 | # allowed: all, insecure, False 4 | update: insecure 5 | 6 | # configure dependency pinning globally 7 | # default: True 8 | # allowed: True, False 9 | pin: False 10 | 11 | # update schedule 12 | # default: empty 13 | # allowed: "every day", "every week", .. 14 | schedule: "every week" 15 | 16 | # search for requirement files 17 | # default: True 18 | # allowed: True, False 19 | search: True 20 | 21 | # add a label to pull requests, default is not set 22 | # requires private repo permissions, even on public repos 23 | # default: empty 24 | label_prs: update 25 | 26 | # assign users to pull requests, default is not set 27 | # requires private repo permissions, even on public repos 28 | # default: empty 29 | assignees: 30 | - lrivallain 31 | 32 | # configure the branch prefix the bot is using 33 | # default: pyup- 34 | branch_prefix: pyup/ 35 | 36 | # allow to close stale PRs 37 | # default: True 38 | close_prs: True -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | version: 2 3 | python: 4 | version: 3.6 5 | install: 6 | - requirements: requirements_dev.txt 7 | - method: pip 8 | path: . -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Ludovic Rivallain 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Timo Sugliani: @tsugliani 14 | 15 | None yet. Why not be the first? 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/lrivallain/vro_package_diff/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | vro_package_diff could always use more documentation, whether as part of the 42 | official vro_package_diff docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/vro_package_diff/vro_package_diff/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `vro_package_diff` for local development. 61 | 62 | 1. Fork the `vro_package_diff` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/vro_package_diff.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv vro_package_diff 70 | $ cd vro_package_diff/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 vro_package_diff tests 83 | $ python setup.py test or pytest 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check 106 | https://travis-ci.org/lrivallain/vro_package_diff/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | $ pytest tests.test_vro_package_diff 115 | 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bump2version patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 6 | 2.2.3 (in progress) 7 | ------------------- 8 | 9 | Display the vro-diff version with a click option #48 10 | 11 | 12 | 2.2.2 (2020-12-15) 13 | ------------------ 14 | 15 | Add support for PolicyTemplate items #46 16 | 17 | 18 | 2.2.1 (2020-11-06) 19 | ------------------ 20 | 21 | Fix: Error in version comparaison #44 22 | 23 | 24 | 2.2.0 (2020-09-18) 25 | ------------------ 26 | 27 | Check for values in the configuration elements: if so, exit with failure status. 28 | 29 | Add a new flag to enable this check: 30 | 31 | :: 32 | 33 | -e, --empty-config Check for values in the configuration 34 | elements: if so, exit with failure status. 35 | 36 | 37 | 2.1.0 (2019-12-19) 38 | ------------------ 39 | 40 | Provide an option to export diff files to a specific folder when a conflict is detected 41 | 42 | Add a new option to specify a diff destination folder: 43 | 44 | :: 45 | 46 | -d, --diff PATH A folder where to generate unified diff 47 | files output 48 | 49 | 50 | 51 | 2.0.2 (2019-12-10) 52 | ------------------ 53 | 54 | Support for non UTF8 and non colorized output(s) 55 | 56 | Add two new flag to enable specific output formating: 57 | 58 | :: 59 | 60 | -a, --ascii Only use ASCII symbols to display results 61 | -b, --no_color Do not colorized the output 62 | 63 | 64 | 2.0.1 (2019-08-06) 65 | ------------------ 66 | 67 | MacOSX and Windows support 68 | 69 | Note for Windows users: 70 | 71 | Windows usage is supported with some limitations: 72 | 73 | - No colored output. 74 | 75 | - Currently I have no idea on how to fix this. 76 | 77 | - Some UTF-8 symbols used in output are only with some fonts like 78 | *DejaVu Sans Mono*. 79 | 80 | - In future, I will try to implement a version of script that do not 81 | request UTF-8 support to return results. 82 | 83 | 84 | 2.0.0 (2019-08-06) 85 | ------------------ 86 | 87 | vro-package-diff is now a Pypi hosted project 88 | 89 | Changes: 90 | 91 | - ``vro-package-diff`` is now a Pypi hosted project: 92 | `vro-package-diff` and so, can be installed with ``pip install`` 93 | command. 94 | - An endpoit ``vro-diff`` to access to the tool from any path location. 95 | - Usage of ```click``` to manage: 96 | 97 | - inputs packages 98 | - help 99 | - legend display 100 | - test feature 101 | 102 | - A *test* feature 103 | - Documentation is hosted on `vro-package-diff.readthedocs.io` 104 | - `Travis pipeline` 105 | 106 | .. vro-package-diff: https://pypi.org/project/vro-package-diff/ 107 | .. ``click``: https://click.palletsprojects.com/ 108 | .. vro-package-diff.readthedocs.io: https://vro-package-diff.readthedocs.io 109 | .. Travis pipeline: https://travis-ci.org/lrivallain/vro-package-diff/ 110 | 111 | 112 | 1.0.0 (2018-02-22) 113 | ------------------ 114 | 115 | * First release. 116 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ludovic Rivallain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 vro_package_diff tests setup.py 55 | 56 | test: ## run tests quickly with the default Python 57 | pytest 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source vro_package_diff -m pytest 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/vro_package_diff.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ vro_package_diff 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | 75 | servedocs: docs ## compile the docs watching for changes 76 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 77 | 78 | release: dist ## package and upload a release 79 | twine upload dist/* 80 | 81 | dist: clean ## builds source and wheel package 82 | python setup.py sdist 83 | python setup.py bdist_wheel 84 | ls -l dist 85 | 86 | install: clean ## install the package to the active Python's site-packages 87 | python setup.py install 88 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | vRO-package-diff tool 2 | ===================== 3 | 4 | |PyPI version shields.io| |PyPI pyversions| |GitHub actions build status| |Travis build status| 5 | |Documentation status| |GitHub| |Fossa Status| 6 | 7 | Project description 8 | ------------------- 9 | 10 | vRO-package-diff is a Python package to compare 2 VMware vRealize 11 | Orchestrator packages. 12 | 13 | It provides a table-formated diff of two packages: 14 | 15 | |Sample of output| 16 | 17 | It is also possible to export `unified diff`_ files for each supported 18 | element: 19 | 20 | :: 21 | 22 | tree -L 2 ./diff/ 23 | ./diff/ 24 | ├── conflict 25 | │ ├── action 26 | │ ├── configurationelement 27 | │ ├── resourceelement 28 | │ └── workflow 29 | ├── no_upgrade 30 | │ ├── action 31 | │ ├── configurationelement 32 | │ └── workflow 33 | └── upgrade 34 | ├── action 35 | ├── configurationelement 36 | ├── resourceelement 37 | └── workflow 38 | 39 | cat ./diff/conflict/action/af7b881d-ba59-40d0-8207-be9e9b2ae34d.diff 40 | 41 | .. code:: diff 42 | 43 | --- tests/data/package_v1.0.package - Action: this_is_action_a (0.0.1) 44 | +++ tests/data/package_v1.1.package - Action: this_is_action_a (0.0.1) 45 | @@ -13,7 +13,5 @@ 46 | // nothing, just for fun :) 47 | } 48 | 49 | -Plop; 50 | - 51 | System.debug("this_is_action_a stopped");]]> 52 | 53 | Installing 54 | ---------- 55 | 56 | Install and update using pip: 57 | 58 | :: 59 | 60 | pip install vro-package-diff 61 | 62 | vRO-package-diff supports Python 3.5 and newer. 63 | 64 | 65 | Test installation 66 | ----------------- 67 | 68 | :: 69 | vro-diff -V 70 | 71 | This should produce an output with the current installed version of ``vro-package-diff``. 72 | 73 | 74 | Usage example 75 | ------------- 76 | 77 | :: 78 | 79 | vro-diff --legend --reference_package tests/data/package_v1.0.package tests/data/package_v1.1.package 80 | 81 | CLI help 82 | ~~~~~~~~ 83 | 84 | You can get the usage help by using the ``-h``/``--help`` flag: 85 | 86 | :: 87 | 88 | vro-diff -h 89 | 90 | Usage: vro-diff [OPTIONS] COMPARED_PACKAGE 91 | 92 | Compare two vRealize Orchestrator packages. 93 | 94 | Use the [-r/--reference_package] option to specify the reference package. 95 | 96 | Options: 97 | -r, --reference_package FILENAME 98 | Reference package to compare your package 99 | with. [required] 100 | -l, --legend Display the legend after the diff table 101 | -t, --test Exit with `0` if package can be safely 102 | imported. Else, returns the number of errors 103 | -a, --ascii Only use ASCII symbols to display results 104 | -b, --no_color Do not colorized the output 105 | -d, --diff PATH A folder where to generate unified diff 106 | files output 107 | -e, --empty-config Check for values in the configuration 108 | elements: if so, exit with failure status. 109 | -h, --help Show this message and exit. 110 | 111 | 112 | .. _unified diff: https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html 113 | 114 | .. |PyPI version shields.io| image:: https://img.shields.io/pypi/v/vro-package-diff.svg 115 | :target: https://pypi.python.org/pypi/vro-package-diff/ 116 | .. |PyPI pyversions| image:: https://img.shields.io/pypi/pyversions/vro-package-diff.svg 117 | :target: https://pypi.python.org/pypi/vro-package-diff/ 118 | .. |GitHub actions build status| image:: https://github.com/lrivallain/vro-package-diff/workflows/Python%20application/badge.svg 119 | :target: https://github.com/lrivallain/vro-package-diff/actions 120 | .. |Travis build status| image:: https://travis-ci.org/lrivallain/vro-package-diff.svg?branch=master 121 | :target: https://travis-ci.org/lrivallain/vro-package-diff 122 | .. |Documentation status| image:: https://readthedocs.org/projects/vro_package_diff/badge/?version=latest 123 | :target: https://vro_package_diff.readthedocs.io/en/latest/?badge=latest 124 | .. |GitHub| image:: https://img.shields.io/github/license/lrivallain/vro-package-diff 125 | .. |Sample of output| image:: ./docs/_static/vro-package-diff-sample.png 126 | .. |Fossa Status| image:: https://app.fossa.com/api/projects/git%2Bgithub.com%2Flrivallain%2Fvro-package-diff.svg?type=shield 127 | 128 | 129 | License 130 | ~~~~~~~ 131 | 132 | |Fossa Status large| 133 | 134 | .. |Fossa Status large| image:: https://app.fossa.com/api/projects/git%2Bgithub.com%2Flrivallain%2Fvro-package-diff.svg?type=large 135 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = vro_package_diff 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/_static/vro-package-diff-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrivallain/vro-package-diff/04de43b471d4c56cf96e865f670bd9c552b1e9d5/docs/_static/vro-package-diff-sample.png -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'vro-package-diff' 21 | copyright = 'MIT License, 2019, Ludovic Rivallain' 22 | author = 'Ludovic Rivallain' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | import vro_package_diff 26 | release = vro_package_diff.__version__ 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.napoleon', 41 | 'sphinx.ext.doctest', 42 | 'sphinx.ext.todo', 43 | 'sphinx.ext.autosummary', 44 | 'sphinx.ext.extlinks', 45 | 'sphinx.ext.viewcode' 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = ['.rst'] 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # 63 | # This is also used if you do content translation via gettext catalogs. 64 | # Usually you set "language" from the command line for these cases. 65 | language = None 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = "monokai" 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'sphinx_rtd_theme' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | html_theme_options = { 87 | 'collapse_navigation': False, 88 | 'sticky_navigation': True, 89 | 'navigation_depth': 4, 90 | 'includehidden': True, 91 | 'titles_only': False 92 | } 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | # html_static_path = ['_static'] 98 | 99 | # Custom sidebar templates, must be a dictionary that maps document names 100 | # to template names. 101 | # 102 | # The default sidebars (for documents that don't match any pattern) are 103 | # defined by theme itself. Builtin themes are using these templates by 104 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 105 | # 'searchbox.html']``. 106 | # html_sidebars = {'index': ['globaltoc.html','indexsidebar.html', 'searchbox.html']} 107 | 108 | # If given, this must be the name of an image file (path relative to the 109 | # configuration directory) that is the favicon of the docs. Modern browsers 110 | # use this as the icon for tabs, windows and bookmarks. It should be a 111 | # Windows-style icon file (.ico), which is 16x16 or 32x32 pixels large. 112 | # Default: None. 113 | # 114 | # New in version 0.4: The image file will be copied to the _static directory 115 | # of the output HTML, but only if the file does not already exist there. 116 | # html_favicon = "./favicon.png" 117 | 118 | # If true, “Created using Sphinx” is shown in the HTML footer. Default is True. 119 | html_show_sphinx = False 120 | 121 | # -- Options for HTMLHelp output --------------------------------------------- 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = 'vro_package_diff' 125 | 126 | # -- Extension configuration ------------------------------------------------- 127 | # This value selects if automatically documented members are sorted alphabetical 128 | # (value 'alphabetical'), by member type (value 'groupwise') or by source order 129 | # (value 'bysource'). The default is alphabetical. 130 | autodoc_member_order = 'bysource' 131 | 132 | # If true, the current module name will be prepended to all description 133 | # unit titles (such as .. function::). 134 | add_module_names = False -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to vro_package_diff's documentation! 2 | ============================================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | modules 10 | contributing 11 | authors 12 | history 13 | 14 | Indices and tables 15 | ================== 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | vro_package_diff 2 | ================ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | vro_package_diff 8 | -------------------------------------------------------------------------------- /docs/vro_package_diff.rst: -------------------------------------------------------------------------------- 1 | vro\_package\_diff package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | vro\_package\_diff.config module 8 | -------------------------------- 9 | 10 | .. automodule:: vro_package_diff.config 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | vro\_package\_diff.vro\_element module 16 | -------------------------------------- 17 | 18 | .. automodule:: vro_package_diff.vro_element 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: vro_package_diff 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip 2 | bump2version 3 | wheel 4 | watchdog 5 | tox 6 | coverage 7 | Sphinx 8 | twine 9 | ptpython 10 | pytest 11 | pytest-runner 12 | flake8 13 | flake8-bugbear 14 | flake8-colors 15 | flake8-docstrings 16 | flake8-import-order 17 | pep8-naming 18 | sphinx_rtd_theme -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:vro_package_diff/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | max-line-length = 119 20 | 21 | [aliases] 22 | # Define setup.py command aliases here 23 | test = pytest 24 | 25 | [tool:pytest] 26 | collect_ignore = ['setup.py'] 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | # default python modules 7 | from setuptools import find_packages, setup 8 | 9 | # local imports 10 | import vro_package_diff 11 | 12 | 13 | with open('README.rst') as readme_file: 14 | readme = readme_file.read() 15 | 16 | with open('HISTORY.rst') as history_file: 17 | history = history_file.read() 18 | 19 | requirements = [ 20 | "terminaltables", # Pretty print a table 21 | "colored", # A bit of colors from fancy term 22 | "click", # CLI arguments management 23 | "packaging" # Used to compare versions numbers 24 | ] 25 | 26 | setup_requirements = [ 27 | 'pytest-runner' 28 | ] 29 | 30 | test_requirements = [ 31 | 'pytest>=3' 32 | ] 33 | 34 | description = "A diff tool for VMware vRealize Orchestrator packages files" 35 | 36 | setup( 37 | name='vro_package_diff', 38 | version=vro_package_diff.__version__, 39 | author="Ludovic Rivallain", 40 | author_email='ludovic.rivallain@gmail.com', 41 | python_requires='>=3.5', 42 | packages=find_packages(include=['vro_package_diff', 'vro_package_diff.*']), 43 | description=description, 44 | long_description=readme + '\n\n' + history, 45 | long_description_content_type='text/x-rst', 46 | include_package_data=True, 47 | install_requires=requirements, 48 | setup_requires=setup_requirements, 49 | test_suite='tests', 50 | tests_require=test_requirements, 51 | url='https://github.com/lrivallain/vro_package_diff', 52 | classifiers=[ 53 | 'Programming Language :: Python :: 3.5', 54 | 'Programming Language :: Python :: 3.6', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Programming Language :: Python :: 3.7', 57 | 'Programming Language :: Python :: 3.8', 58 | 'Programming Language :: Python :: 3.9', 59 | 'Operating System :: MacOS :: MacOS X', 60 | 'Operating System :: Microsoft :: Windows', 'Operating System :: Unix', 61 | 'Operating System :: POSIX :: Linux', 62 | "License :: OSI Approved :: MIT License", "Environment :: Console" 63 | ], 64 | entry_points={ 65 | 'console_scripts': [ 66 | 'vro-diff=vro_package_diff.__main__:main', 67 | ], 68 | }) 69 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for vro_package_diff.""" 4 | -------------------------------------------------------------------------------- /tests/data/package_v1.0.package: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrivallain/vro-package-diff/04de43b471d4c56cf96e865f670bd9c552b1e9d5/tests/data/package_v1.0.package -------------------------------------------------------------------------------- /tests/data/package_v1.1.package: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrivallain/vro-package-diff/04de43b471d4c56cf96e865f670bd9c552b1e9d5/tests/data/package_v1.1.package -------------------------------------------------------------------------------- /tests/data/test_vro_package_diff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `vro_package_diff` package.""" 5 | 6 | import pytest 7 | # import vro_package_diff 8 | 9 | 10 | @pytest.fixture 11 | def response(): 12 | """Sample pytest fixture. 13 | 14 | See more at: http://doc.pytest.org/en/latest/fixture.html 15 | """ 16 | # import requests 17 | # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') 18 | 19 | 20 | def test_content(response): 21 | """Sample pytest test function with the pytest fixture as an argument.""" 22 | # from bs4 import BeautifulSoup 23 | # assert 'GitHub' in BeautifulSoup(response.content).title.string 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, flake8, build, readme 3 | 4 | [travis] 5 | python = 6 | 3.8: py38, flake8, build, readme 7 | 3.7: py37 8 | 3.6: py36 9 | 3.5: py35 10 | 11 | [gh-actions] 12 | python = 13 | 3.8: py38, flake8, build, readme 14 | 3.7: py37 15 | 3.6: py36 16 | 3.5: py35 17 | 18 | [testenv] 19 | setenv = 20 | PYTHONPATH = {toxinidir} 21 | PYTHONIOENCODING = utf-8 22 | deps = 23 | -r{toxinidir}/requirements_dev.txt 24 | commands = 25 | pytest --basetemp={envtmpdir} 26 | vro-diff --help 27 | vro-diff --legend -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package 28 | - vro-diff --test -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package # || if [[ $? -eq 5 ]]; then true; else false; fi 29 | - vro-diff --empty-config -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package # test empty configElements 30 | vro-diff -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package -a # ASCII only 31 | vro-diff -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package -b # Uncolorized 32 | vro-diff -r {toxinidir}/tests/data/package_v1.0.package {toxinidir}/tests/data/package_v1.1.package -d /tmp/testdiff/ # diff file generation 33 | 34 | [testenv:flake8] 35 | skip_install = true 36 | setenv = 37 | basepython = python 38 | deps = 39 | -r{toxinidir}/requirements_dev.txt 40 | format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s 41 | commands = flake8 vro_package_diff setup.py 42 | 43 | [testenv:build] 44 | basepython = python3 45 | skip_install = true 46 | deps = 47 | wheel 48 | setuptools 49 | commands = 50 | python setup.py -q sdist bdist_wheel 51 | 52 | [testenv:readme] 53 | skip_install = true 54 | deps = 55 | {[testenv:build]deps} 56 | twine 57 | commands = 58 | {[testenv:build]commands} 59 | twine check dist/* 60 | -------------------------------------------------------------------------------- /vro_package_diff/__init__.py: -------------------------------------------------------------------------------- 1 | """Provide a table-formated diff of two VMware vRealize Orchestrator packages. 2 | 3 | .. moduleauthor:: Ludovic Rivallain gmail.com> 4 | 5 | """ 6 | 7 | import sys 8 | if sys.version_info < (3, 5): 9 | raise Exception('vRO package diff tool requires Python versions 3.5 or later.') 10 | 11 | __all__ = [ 12 | 'config' 13 | 'vro_element', 14 | ] 15 | 16 | __version__ = "2.2.3" 17 | """Define the version of the package. 18 | """ 19 | -------------------------------------------------------------------------------- /vro_package_diff/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Provide a table-formated diff of two VMware vRealize Orchestrator packages.""" 3 | 4 | # default python modules 5 | import logging 6 | import os 7 | import platform 8 | import zipfile 9 | from difflib import unified_diff 10 | 11 | # external modules 12 | import click 13 | 14 | from terminaltables import AsciiTable, SingleTable 15 | 16 | # local imports 17 | from . import __version__ 18 | from .config import CLI_CONTEXT_SETTINGS, LOGGING_FILE, LOGGING_LEVEL_FILE, OUTPUT_SETUP, SUPPORTED_ELEMENT_TYPES 19 | from .vro_element import VROElementMetadata 20 | 21 | # Windows trick: no colored output 22 | if platform.system().lower() == "windows": 23 | def stylize(text: str, color: str): 24 | """No color for windows users: sorry. 25 | 26 | Args: 27 | text (str): Text to print 28 | color (str): Unused color for text 29 | 30 | Returns: 31 | str: Text from input 32 | """ 33 | return text 34 | else: 35 | from colored import stylize 36 | 37 | # Configure logger 38 | logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', 39 | datefmt='%Y/%m/%d %H:%M:%S', 40 | filename=LOGGING_FILE, 41 | filemode='w', 42 | level=LOGGING_LEVEL_FILE) 43 | logger = logging.getLogger(__name__) 44 | 45 | 46 | def _stylize(text: str, color: str = "", colorized: bool = True): 47 | """Print colored or uncolored text. 48 | 49 | Args: 50 | text (str): Text to print 51 | color (str): Color to use 52 | colorized (bool, optional): Use color or not?. Defaults to True. 53 | """ 54 | if not colorized: 55 | return text 56 | else: 57 | return stylize(text, color) 58 | 59 | 60 | def get_vroitems_from_package(package): 61 | """Get all the items from the vRO Package. 62 | 63 | Args: 64 | package (str): Path to a package file. 65 | 66 | Returns: 67 | VROElementMetadata[]: a list of VROElementMetadata. 68 | """ 69 | vro_items_id, vro_items = [], [] 70 | with zipfile.ZipFile(package, 'r') as zip_ref: 71 | for x in zip_ref.namelist(): 72 | if x.startswith("elements"): 73 | item_id = os.path.basename(os.path.split(x)[0]) 74 | if item_id not in vro_items_id: 75 | with zip_ref.open('elements/' + item_id + '/info', 'r') as xml_info_file: 76 | xml_info = xml_info_file.read() 77 | with zip_ref.open('elements/' + item_id + '/data', 'r') as data_file: 78 | data = data_file.read() 79 | vro_item = VROElementMetadata(item_id, xml_info, data) 80 | vro_items.append(vro_item) 81 | vro_items_id.append(item_id) 82 | logger.info("New item %s" % vro_item) 83 | return vro_items 84 | 85 | 86 | def legend_print(ascii: bool = False, colorized: bool = True): 87 | """Print a legend at the end of diff table. 88 | 89 | Args: 90 | ascii (bool): Use ASCII for output or not? Defaults to False. 91 | colorized (bool, optional): Use color or not?. Defaults to True. 92 | """ 93 | data = [["Legend", '']] 94 | pretty_table = SingleTable(data) 95 | legend = "" 96 | symbol_mode = "symbol_utf8" 97 | if ascii: 98 | symbol_mode = "symbol_ascii" 99 | list_bullet = "*" # • 100 | for loi in OUTPUT_SETUP.keys(): 101 | legend += "{} {} {}\n".format( 102 | list_bullet, 103 | _stylize( 104 | OUTPUT_SETUP[loi].get(symbol_mode), 105 | OUTPUT_SETUP[loi].get('color'), 106 | colorized 107 | ), 108 | OUTPUT_SETUP[loi].get('legend') 109 | ) 110 | legend += "\nFor items with conflict:\n" 111 | legend += " ‣ Version in package file is lower than in the reference package.\n" 112 | legend += " ‣ If versions are the same, the content is not. Upgrade version on\n" 113 | legend += " compared package to overwrite item during the import process.\n" 114 | pretty_table.table_data[0][1] = legend 115 | print("\n%s" % pretty_table.table) 116 | 117 | 118 | def table_pprint(lists_of_items_by_state: dict, ascii: bool = False, colorized: bool = True): 119 | """Generate and print a pretty table for output information. 120 | 121 | Args: 122 | lists_of_items_by_state (dict of VROElementMetadata[]): A dict of items, stored by 123 | import state 124 | ascii (bool): Use ASCII for output or not? Defaults to False. 125 | colorized (bool, optional): Use color or not?. Defaults to True. 126 | """ 127 | data = [] 128 | title = "Diff betwenn packages" 129 | # Headers 130 | data.append(["ID", "Name", "Type", "Reference", "Package", "Result"]) 131 | symbol_mode = "symbol_utf8" 132 | if ascii: 133 | symbol_mode = "symbol_ascii" 134 | for loi in OUTPUT_SETUP.keys(): 135 | for element in lists_of_items_by_state.get(loi, []): 136 | data.append([ 137 | element.id, 138 | element.name, 139 | element.type, 140 | element.comp_version, # Reference version 141 | element.version, # Package version 142 | _stylize( 143 | OUTPUT_SETUP[loi].get(symbol_mode), 144 | OUTPUT_SETUP[loi].get('color'), 145 | colorized 146 | ) 147 | ]) 148 | if ascii: 149 | print(AsciiTable(data, title).table) 150 | else: 151 | print(SingleTable(data, title).table) 152 | 153 | 154 | def unexpected_values_pprint(lists_of_items_by_state: dict, ascii: bool = False): 155 | """Generate and print a pretty table for output information. 156 | 157 | Args: 158 | lists_of_items_by_state (dict of VROElementMetadata[]): A dict of items, stored by 159 | import state 160 | ascii (bool): Use ASCII for output or not? Defaults to False. 161 | colorized (bool, optional): Use color or not?. Defaults to True. 162 | """ 163 | if not lists_of_items_by_state['unexpected_values']: 164 | return 165 | data = [] 166 | title = "Unexpected values in configurationElements" 167 | # Headers 168 | data.append(["ID", "Name", "Type", "Package", "Nb values"]) 169 | for element in lists_of_items_by_state.get('unexpected_values', []): 170 | data.append([ 171 | element.id, 172 | element.name, 173 | element.type, 174 | element.version, 175 | element.valued_items 176 | ]) 177 | if ascii: 178 | print("\n" + AsciiTable(data, title).table) 179 | else: 180 | print("\n" + SingleTable(data, title).table) 181 | 182 | 183 | def create_diff_file(src_elt: bytes, dst_elt: bytes, src_name: str, dst_name: str, diff_folder: str, state: str): 184 | """Create a diff file between two versions of element data_content. 185 | 186 | Args: 187 | src_elt (VROElementMetadata): Primary content. 188 | dst_elt (VROElementMetadata): Destination content. 189 | src_name (str): Name of the source content. 190 | dst_name (str): Name of the destination content. 191 | diff_folder (str): Destination folder to store diff files. 192 | state (str): State of the current item (used for sub folder) 193 | """ 194 | if not (src_elt.dec_data_content and dst_elt.dec_data_content): 195 | logger.info("Ignoring (binary?) content for element with ID: %s" % src_elt.id) 196 | return 197 | logger.info("Creating a new diff file for element ID: %s" % src_elt.id) 198 | new_file_name = src_elt.id + ".diff" 199 | diff_folder_target = os.path.join(diff_folder, state, src_elt.type.lower()) 200 | if not os.path.isdir(diff_folder_target): 201 | logger.debug("Creating a missing diff target folder: %s" % diff_folder_target) 202 | os.makedirs(diff_folder_target, exist_ok=True) 203 | with open(os.path.join(diff_folder_target, new_file_name), 'w', encoding='utf-8') as output_f: 204 | for line in unified_diff( 205 | src_elt.dec_data_content.splitlines(keepends=True), 206 | dst_elt.dec_data_content.splitlines(keepends=True), 207 | fromfile="%s - %s: %s (%s)" % (src_name, src_elt.type, src_elt.name, src_elt.version), 208 | tofile="%s - %s: %s (%s)" % (dst_name, dst_elt.type, dst_elt.name, dst_elt.version), 209 | n=3, 210 | lineterm='\n'): 211 | output_f.write(line) 212 | logger.info("End of diff file generation for the element with ID: %s" % src_elt.id) 213 | 214 | 215 | def diff_vro_items(items_src, 216 | items_dst, 217 | reference_package: str, 218 | compared_package: str, 219 | ascii: bool = False, 220 | colorized: bool = True, 221 | diff_folder: bool = None, 222 | empty_config: bool = True): 223 | """Compare two vRO items lists. 224 | 225 | Args: 226 | items_src (VROElementMetadata[]): Original list of vRO items. 227 | items_dst (VROElementMetadata[]): Destination list of vRO items. 228 | reference_package (str): package to use as source. 229 | compared_package (str): package to compare with reference one. 230 | ascii (bool): Use ASCII for output or not? Defaults to False. 231 | colorized (bool, optional): Use color or not?. Defaults to True. 232 | diff_folder (str, optional): Generate unified diff files output. Defaults to None. 233 | """ 234 | lists_of_items_by_state = { 235 | 'no_upgrade': [], 236 | 'upgrade': [], 237 | 'conflict': [], 238 | 'new': [], 239 | 'unsupported': [], 240 | 'unexpected_values': [] 241 | } 242 | for idst in items_dst: 243 | found = False 244 | if idst.type not in SUPPORTED_ELEMENT_TYPES: 245 | state = 'unsupported' 246 | else: 247 | for isrc in items_src: 248 | if isrc.id == idst.id: 249 | logger.debug("%s is IN source package" % idst) 250 | found = True 251 | idst.comp_version = isrc.version 252 | if idst.version == "n/a": 253 | state = 'unsupported' 254 | elif idst.version > isrc.version: 255 | state = 'upgrade' 256 | elif idst.version < isrc.version: 257 | state = 'conflict' 258 | logger.warning("Conflict detected on item: %s" % idst) 259 | elif idst.version == isrc.version: 260 | if idst.checksum == isrc.checksum: 261 | state = 'no_upgrade' 262 | else: 263 | state = 'conflict' 264 | logger.warning("Conflict detected on item: %s" % idst) 265 | if diff_folder: 266 | create_diff_file( 267 | isrc, 268 | idst, 269 | src_name=reference_package, 270 | dst_name=compared_package, 271 | diff_folder=diff_folder, 272 | state=state 273 | ) 274 | if (not found) and (idst.type in SUPPORTED_ELEMENT_TYPES): 275 | logger.debug("%s is NOT IN source package" % idst) 276 | state = 'new' 277 | if idst.type == "ConfigurationElement" and empty_config: 278 | if idst.count_values_from_configuration_elt(): 279 | lists_of_items_by_state['unexpected_values'].append(idst) 280 | lists_of_items_by_state[state].append(idst) 281 | logger.info("File A: %d elements" % len(items_src)) 282 | logger.info("File B: %d elements" % len(items_dst)) 283 | logger.info("Items to upgrade:\t\t%d" % len(lists_of_items_by_state['upgrade'])) 284 | logger.info("Items without upgrade:\t%d" % len(lists_of_items_by_state['no_upgrade'])) 285 | logger.info("Items in upgrade conflict:\t%d" % len(lists_of_items_by_state['conflict'])) 286 | logger.info("New items:\t\t\t%d" % len(lists_of_items_by_state['new'])) 287 | logger.info("ConfigurationElements with values:\t\t\t%d" % len(lists_of_items_by_state['unexpected_values'])) 288 | logger.warning("Unsupported items:\t\t%d" % len(lists_of_items_by_state['unsupported'])) 289 | total = ( 290 | len(lists_of_items_by_state['unsupported']) 291 | + len(lists_of_items_by_state['upgrade']) 292 | + len(lists_of_items_by_state['no_upgrade']) 293 | + len(lists_of_items_by_state['conflict']) 294 | + len(lists_of_items_by_state['new']) 295 | ) 296 | logger.info("Total items:\t\t\t%s" % total) 297 | table_pprint(lists_of_items_by_state, ascii=ascii, colorized=colorized) 298 | return lists_of_items_by_state 299 | 300 | 301 | @click.command(context_settings=CLI_CONTEXT_SETTINGS) 302 | @click.version_option(__version__) 303 | @click.option('-r', '--reference_package', 304 | help="Reference package to compare your package with.", 305 | type=click.File('rb'), 306 | required=True) 307 | @click.argument('compared_package', 308 | type=click.File('rb')) 309 | @click.option('-l', '--legend', 310 | is_flag=True, 311 | help="Display the legend after the diff table") 312 | @click.option('-t', '--test', 313 | is_flag=True, 314 | help="Exit with `0` if package can be safely imported. Else, returns the number of errors") 315 | @click.option('-a', '--ascii', 316 | is_flag=True, 317 | help="Only use ASCII symbols to display results") 318 | @click.option('-b', '--no_color', 319 | is_flag=True, 320 | help="Do not colorized the output") 321 | @click.option('-d', '--diff', 322 | type=click.Path(dir_okay=True, resolve_path=True), 323 | help="A folder where to generate unified diff files output") 324 | @click.option('-e', '--empty-config', 325 | is_flag=True, 326 | help="Check for values in the configuration elements: if so, exit with failure status.") 327 | def cli(reference_package: str, compared_package: str, legend: bool = False, 328 | test: bool = False, ascii: bool = False, no_color: bool = False, diff: str = None, 329 | empty_config: bool = False): 330 | """Compare two vRealize Orchestrator packages. 331 | 332 | Use the [-r/--reference_package] option to specify the reference package. 333 | """ 334 | logger.info("Reading items from the source package") 335 | vro_items_src = get_vroitems_from_package(reference_package) 336 | logger.info("Reading items from the destination package") 337 | vro_items_dst = get_vroitems_from_package(compared_package) 338 | logger.info("Starting the comparison of both contents") 339 | lists_of_items_by_state = diff_vro_items( 340 | vro_items_src, 341 | vro_items_dst, 342 | ascii=ascii, 343 | colorized=not no_color, 344 | diff_folder=diff, 345 | reference_package=reference_package.name, 346 | compared_package=compared_package.name 347 | ) 348 | if legend: 349 | logger.info("Legend display was requested.") 350 | legend_print(ascii=ascii, colorized=not no_color) 351 | if diff: 352 | logger.info("Unified diff files are stored in: %s" % diff) 353 | print("Unified diff files are stored in: %s" % diff) 354 | exit_code = 0 355 | if test: 356 | logger.info("Exiting with number of conflicts:" + str(len(lists_of_items_by_state['conflict']))) 357 | exit_code += len(lists_of_items_by_state['conflict']) 358 | if empty_config: 359 | unexpected_values_pprint(lists_of_items_by_state, ascii=ascii) 360 | logger.info("Exiting with number of values in configurationElements") 361 | exit_code += len(lists_of_items_by_state['unexpected_values']) 362 | logger.info("End of execution of the diff tool for vRO packages.") 363 | exit(exit_code) 364 | 365 | 366 | def main(): 367 | """Start the main diff process.""" 368 | logger.info("Starting the diff tool for vRO packages.") 369 | cli(obj={}) 370 | 371 | 372 | if __name__ == '__main__': 373 | main() 374 | -------------------------------------------------------------------------------- /vro_package_diff/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Some config items.""" 3 | 4 | # default python modules 5 | import logging 6 | 7 | # external modules 8 | import colored 9 | 10 | LOGGING_LEVEL_FILE = logging.DEBUG 11 | """int: Log level to use.""" 12 | 13 | LOGGING_FILE = "diff.log" 14 | """str: Log file location.""" 15 | 16 | SUPPORTED_ELEMENT_TYPES = [ 17 | "Workflow", 18 | "ScriptModule", 19 | "Action", 20 | "ResourceElement", 21 | "ConfigurationElement", 22 | "PolicyTemplate" 23 | ] 24 | """list: Currently support is limited to the following types of items.""" 25 | 26 | OUTPUT_SETUP = { 27 | 'no_upgrade': { 28 | 'symbol_utf8': ' ⇄ ', 29 | 'symbol_ascii': '<->', 30 | 'color': colored.fg("turquoise_2"), 31 | 'legend': "Items with no upgrade required" 32 | }, 33 | 'upgrade': { 34 | 'symbol_utf8': ' ⇉ ', 35 | 'symbol_ascii': '==>', 36 | 'color': colored.fg("chartreuse_2a"), 37 | 'legend': "Items that will be upgraded in import process" 38 | }, 39 | 'new': { 40 | 'symbol_utf8': ' ⊕ ', 41 | 'symbol_ascii': '[+]', 42 | 'color': colored.fg("yellow_1"), 43 | 'legend': "New items (will be imported)" 44 | }, 45 | 'unsupported': { 46 | 'symbol_utf8': ' ⇄ ', 47 | 'symbol_ascii': ' ? ', 48 | 'color': colored.fg("grey_78"), 49 | 'legend': "Items ignored in the vRO merge process" 50 | }, 51 | 'conflict': { 52 | 'symbol_utf8': ' ≠ ', 53 | 'symbol_ascii': '=/=', 54 | 'color': colored.fg("red_1"), 55 | 'legend': "Items with a version conflict" 56 | }, 57 | } 58 | """dict: Define the configuration of output display (color and symbols)""" 59 | 60 | CLI_CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 61 | """dict: Click module settings to add two way to get help.""" 62 | -------------------------------------------------------------------------------- /vro_package_diff/vro_element.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Define VROElementMetadata object class.""" 3 | 4 | # default python modules 5 | import hashlib 6 | import io 7 | import logging 8 | import xml.etree.ElementTree as Etree 9 | import zipfile 10 | 11 | # third Party 12 | from packaging import version 13 | 14 | # local imports 15 | from .config import SUPPORTED_ELEMENT_TYPES 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class VROElementMetadata(): 22 | """Abstract class to represent vRealize Orchestrator elements extracted from a vRO package.""" 23 | 24 | def __init__(self, id: str, xml_info: bytes, data_content: bytes): 25 | """Build a new VROElementMetadata object from id, xml_info, data_content. 26 | 27 | Args: 28 | id (str): Object ID (from the folder name in zip-package file). 29 | xml_info (bytes): info file content. 30 | data_content (bytes): data file content (could be a nested zip file or an XML one). 31 | """ 32 | self.name = None # populated with self.read_data later 33 | self.type = None # populated with self.read_data later 34 | self.version = version.parse("0.0.0") # populated with self.read_data later 35 | self.dec_data_content = None # populated with self.read_data later 36 | self.valued_items = 0 # populated in count_values_from_configuration_elt later 37 | self.id = id 38 | self.type = self.get_item_type(xml_info) 39 | self.comp_version = None 40 | if self.type in SUPPORTED_ELEMENT_TYPES: 41 | self.data_content = data_content 42 | self.read_data() 43 | self.checksum = hashlib.sha1(data_content).hexdigest() 44 | 45 | def __str__(self): 46 | """Define the string representation for object VROElementMetadata. 47 | 48 | Returns: 49 | str: string representation 50 | """ 51 | return "[%s]%s" % (self.type, self.id) 52 | 53 | def get_item_type(self, xml_str: bytes): 54 | """Get the item type. 55 | 56 | Args: 57 | xml_str (bytes): The XML content for item info. 58 | 59 | Returns: 60 | str: The type name. 61 | """ 62 | root = Etree.fromstring(xml_str) 63 | for x in root.findall('entry'): 64 | if x.get('key') == "type": 65 | raw_type = x.text 66 | if raw_type in SUPPORTED_ELEMENT_TYPES: 67 | if raw_type == 'ScriptModule': 68 | return "Action" # rename scriptmodule --> action 69 | return raw_type 70 | else: 71 | logger.warning("Unsupported element type for item: %s (%s)" % (self.id, raw_type)) 72 | return "Unsupported" 73 | 74 | def u_decode_plain_content(self): 75 | """UTF-16 or UTF-8 decoding of plain files. 76 | 77 | Returns: 78 | str: a decoded version of the input data. 79 | """ 80 | try: 81 | dec_data = self.data_content.decode('utf-16-be') 82 | logger.debug("UTF-16 decoding for item %s" % self.id) 83 | except UnicodeDecodeError: 84 | try: 85 | dec_data = self.data_content.decode('utf-8') 86 | logger.debug("UTF-8 decoding failed for item %s" % self.id) 87 | except UnicodeDecodeError: 88 | logger.error("Both UTF-16 and UTF-8 decoding failed for item %s" % self.id) 89 | dec_data = None 90 | return dec_data 91 | 92 | def read_data(self): 93 | """Read data content to extract object name. 94 | 95 | Populate self.name, self.version and self.type. 96 | """ 97 | self.name = "Unsupported: %s" % self.type # default value 98 | self.version = "n/a" # default value 99 | # specific case of nested zip file for resourcesElements 100 | if self.type == "ResourceElement": 101 | with zipfile.ZipFile(io.BytesIO(self.data_content), 'r') as zip_data: 102 | with zip_data.open('VSO-RESOURCE-INF/attribute_name', 'r') as name_file: 103 | self.name = name_file.read().decode('utf-8') 104 | try: 105 | with zip_data.open('VSO-RESOURCE-INF/attribute_version', 'r') as version_file: 106 | _version = version_file.read().decode('utf-8') 107 | except KeyError: 108 | _version = "0.0.0" 109 | self.version = version.parse(_version) 110 | with zip_data.open('VSO-RESOURCE-INF/data', 'r') as data_file: 111 | self.data_content = data_file.read() 112 | self.dec_data_content = self.u_decode_plain_content() 113 | elif self.type in SUPPORTED_ELEMENT_TYPES: 114 | self.dec_data_content = self.u_decode_plain_content() 115 | root = Etree.fromstring(self.dec_data_content) 116 | _version = root.get('version', "0.0.0") 117 | if self.type == 'Workflow': 118 | namespaces = {'workflow': 'http://vmware.com/vco/workflow'} 119 | self.name = root.find('workflow:display-name', namespaces).text 120 | elif self.type == 'Action' or self.type == "ScriptModule" or self.type == "PolicyTemplate": 121 | self.name = root.get('name') 122 | elif self.type == 'ConfigurationElement': 123 | self.name = root.find('display-name').text 124 | self.version = version.parse(_version) 125 | 126 | def count_values_from_configuration_elt(self): 127 | """Count the number of values found in a configurationElement. 128 | 129 | Returns: 130 | int: number of values found in the configurationElement items. 131 | """ 132 | if not self.type == 'ConfigurationElement': 133 | logger.warn("Invalid type to count values in") 134 | return 0 135 | self.dec_data_content = self.u_decode_plain_content() 136 | root = Etree.fromstring(self.dec_data_content) 137 | atts = root.find('atts') 138 | for att in atts.findall('att'): 139 | if att.find('value') is not None: 140 | self.valued_items += 1 141 | logger.debug("Found %d values in %s" % (self.valued_items, self.name)) 142 | return self.valued_items 143 | --------------------------------------------------------------------------------