├── .flake8 ├── .github └── workflows │ ├── release.yml │ ├── releaser-pleaser.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── molecule_hetznercloud ├── __init__.py ├── _version.py ├── cookiecutter │ ├── cookiecutter.json │ └── {{cookiecutter.molecule_directory}} │ │ └── {{cookiecutter.scenario_name}} │ │ ├── INSTALL.rst │ │ ├── converge.yml │ │ └── molecule.yml ├── driver.json ├── driver.py └── playbooks │ ├── create.yml │ ├── destroy.yml │ └── filter_plugins │ └── get_platforms_data.py ├── pyproject.toml ├── renovate.json ├── requirements.yml ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── functional │ ├── __init__.py │ ├── fixtures │ │ ├── __init__.py │ │ └── test_init_scenario │ │ │ ├── meta │ │ │ └── main.yml │ │ │ └── tasks │ │ │ └── main.yml │ └── test_hetznercloud.py ├── integration │ ├── molecule │ │ └── default │ │ │ ├── converge.yml │ │ │ ├── molecule.yml │ │ │ └── verify.yml │ └── test_integration.py └── unit │ ├── __init__.py │ ├── test_driver.py │ └── test_plugins.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = 3 | E501 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v6 18 | with: 19 | python-version: 3.x 20 | cache: pip 21 | 22 | - name: Install dependencies 23 | run: pip install build twine 24 | 25 | - name: Build 26 | run: python3 -m build 27 | 28 | - name: Check 29 | run: twine check --strict dist/* 30 | 31 | - name: Upload packages artifact 32 | if: github.event_name == 'release' 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: python-packages 36 | path: dist/ 37 | 38 | publish: 39 | if: github.event_name == 'release' 40 | 41 | environment: 42 | name: pypi 43 | url: https://pypi.org/p/molecule-hetznercloud 44 | permissions: 45 | id-token: write 46 | 47 | runs-on: ubuntu-latest 48 | needs: [build] 49 | steps: 50 | - name: Download packages artifact 51 | uses: actions/download-artifact@v5 52 | with: 53 | name: python-packages 54 | path: dist/ 55 | 56 | - name: Publish packages to PyPI 57 | uses: pypa/gh-action-pypi-publish@v1.13.0 58 | -------------------------------------------------------------------------------- /.github/workflows/releaser-pleaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser-pleaser 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request_target: 7 | types: 8 | - edited 9 | - labeled 10 | - unlabeled 11 | 12 | concurrency: 13 | group: releaser-pleaser 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | releaser-pleaser: 18 | # Do not run on forks. 19 | if: github.repository == 'ansible-community/molecule-hetznercloud' 20 | 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: releaser-pleaser 24 | uses: apricote/releaser-pleaser@v0.7.1 25 | with: 26 | token: ${{ secrets.HCLOUD_BOT_TOKEN }} 27 | extra-files: | 28 | molecule_hetznercloud/_version.py 29 | setup.py 30 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: "30 12 * * *" 6 | 7 | jobs: 8 | stale: 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | uses: hetznercloud/.github/.github/workflows/stale.yml@main 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | unit: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | python-version: ["3.10", "3.11", "3.12", "3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Setup python 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | make venv 27 | venv/bin/pip install tox tox-gh-actions 28 | 29 | - name: Run tests 30 | run: make test ARGS="--cov --cov-config=pyproject.toml --cov-report=xml" 31 | 32 | - name: Upload coverage reports to Codecov 33 | if: > 34 | !startsWith(github.head_ref, 'renovate/') && 35 | !startsWith(github.head_ref, 'releaser-pleaser--') 36 | uses: codecov/codecov-action@v5 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | files: coverage.xml 40 | flags: unit 41 | 42 | e2e: 43 | runs-on: ubuntu-latest 44 | 45 | strategy: 46 | matrix: 47 | python-version: ["3.10", "3.11", "3.12", "3.13"] 48 | 49 | permissions: 50 | id-token: write 51 | 52 | steps: 53 | - uses: hetznercloud/tps-action@main 54 | with: 55 | tps-token: ${{ secrets.TPS_TOKEN }} 56 | 57 | - uses: actions/checkout@v5 58 | 59 | - name: Setup python 60 | uses: actions/setup-python@v6 61 | with: 62 | python-version: ${{ matrix.python-version }} 63 | cache: pip 64 | 65 | - name: Install dependencies 66 | run: | 67 | make venv 68 | venv/bin/pip install tox tox-gh-actions 69 | 70 | - name: Run integrations 71 | run: make integration ARGS="--cov --cov-config=pyproject.toml --cov-report=xml" 72 | env: 73 | PY_COLORS: "1" 74 | ANSIBLE_FORCE_COLOR: "1" 75 | 76 | - name: Upload coverage reports to Codecov 77 | if: > 78 | !startsWith(github.head_ref, 'renovate/') && 79 | !startsWith(github.head_ref, 'releaser-pleaser--') 80 | uses: codecov/codecov-action@v5 81 | with: 82 | token: ${{ secrets.CODECOV_TOKEN }} 83 | files: coverage.xml 84 | flags: e2e 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ansible/ 2 | ansible_collections 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://pre-commit.com for more information 3 | # See https://pre-commit.com/hooks.html for more hooks 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-case-conflict 10 | - id: check-executables-have-shebangs 11 | - id: check-shebang-scripts-are-executable 12 | - id: check-symlinks 13 | - id: destroyed-symlinks 14 | 15 | - id: check-json 16 | - id: check-yaml 17 | - id: check-toml 18 | 19 | - id: check-merge-conflict 20 | - id: end-of-file-fixer 21 | - id: mixed-line-ending 22 | args: [--fix=lf] 23 | - id: trailing-whitespace 24 | 25 | - repo: https://github.com/pre-commit/mirrors-prettier 26 | rev: v3.1.0 27 | hooks: 28 | - id: prettier 29 | files: \.(md|ya?ml)$ 30 | exclude: ^CHANGELOG\.md$ 31 | 32 | - repo: https://github.com/asottile/pyupgrade 33 | rev: v3.21.0 34 | hooks: 35 | - id: pyupgrade 36 | args: [--py310-plus] 37 | 38 | - repo: https://github.com/pycqa/isort 39 | rev: 7.0.0 40 | hooks: 41 | - id: isort 42 | 43 | - repo: https://github.com/psf/black-pre-commit-mirror 44 | rev: 25.9.0 45 | hooks: 46 | - id: black 47 | 48 | - repo: https://github.com/pycqa/flake8 49 | rev: 7.3.0 50 | hooks: 51 | - id: flake8 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v2.5.0](https://github.com/ansible-community/molecule-hetznercloud/releases/tag/v2.5.0) 4 | 5 | While this release drops support for older versions of molecule, users may still use them. We only stopped testing the older versions of molecule in our CI. 6 | 7 | ### Features 8 | 9 | - drop support for molecule v5 and v6 (#169) 10 | 11 | ### Bug Fixes 12 | 13 | - ansible-core 2.19 compatibility (#180) 14 | 15 | ## [v2.4.1](https://github.com/ansible-community/molecule-hetznercloud/releases/tag/v2.4.1) 16 | 17 | ### Bug Fixes 18 | 19 | - invalid package repository url (#165) 20 | 21 | ## [v2.4.0](https://github.com/ansible-community/molecule-hetznercloud/releases/tag/v2.4.0) 22 | 23 | ### Features 24 | 25 | - add support for molecule 25 (#148) 26 | - add support for python 3.13 (#155) 27 | 28 | ## [2.3.2](https://github.com/ansible-community/molecule-hetznercloud/compare/2.3.1...2.3.2) (2025-01-13) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * relax molecule version range ([#136](https://github.com/ansible-community/molecule-hetznercloud/issues/136)) ([f0d176c](https://github.com/ansible-community/molecule-hetznercloud/commit/f0d176c19c3dce98633f6766c759643c41b7a95e)), closes [#135](https://github.com/ansible-community/molecule-hetznercloud/issues/135) 34 | 35 | ## [2.3.1](https://github.com/ansible-community/molecule-hetznercloud/compare/2.3.0...2.3.1) (2024-10-08) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **deps:** update dependency molecule to >=5.0.0,<24.10 ([#119](https://github.com/ansible-community/molecule-hetznercloud/issues/119)) ([0c8027d](https://github.com/ansible-community/molecule-hetznercloud/commit/0c8027db126bf0603bc27fb5017425855235e3a3)) 41 | 42 | ## [2.3.0](https://github.com/ansible-community/molecule-hetznercloud/compare/2.2.0...2.3.0) (2024-08-19) 43 | 44 | 45 | ### Features 46 | 47 | * require python>=3.10 ([#108](https://github.com/ansible-community/molecule-hetznercloud/issues/108)) ([a623c02](https://github.com/ansible-community/molecule-hetznercloud/commit/a623c02e2ee9dbf4e0a374eec2960fad5702ad7c)) 48 | 49 | 50 | ### Dependencies 51 | 52 | * update pre-commit hook pycqa/flake8 to v7.1.0 ([e33c1af](https://github.com/ansible-community/molecule-hetznercloud/commit/e33c1af40d8a8d17f7508ffa87413c3d2ec634da)) 53 | * update pypa/gh-action-pypi-publish action to v1.9.0 ([#104](https://github.com/ansible-community/molecule-hetznercloud/issues/104)) ([bf0ca8a](https://github.com/ansible-community/molecule-hetznercloud/commit/bf0ca8a5508db0d718faff9d606c18c46365d17e)) 54 | 55 | ## [2.2.0](https://github.com/ansible-community/molecule-hetznercloud/compare/2.1.1...2.2.0) (2024-06-11) 56 | 57 | 58 | ### Features 59 | 60 | * instance `server_type` now defaults to `cx22` ([#99](https://github.com/ansible-community/molecule-hetznercloud/issues/99)) ([b1bb242](https://github.com/ansible-community/molecule-hetznercloud/commit/b1bb24242272e2d8cfd82c6efde86f90994162c0)) 61 | 62 | 63 | ### Dependencies 64 | 65 | * update dependency molecule to >=5.0.0,<24.7 ([#98](https://github.com/ansible-community/molecule-hetznercloud/issues/98)) ([eb56da9](https://github.com/ansible-community/molecule-hetznercloud/commit/eb56da94eb088dbd5549882c294fe0207853670f)) 66 | * update dependency pytest-cov to v5 ([#92](https://github.com/ansible-community/molecule-hetznercloud/issues/92)) ([3cde3ca](https://github.com/ansible-community/molecule-hetznercloud/commit/3cde3ca9a028ce4c1a1c8e1caaa1086f5074b328)) 67 | * update pre-commit hook asottile/pyupgrade to v3.15.1 ([#83](https://github.com/ansible-community/molecule-hetznercloud/issues/83)) ([76cd33f](https://github.com/ansible-community/molecule-hetznercloud/commit/76cd33f77867fff898aac534e062e552e5aef401)) 68 | * update pre-commit hook asottile/pyupgrade to v3.15.2 ([26bfb65](https://github.com/ansible-community/molecule-hetznercloud/commit/26bfb652fbe87177b107e00ab69c66f80e728095)) 69 | * update pre-commit hook asottile/pyupgrade to v3.16.0 ([292dda1](https://github.com/ansible-community/molecule-hetznercloud/commit/292dda12a67a08ce453d1f235085810e8f1826fe)) 70 | * update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 ([73035ce](https://github.com/ansible-community/molecule-hetznercloud/commit/73035ce0da0b474f118b4fb386b56bde1d3722af)) 71 | * update pre-commit hook psf/black-pre-commit-mirror to v24.2.0 ([#82](https://github.com/ansible-community/molecule-hetznercloud/issues/82)) ([96030aa](https://github.com/ansible-community/molecule-hetznercloud/commit/96030aa38bb0cfb4c68301cab0f50ef73cf30ac5)) 72 | * update pre-commit hook psf/black-pre-commit-mirror to v24.3.0 ([a8fce1f](https://github.com/ansible-community/molecule-hetznercloud/commit/a8fce1f11f23ad78c5c0746fe2191d91484d4e68)) 73 | * update pre-commit hook psf/black-pre-commit-mirror to v24.4.0 ([41225db](https://github.com/ansible-community/molecule-hetznercloud/commit/41225db7d216fcd9a35afa352679f05cbd6d132f)) 74 | * update pre-commit hook psf/black-pre-commit-mirror to v24.4.1 ([#95](https://github.com/ansible-community/molecule-hetznercloud/issues/95)) ([06df822](https://github.com/ansible-community/molecule-hetznercloud/commit/06df8229c11775bace06fed18657be2244a9ec65)) 75 | * update pre-commit hook psf/black-pre-commit-mirror to v24.4.2 ([e03b63e](https://github.com/ansible-community/molecule-hetznercloud/commit/e03b63e9c035636df0bca464dc124e876d460d15)) 76 | * update pypa/gh-action-pypi-publish action to v1.8.12 ([#87](https://github.com/ansible-community/molecule-hetznercloud/issues/87)) ([440c71d](https://github.com/ansible-community/molecule-hetznercloud/commit/440c71d8273b47985d0113ac26de8edf20fdab6f)) 77 | * update pypa/gh-action-pypi-publish action to v1.8.14 ([#88](https://github.com/ansible-community/molecule-hetznercloud/issues/88)) ([c1f46bb](https://github.com/ansible-community/molecule-hetznercloud/commit/c1f46bba686f7369931441b3ebe24ab9eeb6c674)) 78 | 79 | ## [2.1.1](https://github.com/ansible-community/molecule-hetznercloud/compare/2.1.0...2.1.1) (2024-02-09) 80 | 81 | 82 | ### Dependencies 83 | 84 | * update dependency molecule to v24 ([#81](https://github.com/ansible-community/molecule-hetznercloud/issues/81)) ([30516de](https://github.com/ansible-community/molecule-hetznercloud/commit/30516deaeec7643c4f6f2708c68eef3cfec55251)) 85 | 86 | ## [2.1.0](https://github.com/ansible-community/molecule-hetznercloud/compare/2.0.0...2.1.0) (2024-02-02) 87 | 88 | 89 | ### Features 90 | 91 | * change the generated ssh key type to `ed25519` ([#69](https://github.com/ansible-community/molecule-hetznercloud/issues/69)) ([2b6ab8a](https://github.com/ansible-community/molecule-hetznercloud/commit/2b6ab8a481f3f9d25172b6a564495fe7499a940c)) 92 | * enable support for python 3.12 ([#50](https://github.com/ansible-community/molecule-hetznercloud/issues/50)) ([f665af4](https://github.com/ansible-community/molecule-hetznercloud/commit/f665af4d62b0daf7ce0ae4a3b42ad8484659226d)) 93 | 94 | ## [2.0.0](https://github.com/ansible-community/molecule-hetznercloud/compare/1.3.0...v2.0.0) (2023-10-09) 95 | 96 | ### ⚠ BREAKING CHANGES 97 | 98 | - the instance volumes now require a `name` 99 | - the molecule driver drops support for python version <3.9 100 | - the molecule driver drops support for molecule version <5.0 101 | - the molecule driver name was renamed from `hetznercloud` to `molecule_hetznercloud` 102 | - rewrite hetznercloud molecule driver ([#46](https://github.com/ansible-community/molecule-hetznercloud/issues/46)) 103 | 104 | ### Features 105 | 106 | - allow user defined RESOURCE_NAMESPACE ([#54](https://github.com/ansible-community/molecule-hetznercloud/issues/54)) ([1efd919](https://github.com/ansible-community/molecule-hetznercloud/commit/1efd919552d0507a21945efcdf4799aeee821065)) 107 | - instance `server_type` now defaults to `cx11` ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 108 | - rename driver name to `molecule_hetznercloud` ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 109 | - require instance volumes name ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 110 | - require molecule >=5.0,<7.0 ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 111 | - require python>=3.9 ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 112 | - rewrite hetznercloud molecule driver ([#46](https://github.com/ansible-community/molecule-hetznercloud/issues/46)) ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 113 | - update driver schema ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 114 | 115 | ### Bug Fixes 116 | 117 | - remove ansible-compat dependency ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 118 | - remove unused dependencies ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 119 | 120 | ### Documentation 121 | 122 | - add project history ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 123 | - the hetzner.hcloud collection require ansible-core>=2.13 ([36a28f4](https://github.com/ansible-community/molecule-hetznercloud/commit/36a28f40da6b98eb7473739cf0edc0989f89b978)) 124 | 125 | ## [1.3.0] - 2021-09-02 126 | 127 | Changes: 128 | 129 | - Remove deprecated molecule cache ([#35](https://github.com/ansible-community/molecule-hetznercloud/pull/35)) @ssbarnea 130 | - Remove duplicated playbook code ([#33](https://github.com/ansible-community/molecule-hetznercloud/pull/33)) @ekeih 131 | - Remove async for network deletion ([#30](https://github.com/ansible-community/molecule-hetznercloud/pull/30)) @ggggut 132 | - Typo fix ([#28](https://github.com/ansible-community/molecule-hetznercloud/pull/28)) @aminvakil 133 | 134 | Bugfixes: 135 | 136 | - Remove deprecated molecule cache ([#35](https://github.com/ansible-community/molecule-hetznercloud/pull/35)) @ssbarnea 137 | - Testing configuration cleanup ([#11](https://github.com/ansible-community/molecule-hetznercloud/pull/11)) @ssbarnea 138 | - Match API expectations ([#8](https://github.com/ansible-community/molecule-hetznercloud/pull/8)) @decentral1se 139 | - Allow Molecule to load the cookiecutter correctly ([#7](https://github.com/ansible-community/molecule-hetznercloud/pull/7)) @decentral1se 140 | - Fix CI ([#6](https://github.com/ansible-community/molecule-hetznercloud/pull/6)) @decentral1se 141 | - Add a missing entry point option to setup.cfg ([#5](https://github.com/ansible-community/molecule-hetznercloud/pull/5)) @tadeboro 142 | - Enabling Travis testing ([#1](https://github.com/ansible-community/molecule-hetznercloud/pull/1)) @decentral1se 143 | 144 | ## [1.2.1] - 2021-06-02 145 | 146 | ### Fixed 147 | 148 | - Remove async task handling for network deletion ([#30](https://github.com/ansible-community/molecule-hetznercloud/pull/30), credit @ggggut) 149 | 150 | ## [1.2.0] - 2021-06-02 151 | 152 | ### Added 153 | 154 | - Allow to create networks during test runs ([#29](https://github.com/ansible-community/molecule-hetznercloud/pull/29), thanks @ggggut!) 155 | 156 | ## [1.1.0] - 2021-03-30 157 | 158 | ## Changed 159 | 160 | - Relaxed bounds on Molecule to allow all versions less than `v4` ([#27](https://github.com/ansible-community/molecule-hetznercloud/pull/27)) 161 | 162 | ## [1.0.0] - 2021-01-06 163 | 164 | This is a major release with breaking changes for your schema and support for a 165 | new major version of Molecule. If you use the `volumes:` key in your 166 | `molecule.yml` then this change will break your configuration. Please see the 167 | section on "Volume Handling" in the README.md on how to upgrade successfully. 168 | You will now need to install Ansible yourself as Molecule does not do it for 169 | you. If there are any other breaking changes, please report them on the issue 170 | tracker so that we can mention them here. 171 | 172 | - Support Python 3.9. 173 | - Support Molecule 3.2.1 174 | - Add volume creation and clean up handling 175 | 176 | ## [0.2.2] - 2020-06-15 177 | 178 | ### Fixed 179 | 180 | - Point to an open issue tracker 181 | 182 | ## [0.2.1] - 2020-04-29 183 | 184 | ### Fixed 185 | 186 | - Pinned Molecule to avoid issues with `sh` dependency. 187 | 188 | ## [0.2.0] - 2020-04-27 189 | 190 | ### Added 191 | 192 | - Add bundled playbooks so as to reduce required configuration on end-user side 193 | - Added an internal `molecule.yml` so that `molecule init role` can get good defaults (will work with Molecule >= 3.0.4) 194 | 195 | ## [0.1.0] - 2020-04-27 196 | 197 | ### Added 198 | 199 | - py36,37,38 are now supported 200 | 201 | ## [0.0.1] - 2020-04-25 202 | 203 | ### Added 204 | 205 | - Molecule 3.x support 206 | - Usage documentation in the README.md 207 | - Drone CI/CD integration testing 208 | 209 | ### Changed 210 | 211 | - Migrate to git.autonomic.zone for maintenance 212 | - Mirroring of Github repository and discussion with Molecule team 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | 3 | venv: 4 | python3 -m venv venv 5 | venv/bin/pip install -e .[test] 6 | 7 | export ANSIBLE_COLLECTIONS_PATH = $(shell pwd)/ansible_collections 8 | 9 | ansible_collections: venv 10 | venv/bin/ansible-galaxy collection install -r requirements.yml 11 | 12 | .PHONY: test 13 | test: venv 14 | venv/bin/tox -- $(ARGS) tests/functional tests/unit 15 | 16 | .PHONY: integration 17 | integration: venv ansible_collections 18 | venv/bin/tox -- $(ARGS) tests/integration 19 | 20 | .PHONY: clean 21 | clean: 22 | git clean -xdf 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Molecule Hetzner Cloud Driver 2 | 3 | [![PyPI Package](https://img.shields.io/pypi/v/molecule-hetznercloud)](https://pypi.org/project/molecule-hetznercloud/) 4 | [![Codecov](https://img.shields.io/codecov/c/github/ansible-community/molecule-hetznercloud/main)](https://app.codecov.io/gh/ansible-community/molecule-hetznercloud/tree/main) 5 | [![License](https://img.shields.io/badge/license-LGPL-brightgreen.svg)](LICENSE) 6 | 7 | A [Hetzner Cloud](https://www.hetzner.com/cloud) driver for [Molecule](https://ansible.readthedocs.io/projects/molecule/). 8 | 9 | This plugin allows you to use on-demand Hetzner Cloud servers for your molecule integration tests. 10 | 11 | ## Install 12 | 13 | ```bash 14 | $ pip install molecule-hetznercloud 15 | ``` 16 | 17 | ## Upgrade 18 | 19 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 20 | 21 | ```bash 22 | $ pip install --upgrade molecule-hetznercloud 23 | ``` 24 | 25 | ### Upgrading to v2 26 | 27 | - In your `molecule.yml` files, rename the molecule driver name (`driver.name`) to `molecule_hetznercloud`: 28 | 29 | ```patch 30 | driver: 31 | - name: hetznercloud 32 | + name: molecule_hetznercloud 33 | ``` 34 | 35 | - In your `molecule.yml` files, the volumes name (`platforms[].volumes[].name`) field is now required. If the field is missing, you MUST add it: 36 | 37 | ```patch 38 | platforms: 39 | - name: instance-1 40 | image: debian-12 41 | volumes: 42 | - - size: 20 43 | + - name: volume-1 44 | + size: 20 45 | ``` 46 | 47 | - Each resource (servers, volumes, networks) name are prefixed with a hash (32 chars) based on the role and scenario path. This means you MAY reuse the same names (e.g. `instance-1`) across your scenarios. Resource names MUST not exceed their max length, for example the server name max length is 63 chars, with the prefix you only have 31 chars left for your name. 48 | 49 | - In your `molecule.yml` files, the platforms server type (`platforms[].server_type`) field now defaults to `cx22`. If you already use the default, you MAY remove the field: 50 | 51 | ```patch 52 | platforms: 53 | - name: instance-1 54 | image: debian-12 55 | - server_type: cx22 56 | ``` 57 | 58 | ## Usage 59 | 60 | To communicate with the Hetzner Cloud API, you need to expose a `HCLOUD_TOKEN` environment variable. Find out more about how to get a Hetzner Cloud API token in the [authentication documentation](https://docs.hetzner.cloud/reference/cloud#authentication). 61 | 62 | ```bash 63 | $ export HCLOUD_TOKEN="set_the_hcloud_token_here" 64 | ``` 65 | 66 | Then setup a new molecule scenario using the driver plugin. 67 | 68 | ```bash 69 | $ molecule init scenario --driver-name molecule_hetznercloud 70 | ``` 71 | 72 | > [!WARNING] 73 | > With molecule 6, the `molecule init scenario` command dropped support for driver provided configuration. If you are using molecule >=6, please copy the example below and paste it in your scenario `molecule.yml` file. 74 | > See [this commit](https://github.com/ansible/molecule/commit/21dcd2bb7e8e9002be8bbc19de3e66ec3ce586f1) for details. 75 | 76 | Your `molecule/default/molecule.yml` should then look like the following. 77 | 78 | ```yaml 79 | --- 80 | driver: 81 | name: molecule_hetznercloud 82 | platforms: 83 | - # Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123). 84 | # required 85 | name: instance-1 86 | # Name of the Image the Server is created from. 87 | # required 88 | image: debian-12 89 | # Name of the Server type this Server should be created with. 90 | # default: cx22 91 | server_type: cx22 92 | # Name of Location to create Server in (must not be used together with datacenter). 93 | # default: omit 94 | location: hel1 95 | # Name of Datacenter to create Server in (must not be used together with location). 96 | # default: omit 97 | datacenter: null 98 | # Cloud-Init user data to use during Server creation. This field is limited to 32KiB. 99 | # default: omit 100 | user_data: null 101 | 102 | # List of volumes to attach to the server. 103 | volumes: 104 | - # Name of the volume. 105 | # required 106 | name: volume-1 107 | # Size of the Volume in GB. 108 | # default: 10 109 | size: 10 110 | 111 | # Dictionary of private networks the server should be attached to. 112 | networks: 113 | # Name of the network 114 | network-1: 115 | # IP range of the whole network which must span all included subnets. Must be one of the private IPv4 ranges of RFC1918. 116 | # If multiple hosts using the same network, you may only define it once. 117 | # required 118 | ip_range: 10.0.0.0/16 119 | subnet: 120 | # IP to assign to the server. 121 | # required 122 | ip: 10.0.0.1/24 123 | # Type of subnetwork. 124 | # default: cloud 125 | type: cloud 126 | # Name of network zone. 127 | # default: eu-central 128 | network_zone: eu-central 129 | network-2: 130 | ip_range: 10.1.0.0/16 131 | subnet: 132 | ip: 10.1.0.1/24 133 | ``` 134 | 135 | > [!NOTE] 136 | > The `networks.ip_range` is important for creating. If you have multiple 137 | > hosts, you may only define it once. 138 | 139 | > [!NOTE] 140 | > You may list the server types and available images using the `hcloud` command line tool: 141 | > 142 | > ```bash 143 | > # List server types 144 | > $ hcloud server-type list --sort name 145 | > # List images for the x86 architecture 146 | > $ hcloud image list --type system --architecture x86 --sort name 147 | > ``` 148 | 149 | Then test your role. 150 | 151 | ```bash 152 | $ molecule test 153 | ``` 154 | 155 | To ease initial debugging for getting things started, also expose the following 156 | environment variables. 157 | 158 | ```bash 159 | $ export MOLECULE_NO_LOG=False # not so verbose, helpful 160 | $ export MOLECULE_DEBUG=True # very verbose, last ditch effort 161 | ``` 162 | 163 | You may also define a custom resource namespace by exposing the following 164 | environment variables, for example in CI workflows: 165 | 166 | ```bash 167 | $ export RESOURCE_NAMESPACE=e121dc64ff615ccdfac71bb5c00296b9 # Ensure the value length is <= 32 168 | ``` 169 | 170 | ## Development 171 | 172 | ### Testing 173 | 174 | Run unit tests: 175 | 176 | ```bash 177 | make test 178 | ``` 179 | 180 | Run integration tests 181 | 182 | ```bash 183 | export HCLOUD_TOKEN="set_the_hcloud_token_here" 184 | make integration 185 | ``` 186 | 187 | ## History 188 | 189 | The project was initially maintained by [@decentral1se](https://github.com/decentral1se). After a long period [looking for new maintainers](https://github.com/ansible-community/molecule-hetznercloud/issues/43), the project was archived in early 2023. 190 | 191 | In September 2023, the code has been rewritten by [@jooola](https://github.com/jooola) and the project was reactivated to continue development. 192 | 193 | ## License 194 | 195 | The [LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.en.html) license. 196 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | -------------------------------------------------------------------------------- /molecule_hetznercloud/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ._version import __version__ # noqa: F401 4 | -------------------------------------------------------------------------------- /molecule_hetznercloud/_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __version__ = "2.5.0" # x-releaser-pleaser-version 4 | -------------------------------------------------------------------------------- /molecule_hetznercloud/cookiecutter/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "molecule_directory": "molecule", 3 | "role_name": "OVERRIDDEN", 4 | "scenario_name": "OVERRIDDEN" 5 | } 6 | -------------------------------------------------------------------------------- /molecule_hetznercloud/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | Hetzner Cloud plugin installation guide 3 | *************************************** 4 | 5 | Requirements 6 | ============ 7 | 8 | * `ansible-core>=2.13` 9 | * ``HCLOUD_TOKEN`` exposed in your environment 10 | 11 | Install 12 | ======= 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install molecule-hetznercloud 17 | $ ansible-galaxy collection install hetzner.hcloud 18 | -------------------------------------------------------------------------------- /molecule_hetznercloud/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: Include {{ cookiecutter.role_name }} 6 | ansible.builtin.include_role: 7 | name: "{{ cookiecutter.role_name }}" 8 | -------------------------------------------------------------------------------- /molecule_hetznercloud/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: molecule_hetznercloud 4 | platforms: 5 | - # Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123). 6 | # required 7 | name: instance-1 8 | # Name of the Image the Server is created from. 9 | # required 10 | image: debian-12 11 | # Name of the Server type this Server should be created with. 12 | # default: cx22 13 | server_type: cx22 14 | # Name of Location to create Server in (must not be used together with datacenter). 15 | # default: omit 16 | location: hel1 17 | # Name of Datacenter to create Server in (must not be used together with location). 18 | # default: omit 19 | datacenter: null 20 | # Cloud-Init user data to use during Server creation. This field is limited to 32KiB. 21 | # default: omit 22 | user_data: null 23 | 24 | # List of volumes to attach to the server. 25 | volumes: 26 | - # Name of the volume. 27 | # required 28 | name: volume-1 29 | # Size of the Volume in GB. 30 | # default: 10 31 | size: 10 32 | 33 | # Dictionary of private networks the server should be attached to. 34 | networks: 35 | # Name of the network 36 | network-1: 37 | # IP range of the whole network which must span all included subnets. Must be one of the private IPv4 ranges of RFC1918. 38 | # If multiple hosts using the same network, you may only define it once. 39 | # required 40 | ip_range: 10.0.0.0/16 41 | subnet: 42 | # IP to assign to the server. 43 | # required 44 | ip: 10.0.0.1/24 45 | # Type of subnetwork. 46 | # default: cloud 47 | type: cloud 48 | # Name of network zone. 49 | # default: eu-central 50 | network_zone: eu-central 51 | network-2: 52 | ip_range: 10.1.0.0/16 53 | subnet: 54 | ip: 10.1.0.1/24 55 | -------------------------------------------------------------------------------- /molecule_hetznercloud/driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://raw.githubusercontent.com/ansible-community/molecule/main/src/molecule/driver/driver.json", 3 | "$schema": "http://json-schema.org/draft-07/schema", 4 | "type": "object", 5 | "required": ["driver"], 6 | "properties": { 7 | "driver": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "type": "string", 12 | "enum": ["molecule_hetznercloud"] 13 | }, 14 | "options": { 15 | "type": "object" 16 | } 17 | }, 18 | "platforms": { 19 | "type": "array", 20 | "items": { 21 | "type": "object", 22 | "required": ["name", "image"], 23 | "additionalProperties": false, 24 | "properties": { 25 | "name": { 26 | "type": "string", 27 | "maxLength": "30" 28 | }, 29 | "image": { 30 | "type": "string" 31 | }, 32 | "server_type": { 33 | "type": "string" 34 | }, 35 | "location": { 36 | "type": "string" 37 | }, 38 | "datacenter": { 39 | "type": "string" 40 | }, 41 | "user_data": { 42 | "type": "string" 43 | }, 44 | "networks": { 45 | "type": "object", 46 | "required": ["subnet"], 47 | "additionalProperties": false, 48 | "patternProperties": { 49 | "^[a-zA-Z0-9-_]+$": { 50 | "ip_range": { 51 | "type": "string" 52 | }, 53 | "subnet": { 54 | "type": "object", 55 | "required": ["ip"], 56 | "additionalProperties": false, 57 | "properties": { 58 | "ip": { 59 | "type": "string" 60 | }, 61 | "type": { 62 | "type": "string" 63 | }, 64 | "network_zone": { 65 | "type": "string" 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }, 72 | "volumes": { 73 | "type": "array", 74 | "items": { 75 | "type": "object", 76 | "additionalProperties": false, 77 | "required": ["name"], 78 | "properties": { 79 | "name": { 80 | "type": "string" 81 | }, 82 | "size": { 83 | "type": "integer" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /molecule_hetznercloud/driver.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from molecule import logger, util 6 | from molecule.api import Driver 7 | 8 | log = logger.get_logger(__name__) 9 | 10 | 11 | class HetznerCloud(Driver): 12 | def __init__(self, config=None): 13 | super().__init__(config) 14 | self._name = "molecule_hetznercloud" 15 | 16 | @property 17 | def name(self): 18 | return self._name 19 | 20 | @name.setter 21 | def name(self, value): 22 | self._name = value 23 | 24 | @property 25 | def login_cmd_template(self): 26 | connection_options = " ".join(self.ssh_connection_options) 27 | 28 | return ( 29 | "ssh {address} " 30 | "-l {user} " 31 | "-p {port} " 32 | "-i {identity_file} " 33 | f"{connection_options}" 34 | ) 35 | 36 | @property 37 | def default_safe_files(self): 38 | return [self.instance_config, "ssh_key"] 39 | 40 | @property 41 | def default_ssh_connection_options(self): 42 | return self._get_ssh_connection_options() 43 | 44 | def login_options(self, instance_name): 45 | config = {"instance": instance_name} 46 | 47 | return util.merge_dicts(config, self._get_instance_config(instance_name)) 48 | 49 | def ansible_connection_options(self, instance_name): 50 | try: 51 | config = self._get_instance_config(instance_name) 52 | 53 | return { 54 | "ansible_user": config["user"], 55 | "ansible_host": config["address"], 56 | "ansible_port": config["port"], 57 | "ansible_private_key_file": config["identity_file"], 58 | "connection": "ssh", 59 | "ansible_ssh_common_args": " ".join(self.ssh_connection_options), 60 | } 61 | except StopIteration: 62 | return {} 63 | except OSError: 64 | return {} 65 | 66 | def template_dir(self): 67 | """ 68 | Return the path to the cookiecutter templates. 69 | """ 70 | return os.path.join(os.path.dirname(__file__), "cookiecutter") 71 | 72 | def _get_instance_config(self, instance_name): 73 | instance_config_dict = util.safe_load_file(self._config.driver.instance_config) 74 | 75 | return next( 76 | item for item in instance_config_dict if item["instance"] == instance_name 77 | ) 78 | 79 | def sanity_checks(self) -> None: 80 | """Confirm that driver is usable. 81 | 82 | Sanity checks to ensure the driver can do work successfully. For 83 | example, when using the Docker driver, we want to know that the Docker 84 | daemon is running and we have the correct Docker Python dependency. 85 | Each driver implementation can decide what is the most stable sanity 86 | check for itself. 87 | """ 88 | 89 | if os.environ.get("HCLOUD_TOKEN", "") == "": 90 | msg = ( 91 | "Missing Hetzner Cloud API token. Please expose the Hetzner Cloud API " 92 | "token in the HCLOUD_TOKEN environment variable." 93 | ) 94 | util.sysexit_with_message(msg) 95 | 96 | def reset(self): 97 | """Release all resources owned by molecule. 98 | 99 | This is a destructive operation that would affect all resources managed 100 | by molecule, regardless the scenario name. Molecule will use metadata 101 | like labels or tags to annotate resources allocated by it. 102 | """ 103 | 104 | def schema_file(self): 105 | return os.path.join(os.path.dirname(__file__), "driver.json") 106 | -------------------------------------------------------------------------------- /molecule_hetznercloud/playbooks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | vars: 8 | resource_namespace: "{{ lookup('ansible.builtin.env', 'RESOURCE_NAMESPACE') | default(molecule_scenario_directory | md5, true) }}" 9 | 10 | ssh_port: 22 11 | ssh_user: root 12 | ssh_key_path: "{{ molecule_ephemeral_directory }}/ssh_key" 13 | ssh_key_name: "{{ resource_namespace }}" 14 | tasks: 15 | - name: Create SSH key 16 | community.crypto.openssh_keypair: 17 | path: "{{ ssh_key_path }}" 18 | type: ed25519 19 | register: generated_ssh_key 20 | 21 | - name: Register SSH key for server(s) 22 | hetzner.hcloud.hcloud_ssh_key: 23 | name: "{{ ssh_key_name }}" 24 | public_key: "{{ generated_ssh_key.public_key }}" 25 | labels: 26 | molecule: "{{ resource_namespace }}" 27 | state: present 28 | 29 | - name: Create server(s) 30 | hetzner.hcloud.hcloud_server: 31 | name: "{{ resource_namespace }}-{{ item.name }}" 32 | image: "{{ item.image }}" 33 | server_type: "{{ item.server_type | default('cx22') }}" 34 | ssh_keys: ["{{ ssh_key_name }}"] 35 | location: "{{ item.location | default(omit) }}" 36 | datacenter: "{{ item.datacenter | default(omit) }}" 37 | user_data: "{{ item.user_data | default(omit) }}" 38 | labels: 39 | molecule: "{{ resource_namespace }}" 40 | state: started 41 | loop: "{{ molecule_yml.platforms }}" 42 | register: servers_jobs 43 | async: 7200 44 | poll: 0 45 | 46 | - name: Wait for server(s) creation to complete 47 | ansible.builtin.async_status: 48 | jid: "{{ item.ansible_job_id }}" 49 | loop: "{{ servers_jobs.results }}" 50 | register: servers 51 | until: servers is finished 52 | retries: 300 53 | 54 | - name: Create volume(s) 55 | hetzner.hcloud.hcloud_volume: 56 | name: "{{ resource_namespace }}-{{ item.name }}" 57 | server: "{{ resource_namespace }}-{{ item.server_name }}" 58 | size: "{{ item.size | default(10) }}" 59 | labels: 60 | molecule: "{{ resource_namespace }}" 61 | state: present 62 | loop: "{{ molecule_yml.platforms | molecule_get_hetznercloud_volumes() }}" 63 | 64 | - name: Create network(s) 65 | hetzner.hcloud.hcloud_network: 66 | name: "{{ resource_namespace }}-{{ item.name }}" 67 | ip_range: "{{ item.ip_range | default(omit) }}" 68 | labels: 69 | molecule: "{{ resource_namespace }}" 70 | state: present 71 | loop: "{{ molecule_yml.platforms | molecule_get_hetznercloud_networks() }}" 72 | 73 | - name: Create subnetwork(s) # noqa jinja[invalid] 74 | hetzner.hcloud.hcloud_subnetwork: 75 | network: "{{ resource_namespace }}-{{ item.network_name }}" 76 | ip_range: "{{ item.ip | ansible.utils.ipaddr('network/prefix') }}" 77 | network_zone: "{{ item.network_zone | default('eu-central') }}" 78 | type: "{{ item.type | default('cloud') }}" 79 | state: present 80 | loop: "{{ molecule_yml.platforms | molecule_get_hetznercloud_subnetworks() }}" 81 | 82 | - name: Attach server to subnetwork(s) # noqa jinja[invalid] 83 | hetzner.hcloud.hcloud_server_network: 84 | network: "{{ resource_namespace }}-{{ item.network_name }}" 85 | server: "{{ resource_namespace }}-{{ item.server_name }}" 86 | ip: "{{ item.ip | ansible.utils.ipaddr('address') }}" 87 | state: present 88 | loop: > 89 | {{ 90 | molecule_yml.platforms 91 | | molecule_get_hetznercloud_subnetworks() 92 | | rejectattr('type', '==', 'vswitch') 93 | }} 94 | 95 | - name: Create instance config 96 | block: 97 | - name: Populate instance config dict 98 | ansible.builtin.set_fact: 99 | instance_conf_dict: 100 | { 101 | "instance": "{{ item.item.item.name }}", 102 | "address": "{{ item.hcloud_server.ipv4_address }}", 103 | "user": "{{ ssh_user }}", 104 | "port": "{{ ssh_port }}", 105 | "ssh_key_name": "{{ ssh_key_name }}", 106 | "identity_file": "{{ ssh_key_path }}", 107 | "volumes": "{{ item.item.item.volumes | default([]) }}", 108 | "networks": "{{ item.item.item.networks | default({}) | dict2items(key_name='name') }}", 109 | } 110 | loop: "{{ servers.results }}" 111 | register: instance_config_dict 112 | 113 | - name: Convert instance config dict to a list 114 | ansible.builtin.set_fact: 115 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 116 | 117 | - name: Dump instance config 118 | ansible.builtin.copy: 119 | content: | 120 | # Molecule managed 121 | 122 | {{ instance_conf | to_yaml }} 123 | dest: "{{ molecule_instance_config }}" 124 | mode: "0600" 125 | 126 | - name: Wait for SSH 127 | ansible.builtin.wait_for: 128 | host: "{{ item.address }}" 129 | port: "{{ item.port }}" 130 | search_regex: SSH 131 | loop: "{{ lookup('file', molecule_instance_config) | from_yaml }}" 132 | -------------------------------------------------------------------------------- /molecule_hetznercloud/playbooks/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | vars: 8 | resource_namespace: "{{ lookup('ansible.builtin.env', 'RESOURCE_NAMESPACE') | default(molecule_scenario_directory | md5, true) }}" 9 | tasks: 10 | - name: Populate the instance config 11 | ansible.builtin.set_fact: 12 | instance_conf: "{{ lookup('file', molecule_instance_config, errors='warn') | from_yaml | default([], true) }}" 13 | 14 | - name: Destroy server(s) 15 | hetzner.hcloud.hcloud_server: 16 | name: "{{ resource_namespace }}-{{ item.instance }}" 17 | state: absent 18 | register: servers_jobs 19 | loop: "{{ instance_conf }}" 20 | async: 7200 21 | poll: 0 22 | 23 | - name: Wait for server(s) deletion to complete 24 | ansible.builtin.async_status: 25 | jid: "{{ item.ansible_job_id }}" 26 | loop: "{{ servers_jobs.results }}" 27 | register: servers 28 | until: servers is finished 29 | retries: 300 30 | 31 | - name: Destroy volume(s) 32 | hetzner.hcloud.hcloud_volume: 33 | name: "{{ resource_namespace }}-{{ item.1.name }}" 34 | state: absent 35 | loop: "{{ instance_conf | subelements('volumes', skip_missing=True) }}" 36 | register: volumes_jobs 37 | async: 7200 38 | poll: 0 39 | 40 | - name: Wait for volume(s) deletion to complete 41 | ansible.builtin.async_status: 42 | jid: "{{ item.ansible_job_id }}" 43 | loop: "{{ volumes_jobs.results }}" 44 | register: volumes 45 | until: volumes is finished 46 | retries: 300 47 | 48 | - name: Destroy network(s) 49 | hetzner.hcloud.hcloud_network: 50 | name: "{{ resource_namespace }}-{{ item.1.name }}" 51 | state: absent 52 | loop: "{{ instance_conf | subelements('networks', skip_missing=True) }}" 53 | register: networks 54 | 55 | - name: Remove registered SSH key 56 | when: instance_conf | length > 0 57 | hetzner.hcloud.hcloud_ssh_key: 58 | name: "{{ instance_conf[0].ssh_key_name }}" 59 | state: absent 60 | 61 | - name: Populate instance config 62 | ansible.builtin.set_fact: 63 | instance_conf: [] 64 | 65 | - name: Dump instance config 66 | ansible.builtin.copy: 67 | content: | 68 | # Molecule managed 69 | 70 | {{ instance_conf | to_yaml }} 71 | dest: "{{ molecule_instance_config }}" 72 | mode: "0600" 73 | -------------------------------------------------------------------------------- /molecule_hetznercloud/playbooks/filter_plugins/get_platforms_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from __future__ import annotations 5 | 6 | 7 | def merge_two_dicts(x: dict, y: dict) -> dict: 8 | z = x.copy() 9 | z.update(y) 10 | return z 11 | 12 | 13 | def get_hetznercloud_networks(platforms: list[dict]) -> list: 14 | all_networks = {} 15 | for platform in platforms: 16 | if "networks" not in platform: 17 | continue 18 | 19 | for network_name, network in platform["networks"].items(): 20 | if network is None: 21 | continue 22 | network["name"] = network_name 23 | 24 | del network["subnet"] 25 | 26 | existing_network = all_networks.get(network_name, {}) 27 | all_networks[network_name] = merge_two_dicts(existing_network, network) 28 | 29 | return list(all_networks.values()) 30 | 31 | 32 | def get_hetznercloud_subnetworks(platforms: list[dict]) -> list[dict]: 33 | all_subnetworks = [] 34 | for platform in platforms: 35 | if "networks" not in platform: 36 | continue 37 | 38 | for network_name, network in platform["networks"].items(): 39 | if "subnet" in network: 40 | network["subnet"]["server_name"] = platform["name"] 41 | network["subnet"]["network_name"] = network_name 42 | 43 | # Filtering the subnets by 'type' requires the value to be defined 44 | if "type" not in network["subnet"]: 45 | network["subnet"]["type"] = "cloud" 46 | 47 | all_subnetworks.append(network["subnet"]) 48 | 49 | return all_subnetworks 50 | 51 | 52 | def get_hetznercloud_volumes(platforms: list[dict]) -> list[dict]: 53 | all_volumes = [] 54 | for platform in platforms: 55 | if "volumes" not in platform: 56 | continue 57 | 58 | for volume in platform["volumes"]: 59 | volume["server_name"] = platform["name"] 60 | all_volumes.append(volume) 61 | 62 | return all_volumes 63 | 64 | 65 | class FilterModule: 66 | """Core Molecule filter plugins.""" 67 | 68 | def filters(self): 69 | return { 70 | "molecule_get_hetznercloud_networks": get_hetznercloud_networks, 71 | "molecule_get_hetznercloud_subnetworks": get_hetznercloud_subnetworks, 72 | "molecule_get_hetznercloud_volumes": get_hetznercloud_volumes, 73 | } 74 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | combine_as_imports = true 4 | add_imports = ["from __future__ import annotations"] 5 | 6 | [tool.coverage.run] 7 | source = ["molecule_hetznercloud"] 8 | 9 | [build-system] 10 | requires = ["setuptools"] 11 | build-backend = "setuptools.build_meta" 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>hetznercloud/.github//renovate/default"], 4 | "packageRules": [ 5 | { 6 | "matchPackageNames": ["molecule"], 7 | "rangeStrategy": "widen" 8 | }, 9 | { 10 | "matchDepTypes": ["galaxy-collection"], 11 | "groupName": "galaxy-collection" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: community.crypto 4 | version: 3.0.4 5 | 6 | - name: hetzner.hcloud 7 | version: 5.4.0 8 | 9 | - name: ansible.netcommon 10 | version: 8.1.0 11 | 12 | - name: ansible.utils 13 | version: 6.0.0 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open("README.md", encoding="utf-8") as readme_file: 6 | readme = readme_file.read() 7 | 8 | setup( 9 | name="molecule-hetznercloud", 10 | version="2.5.0", # x-releaser-pleaser-version 11 | keywords="ansible molecule driver hcloud hetzner cloud testing", 12 | description="Molecule driver for Hetzner Cloud", 13 | long_description=readme, 14 | long_description_content_type="text/markdown", 15 | author="Hetzner Cloud GmbH", 16 | author_email="support-cloud@hetzner.com", 17 | url="https://github.com/ansible-community/molecule-hetznercloud", 18 | project_urls={ 19 | "Bug Tracker": "https://github.com/ansible-community/molecule-hetznercloud/issues", 20 | "Documentation": "https://github.com/ansible-community/molecule-hetznercloud#readme", 21 | "Changelog": "https://github.com/ansible-community/molecule-hetznercloud/blob/main/CHANGELOG.md", 22 | "Source Code": "https://github.com/ansible-community/molecule-hetznercloud", 23 | }, 24 | license="LGPL", 25 | classifiers=[ 26 | "Development Status :: 5 - Production/Stable", 27 | "Environment :: Console", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: Information Technology", 30 | "Intended Audience :: System Administrators", 31 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 32 | "Natural Language :: English", 33 | "Operating System :: OS Independent", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Programming Language :: Python :: 3.13", 39 | "Topic :: System :: Systems Administration", 40 | "Topic :: Utilities", 41 | ], 42 | python_requires=">=3.10", 43 | install_requires=[ 44 | "molecule>=5.0.0", 45 | # Dependencies for the hetzner.hcloud collection 46 | "python-dateutil>=2.7.5", 47 | "requests>=2.20", 48 | # Dependencies for the ansible.utils collection (ansible.utils.ipaddr) 49 | "netaddr", 50 | ], 51 | extras_require={ 52 | "test": [ 53 | "tox>=4.11.3,<5.0", 54 | "pytest-xdist>=3.3.1,<4.0", 55 | "pytest>=8.4.1,<8.5", 56 | "pytest-ansible>=25.8,<25.9", 57 | "pytest-cov>=7,<7.1", 58 | ], 59 | }, 60 | packages=find_packages(exclude=["tests*"]), 61 | package_data={ 62 | "": [ 63 | "**/*.json", 64 | "**/*.py", 65 | "**/*.rst", 66 | "**/*.yml", 67 | ] 68 | }, 69 | include_package_data=True, 70 | zip_safe=False, 71 | entry_points={ 72 | "molecule.driver": [ 73 | "molecule_hetznercloud=molecule_hetznercloud.driver:HetznerCloud", 74 | ] 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-hetznercloud/d3a246f391bda2c4ebe3c30d29e7c0c64b3cd718/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from contextlib import contextmanager 5 | from pathlib import Path 6 | 7 | 8 | @contextmanager 9 | def change_dir(path: Path): 10 | previous_path = Path.cwd() 11 | 12 | os.chdir(path) 13 | yield 14 | os.chdir(previous_path) 15 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-hetznercloud/d3a246f391bda2c4ebe3c30d29e7c0c64b3cd718/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | here = Path(__file__).parent 6 | -------------------------------------------------------------------------------- /tests/functional/fixtures/test_init_scenario/meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: Hetzner Cloud GmbH 3 | description: Test fixture 4 | license: GPL-3.0-only 5 | 6 | min_ansible_version: 2.13.0 7 | 8 | galaxy_tags: [] 9 | 10 | dependencies: [] 11 | -------------------------------------------------------------------------------- /tests/functional/fixtures/test_init_scenario/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for test_init_scenario 3 | -------------------------------------------------------------------------------- /tests/functional/test_hetznercloud.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from shutil import copytree 5 | 6 | import pytest 7 | from molecule._version import version_tuple as molecule_version_tuple 8 | 9 | if molecule_version_tuple < (25, 1): 10 | from molecule.util import run_command 11 | else: 12 | from molecule.app import get_app 13 | 14 | run_command = get_app(Path()).run_command 15 | 16 | 17 | import molecule_hetznercloud 18 | 19 | from ..conftest import change_dir 20 | from .fixtures import here as fixtures_path 21 | 22 | here = Path(__file__).parent 23 | 24 | package_path = Path(molecule_hetznercloud.__file__).parent 25 | templates_path = ( 26 | package_path 27 | / "cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}" 28 | ) 29 | 30 | 31 | @pytest.mark.skipif( 32 | molecule_version_tuple >= (6, 0), 33 | reason="molecule 6 removed support for custom templates from drivers", 34 | ) 35 | def test_command_init_scenario(tmp_path): 36 | role_name = "test_init_scenario" 37 | scenario_name = "default" 38 | 39 | role_path = tmp_path / role_name 40 | copytree(fixtures_path / role_name, role_path) 41 | 42 | with change_dir(role_path): 43 | cmd = [ 44 | "molecule", 45 | "init", 46 | "scenario", 47 | scenario_name, 48 | "--driver-name", 49 | "molecule_hetznercloud", 50 | ] 51 | result = run_command(cmd) 52 | assert result.returncode == 0 53 | 54 | scenario_path = role_path / "molecule" / scenario_name 55 | molecule_path = scenario_path / "molecule.yml" 56 | converge_path = scenario_path / "converge.yml" 57 | 58 | assert scenario_path.is_dir() 59 | assert molecule_path.is_file() 60 | assert converge_path.is_file() 61 | 62 | assert molecule_path.read_text() == (templates_path / "molecule.yml").read_text() 63 | -------------------------------------------------------------------------------- /tests/integration/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: Debug output 6 | ansible.builtin.debug: 7 | msg: "{{ ansible_hostname }}" 8 | -------------------------------------------------------------------------------- /tests/integration/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | 5 | driver: 6 | name: molecule_hetznercloud 7 | 8 | platforms: 9 | - name: instance-1 10 | image: debian-12 11 | networks: 12 | shared: 13 | ip_range: 10.10.0.0/16 14 | subnet: 15 | ip: 10.10.10.1/24 16 | type: cloud 17 | network_zone: eu-central 18 | isolated: 19 | ip_range: 10.20.0.0/16 20 | subnet: 21 | ip: 10.20.10.1/24 22 | 23 | - name: instance-2 24 | image: centos-stream-9 25 | networks: 26 | shared: 27 | subnet: 28 | ip: 10.10.10.2/24 29 | volumes: 30 | - name: volume-1 31 | - name: volume-2 32 | size: 20 33 | 34 | provisioner: 35 | name: ansible 36 | config_options: 37 | defaults: 38 | callback_whitelist: ansible.posix.timer,ansible.posix.profile_tasks,ansible.posix.profile_roles 39 | callbacks_enabled: ansible.posix.timer,ansible.posix.profile_tasks,ansible.posix.profile_roles 40 | 41 | verifier: 42 | name: ansible 43 | -------------------------------------------------------------------------------- /tests/integration/molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify instance-1 3 | hosts: instance-1 4 | tasks: 5 | - name: Ensure correct os family 6 | ansible.builtin.assert: 7 | that: 8 | - ansible_os_family == 'Debian' 9 | 10 | - name: Ensure private are present 11 | ansible.builtin.assert: 12 | that: 13 | - > 14 | '10.10.10.1' in ansible_all_ipv4_addresses and 15 | '10.20.10.1' in ansible_all_ipv4_addresses 16 | 17 | - name: Verify instance-2 18 | hosts: instance-2 19 | tasks: 20 | - name: Ensure correct os family 21 | ansible.builtin.assert: 22 | that: 23 | - ansible_os_family == 'RedHat' 24 | 25 | - name: Ensure volumes are present 26 | ansible.builtin.assert: 27 | that: 28 | - > 29 | ansible_devices | dict2items 30 | | selectattr('value.model', '==', 'Volume') 31 | | list | count == 2 32 | -------------------------------------------------------------------------------- /tests/integration/test_integration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from pytest_ansible.molecule import MoleculeScenario 6 | 7 | 8 | def test_integration(molecule_scenario: MoleculeScenario) -> None: 9 | """ 10 | Run molecule for each scenario. 11 | """ 12 | assert "HCLOUD_TOKEN" in os.environ 13 | 14 | proc = molecule_scenario.test() 15 | assert proc.returncode == 0 16 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-hetznercloud/d3a246f391bda2c4ebe3c30d29e7c0c64b3cd718/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_driver.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from molecule import api 4 | 5 | 6 | def test_driver_is_detected(): 7 | assert "molecule_hetznercloud" in [str(d) for d in api.drivers()] 8 | -------------------------------------------------------------------------------- /tests/unit/test_plugins.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import deepcopy 4 | 5 | import pytest 6 | 7 | from molecule_hetznercloud.playbooks.filter_plugins.get_platforms_data import ( 8 | get_hetznercloud_networks, 9 | get_hetznercloud_subnetworks, 10 | get_hetznercloud_volumes, 11 | ) 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("data", "networks", "subnetworks", "volumes"), 16 | [ 17 | ( 18 | # Data 19 | [ 20 | dict( 21 | name="instance-1", 22 | image="debian-12", 23 | networks={ 24 | "network-1": dict( 25 | ip_range="10.10.0.0/16", 26 | subnet=dict(ip="10.10.10.1/24"), 27 | ) 28 | }, 29 | volumes=[dict(name="volume-1"), dict(size=20)], 30 | ) 31 | ], 32 | # Networks 33 | [{"name": "network-1", "ip_range": "10.10.0.0/16"}], 34 | # Subnetworks 35 | [ 36 | { 37 | "ip": "10.10.10.1/24", 38 | "type": "cloud", 39 | "server_name": "instance-1", 40 | "network_name": "network-1", 41 | } 42 | ], 43 | # Volumes 44 | [ 45 | {"name": "volume-1", "server_name": "instance-1"}, 46 | {"size": 20, "server_name": "instance-1"}, 47 | ], 48 | ), 49 | ( 50 | # Data 51 | [ 52 | dict( 53 | name="instance-1", 54 | image="debian-12", 55 | networks={ 56 | "network-1": dict( 57 | ip_range="10.10.0.0/16", 58 | subnet=dict(ip="10.10.10.1/24"), 59 | ) 60 | }, 61 | volumes=[dict(name="volume-1")], 62 | ), 63 | dict( 64 | name="instance-2", 65 | image="debian-12", 66 | networks={ 67 | "network-1": dict( 68 | subnet=dict(ip="10.10.10.2/24"), 69 | ) 70 | }, 71 | volumes=[dict(size=20)], 72 | ), 73 | ], 74 | # Networks 75 | [{"name": "network-1", "ip_range": "10.10.0.0/16"}], 76 | # Subnetworks 77 | [ 78 | { 79 | "ip": "10.10.10.1/24", 80 | "type": "cloud", 81 | "server_name": "instance-1", 82 | "network_name": "network-1", 83 | }, 84 | { 85 | "ip": "10.10.10.2/24", 86 | "type": "cloud", 87 | "network_name": "network-1", 88 | "server_name": "instance-2", 89 | }, 90 | ], 91 | # Volumes 92 | [ 93 | {"name": "volume-1", "server_name": "instance-1"}, 94 | {"size": 20, "server_name": "instance-2"}, 95 | ], 96 | ), 97 | ], 98 | ) 99 | def test_get_platform_data(data, networks, subnetworks, volumes): 100 | found = get_hetznercloud_networks(deepcopy(data)) 101 | assert found == networks 102 | found = get_hetznercloud_subnetworks(deepcopy(data)) 103 | assert found == subnetworks 104 | found = get_hetznercloud_volumes(deepcopy(data)) 105 | assert found == volumes 106 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.io/en/latest/ 2 | [tox] 3 | min_version = 4.0 4 | env_list = 5 | py{310,311,312,313} 6 | 7 | [testenv] 8 | usedevelop = True 9 | extras = test 10 | deps = 11 | molecule>=24.0 12 | ansible-core>=2.13 13 | commands = 14 | pytest -v {posargs} 15 | passenv = 16 | ANSIBLE_COLLECTIONS_PATH 17 | ANSIBLE_FORCE_COLOR 18 | HCLOUD_TOKEN 19 | PY_COLORS 20 | 21 | [gh-actions] 22 | python = 23 | 3.10: py310 24 | 3.11: py311 25 | 3.12: py312 26 | 3.13: py313 27 | --------------------------------------------------------------------------------